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

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

Fix: unlocked post renders via its own page; no duplicate supporter button

Two things the tester hit once the passkey flow worked:

  1. Duplicate "Word supporter". The gate's no-WebAuthn fallback relabelled the unlock button to "Word supporter", which sat next to the real "Word supporter op Patreon" button. In the two-button layout that fallback now hides the (unusable) unlock button instead; the single-button layout keeps the relabel since it's the only button.
  1. The unlocked content sat in a bare <div>, so it lost the post page's layout and scoped typography ("div niet in een goeie div"). Instead of injecting HTML into the gate, /paid/unlock now returns a short-lived (120s) signed unlock capability and the client reloads the real post URL with it (?u=). GET /:slug renders the FULL post through its normal template when the capability is valid: correct wrapper, scoped styles, and working audio players (which bind on load and couldn't init in injected HTML). Not a cookie and not stored: the token lives only in that one URL and expires.

Changed files:
src/routes/paid.js

  • /unlock returns { ok, redirect } with a 120s unlock blob; drop the renderPostBodyHtml import (no longer injected here)

src/routes/posts.js

  • a valid ?u= capability bypasses the paid gate and renders the post

src/views/pages/paid-gate.ejs

  • no-WebAuthn fallback hides the unlock button when a Patreon page exists; success reloads the real post via the redirect

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

  • Property mode set to 100644
File size: 5.6 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 <% var _hasPatron = (typeof pgPatronUrl !== 'undefined' && pgPatronUrl); %>
13 <section class="pg-card">
14 <div class="pg-lock">💶</div>
15 <h2 class="pg-h2">Voor supporters</h2>
16 <p class="pg-sub">
17 Deze post is voor supporters van deze site
18 <% if (typeof pgCents !== 'undefined' && pgCents) { %>(vanaf &euro;<%= (pgCents/100).toFixed(2) %> per maand op Patreon)<% } %>.
19 Word supporter en ontgrendel 'm daarna met een passkey. Geen account op deze site, geen cookie.
20 </p>
21
22 <div class="pg-actions">
23 <% if (_hasPatron) { %>
24 <a class="pg-btn" href="<%= pgPatronUrl %>" target="_blank" rel="noopener">Word supporter op Patreon</a>
25 <button type="button" id="pg-unlock" class="pg-btn pg-btn-ghost">Al supporter? Ontgrendelen</button>
26 <% } else { %>
27 <button type="button" id="pg-unlock" class="pg-btn">Ontgrendelen met Patreon</button>
28 <% } %>
29 </div>
30 <p id="pg-status" class="pg-status" hidden></p>
31 </section>
32</article>
33
34<script src="/assets/vendor/simplewebauthn-browser.umd.min.js"></script>
35<script>
36(function () {
37 var base = "<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase ? siteUrlBase : '') %>";
38 var slug = "<%= pgSlug %>";
39 var hasPatron = <%= _hasPatron ? 'true' : 'false' %>;
40 var btn = document.getElementById('pg-unlock');
41 var status = document.getElementById('pg-status');
42 function say(msg, err) { status.hidden = false; status.textContent = msg; status.classList.toggle('is-err', !!err); }
43 function toLink() { location.href = base + '/paid/link?post=' + encodeURIComponent(slug); }
44
45 // No WebAuthn here: an assertion is impossible. With a Patreon page there is
46 // already a "Word supporter" button, so hide the (dead) unlock button rather
47 // than turn it into a second "Word supporter". Without one, this IS the button.
48 if (!window.SimpleWebAuthnBrowser || !window.PublicKeyCredential) {
49 if (hasPatron) { btn.style.display = 'none'; }
50 else { btn.textContent = 'Word supporter'; btn.addEventListener('click', toLink); }
51 return;
52 }
53
54 btn.addEventListener('click', function () {
55 btn.disabled = true;
56 say('Bevestig met je passkey…');
57 fetch(base + '/paid/challenge?post=' + encodeURIComponent(slug))
58 .then(function (r) { if (!r.ok) throw { link: true }; return r.json(); })
59 .then(function (data) {
60 return window.SimpleWebAuthnBrowser.startAuthentication({ optionsJSON: data.options })
61 .then(function (response) {
62 return fetch(base + '/paid/unlock', {
63 method: 'POST', headers: { 'Content-Type': 'application/json' },
64 body: JSON.stringify({ response: response, blob: data.blob }),
65 });
66 });
67 })
68 .then(function (r) { return r.json().then(function (j) { return { status: r.status, j: j }; }); })
69 .then(function (res) {
70 if (res.j && res.j.ok && res.j.redirect) {
71 // Reload the real post page via the one-shot unlock capability, so it
72 // renders through its normal template (layout, styles, audio).
73 location.href = res.j.redirect;
74 } else if (res.status === 403) {
75 toLink(); // no valid passkey yet (or lapsed tier): link via Patreon
76 } else {
77 btn.disabled = false; say('Ontgrendelen mislukt. Probeer opnieuw.', true);
78 }
79 })
80 .catch(function (e) {
81 if (e && e.link) { toLink(); return; }
82 if (e && e.name === 'NotAllowedError') { toLink(); return; } // cancelled / no passkey -> link
83 btn.disabled = false; say('Er ging iets mis. Probeer opnieuw.', true);
84 });
85 });
86})();
87</script>
88
89<style>
90 .pg-navwrap { max-width: 720px; margin: 0.5rem auto 1.5rem; padding: 0 1rem; }
91 .pg-page { max-width: 720px; margin: 0 auto 3rem; padding: 0 1rem; }
92 .pg-title { font-family: var(--font-display, serif); font-size: clamp(1.6rem, 4vw, 2.2rem); margin: 0 0 1rem; }
93 .pg-teaser { font-family: var(--font-body, serif); font-size: 1.1rem; line-height: 1.7; color: var(--ink); opacity: .95;
94 -webkit-mask-image: linear-gradient(180deg, #000 55%, transparent); mask-image: linear-gradient(180deg, #000 55%, transparent); }
95 .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; }
96 .pg-lock { font-size: 38px; margin-bottom: 6px; }
97 .pg-h2 { font-size: 22px; margin: 0 0 8px; }
98 .pg-sub { opacity: .85; line-height: 1.6; margin: 0 auto 12px; max-width: 34em; }
99 .pg-btn { display: inline-block; text-align: center; text-decoration: none; padding: 12px 24px; border: none; border-radius: 10px; font: inherit; font-weight: 600; cursor: pointer;
100 background: var(--accent, #6b8f71); color: #fff; }
101 .pg-btn:disabled { opacity: .6; cursor: default; }
102 .pg-btn:hover { filter: brightness(1.05); }
103 .pg-actions { display: flex; flex-direction: column; gap: 10px; align-items: stretch; max-width: 320px; margin: 0 auto; }
104 .pg-btn-ghost { background: transparent; color: var(--ink, #222); border: 1px solid color-mix(in srgb, var(--ink, #000) 22%, transparent); }
105 .pg-status { margin: 12px 0 0; opacity: .9; }
106 .pg-status.is-err { color: #c0392b; }
107</style>
Note: See TracBrowser for help on using the repository browser.