source: Klonkt/src/views/pages/paid-gate.ejs@ c3d12a6

main
Last change on this file since c3d12a6 was c3d12a6, checked in by Robin <roboburr@…>, 7 weeks ago

Feature: a way to actually become a supporter, and a styled expired page

Bart's test surfaced two gaps in the visitor flow:

  • After "Allow" on Patreon, a non-supporter landed on "Nog geen supporter" with no way to actually pledge. The page now shows a primary "Word supporter op Patreon" button. The site owner sets their public Patreon page in Beheer -> Betaalde posts (new field); it's also linked from the gate itself ("Nog geen supporter? Word het op Patreon").
  • The invalid/expired callback replied with a raw res.send() plain-text line. It now renders the normal paid-result page (reason 'expired').

Changed files:
src/config/database.js

  • paid_patreon.patreon_url column (additive)

src/services/PaidPatreonService.js

  • patreonUrl in config/status/save (undefined keeps, empty clears) + patreonUrl(siteId) helper

src/routes/admin-paid.js

  • save patreon_url from the form

src/views/pages/admin-paid.ejs

  • "Openbare Patreon-pagina" field

src/routes/paid.js

  • expired callback renders paid-result; pass patronUrl to the result pages

src/routes/posts.js

  • pass pgPatronUrl to the gate

src/views/pages/paid-gate.ejs

  • "Word supporter" join line under the unlock button

src/views/pages/paid-result.ejs

  • "Word supporter op Patreon" primary button, 'expired' reason, ghost back button always has a label

test/paid-patron.test.js

  • patreonUrl set/keep/clear semantics

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 4.9 KB
Line 
1<div class="container pg-navwrap">
2 <%- include('../partials/post-nav', { newerPost: (typeof newerPost !== 'undefined' ? newerPost : null), olderPost: (typeof olderPost !== 'undefined' ? olderPost : null) }) %>
3</div>
4
5<article class="pg-page" id="pg-page">
6 <% if (typeof pgTitle !== 'undefined' && pgTitle) { %><h1 class="pg-title"><%= pgTitle %></h1><% } %>
7
8 <% if (typeof pgTeaser !== 'undefined' && pgTeaser) { %>
9 <div class="pg-teaser"><p><%= pgTeaser %></p></div>
10 <% } %>
11
12 <section class="pg-card">
13 <div class="pg-lock">💶</div>
14 <h2 class="pg-h2">Voor supporters</h2>
15 <p class="pg-sub">
16 Deze post is voor supporters van deze site. Steun je de maker op Patreon
17 <% if (typeof pgCents !== 'undefined' && pgCents) { %>(vanaf &euro;<%= (pgCents/100).toFixed(2) %>)<% } %>,
18 dan ontgrendel je 'm met je passkey. Geen account, geen cookie.
19 </p>
20 <button type="button" id="pg-unlock" class="pg-btn">Ontgrendelen</button>
21 <p id="pg-status" class="pg-status" hidden></p>
22 <% if (typeof pgPatronUrl !== 'undefined' && pgPatronUrl) { %>
23 <p class="pg-join">Nog geen supporter? <a href="<%= pgPatronUrl %>" target="_blank" rel="noopener">Word het op Patreon</a> en ontgrendel daarna met je passkey.</p>
24 <% } %>
25 </section>
26</article>
27
28<script src="/assets/vendor/simplewebauthn-browser.umd.min.js"></script>
29<script>
30(function () {
31 var base = "<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase ? siteUrlBase : '') %>";
32 var slug = "<%= pgSlug %>";
33 var btn = document.getElementById('pg-unlock');
34 var status = document.getElementById('pg-status');
35 var page = document.getElementById('pg-page');
36 function say(msg, err) { status.hidden = false; status.textContent = msg; status.classList.toggle('is-err', !!err); }
37 function toLink() { location.href = base + '/paid/link?post=' + encodeURIComponent(slug); }
38
39 if (!window.SimpleWebAuthnBrowser || !window.PublicKeyCredential) { btn.textContent = 'Word supporter'; btn.addEventListener('click', toLink); return; }
40
41 btn.addEventListener('click', function () {
42 btn.disabled = true;
43 say('Bevestig met je passkey…');
44 fetch(base + '/paid/challenge?post=' + encodeURIComponent(slug))
45 .then(function (r) { if (!r.ok) throw { link: true }; return r.json(); })
46 .then(function (data) {
47 return window.SimpleWebAuthnBrowser.startAuthentication({ optionsJSON: data.options })
48 .then(function (response) {
49 return fetch(base + '/paid/unlock', {
50 method: 'POST', headers: { 'Content-Type': 'application/json' },
51 body: JSON.stringify({ response: response, blob: data.blob }),
52 });
53 });
54 })
55 .then(function (r) { return r.json().then(function (j) { return { status: r.status, j: j }; }); })
56 .then(function (res) {
57 if (res.j && res.j.ok) {
58 // Swap the gate for the full post, client-side (no cookie kept).
59 var h = document.createElement('div');
60 h.innerHTML = (res.j.title ? '<h1 class="post-title">' + res.j.title + '</h1>' : '') +
61 '<div class="post-content">' + res.j.html + '</div>';
62 page.replaceWith(h);
63 } else if (res.status === 403) {
64 toLink(); // no valid passkey yet (or lapsed tier): link via Patreon
65 } else {
66 btn.disabled = false; say('Ontgrendelen mislukt. Probeer opnieuw.', true);
67 }
68 })
69 .catch(function (e) {
70 if (e && e.link) { toLink(); return; }
71 if (e && e.name === 'NotAllowedError') { toLink(); return; } // cancelled / no passkey -> link
72 btn.disabled = false; say('Er ging iets mis. Probeer opnieuw.', true);
73 });
74 });
75})();
76</script>
77
78<style>
79 .pg-navwrap { max-width: 720px; margin: 0.5rem auto 1.5rem; padding: 0 1rem; }
80 .pg-page { max-width: 720px; margin: 0 auto 3rem; padding: 0 1rem; }
81 .pg-title { font-family: var(--font-display, serif); font-size: clamp(1.6rem, 4vw, 2.2rem); margin: 0 0 1rem; }
82 .pg-teaser { font-family: var(--font-body, serif); font-size: 1.1rem; line-height: 1.7; color: var(--ink); opacity: .95;
83 -webkit-mask-image: linear-gradient(180deg, #000 55%, transparent); mask-image: linear-gradient(180deg, #000 55%, transparent); }
84 .pg-card { margin: 1.5rem 0 0; border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); border-radius: 18px; padding: 30px 26px; text-align: center; }
85 .pg-lock { font-size: 38px; margin-bottom: 6px; }
86 .pg-h2 { font-size: 22px; margin: 0 0 8px; }
87 .pg-sub { opacity: .85; line-height: 1.6; margin: 0 auto 12px; max-width: 34em; }
88 .pg-btn { padding: 12px 24px; border: none; border-radius: 10px; font: inherit; font-weight: 600; cursor: pointer;
89 background: var(--accent, #6b8f71); color: #fff; }
90 .pg-btn:disabled { opacity: .6; cursor: default; }
91 .pg-status { margin: 12px 0 0; opacity: .9; }
92 .pg-status.is-err { color: #c0392b; }
93 .pg-join { margin: 14px 0 0; font-size: .92rem; opacity: .8; }
94</style>
Note: See TracBrowser for help on using the repository browser.