diff --git a/src/index.html b/src/index.html index 62f3153..e4069c8 100644 --- a/src/index.html +++ b/src/index.html @@ -5780,6 +5780,43 @@ // sent for gated templates. Matching receipts auto-unlock without // re-payment, so a cache wipe / private window / new device feels // seamless. + + // Premium events authored by the current user should appear unlocked + // automatically — a creator should never have to zap themselves to + // open their own design. The soft-gate KDF is deterministic from + // public material + the static client pepper, so we can decrypt + // locally without involving relays / receipts. + async function autoUnlockSelfAuthoredPremium(gatedEvents) { + if (!Array.isArray(gatedEvents) || !gatedEvents.length) return; + const myPubkey = normalizePubkey(state.community?.myPubkey || ''); + if (!myPubkey) return; + for (const ev of gatedEvents) { + if (normalizePubkey(ev.pubkey) !== myPubkey) continue; + if (!isPremiumEvent(ev)) continue; + if (isUnlockedLocally(ev.id)) continue; + let envelope = null; + let decryptedPayload = null; + try { + envelope = JSON.parse(ev.content || '{}'); + decryptedPayload = await Premium.decryptPayload(envelope, { + creatorPubkey: ev.pubkey, + dTag: premiumEventDTag(ev), + schemaVersion: envelope?.payloadSchemaVersion || 5, + }); + } catch (err) { + console.warn('[premium] self-author auto-decrypt failed for', ev.id, err); + continue; + } + const addressCoord = premiumAddressCoord(ev); + markUnlocked(ev.id, { + source: 'self-authored', + address: addressCoord, + payload: decryptedPayload, + previewJpegDataUrl: envelope?.preview?.dataUrl || undefined, + }); + } + } + // Relay fast-path. Premium designs use address-based purchase scope // (PREMIUM_DESIGNS_FEATURE.md §13.2) so a creator's edit-in-place // doesn't invalidate prior unlocks. We query by `#a` first, then `#e` @@ -19180,6 +19217,9 @@ // tag-only events fall back to event-id receipts. const gatedEvents = templateEvents.filter((e) => isPremiumEvent(e) || isZapGated(e)); if (gatedEvents.length && state.community?.myPubkey) { + // Self-authored auto-unlock + receipt fast-path run in + // parallel — they target different events (mine vs. paid-for). + autoUnlockSelfAuthoredPremium(gatedEvents).then(() => notifyFeedProgress()).catch(() => {}); relayFastPathUnlockCheck(gatedEvents).then(() => notifyFeedProgress()).catch(() => {}); } } @@ -22311,17 +22351,62 @@ content.style.display = ''; // Locked premium row → render the watermarked JPEG instead of the - // (unavailable) plaintext design. Add an honest "soft-gate" note. + // (unavailable) plaintext design. For deck premium with multiple + // slide previews, paginate so the buyer can flip through. if (row.premium && !row.premium.decrypted) { const wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;flex-direction:column;align-items:center;gap:8px;padding:8px'; - if (row.premium.previewJpegDataUrl) { + + const previewSlides = Array.isArray(row.premium.previewSlides) + ? row.premium.previewSlides + : null; + + if (previewSlides && previewSlides.length > 1) { + // Paginated viewer for deck premium previews. + const img = document.createElement('img'); + img.alt = row.title || 'Premium deck preview'; + img.style.cssText = 'max-width:100%;height:auto;border:1px solid var(--line);background:#fff'; + const bar = document.createElement('div'); + bar.style.cssText = 'display:flex;align-items:center;justify-content:center;gap:10px;font-size:.9rem;color:var(--ink);padding:4px 0'; + const prev = document.createElement('button'); + prev.type = 'button'; + prev.textContent = '‹ Prev'; + prev.style.cssText = 'padding:4px 12px;cursor:pointer'; + const counter = document.createElement('span'); + counter.style.cssText = 'min-width:11em;text-align:center;font-variant-numeric:tabular-nums'; + const next = document.createElement('button'); + next.type = 'button'; + next.textContent = 'Next ›'; + next.style.cssText = 'padding:4px 12px;cursor:pointer'; + bar.append(prev, counter, next); + wrap.append(img, bar); + const totalDeckSlides = row.premium.previewTotalSlides || previewSlides.length; + let idx = 0; + function render() { + const slide = previewSlides[idx]; + img.src = slide?.dataUrl || ''; + const shown = previewSlides.length; + counter.textContent = totalDeckSlides > shown + ? `Slide ${idx + 1} of ${shown} · ${totalDeckSlides} in deck` + : `Slide ${idx + 1} of ${shown}`; + } + prev.addEventListener('click', () => { + idx = (idx - 1 + previewSlides.length) % previewSlides.length; + render(); + }); + next.addEventListener('click', () => { + idx = (idx + 1) % previewSlides.length; + render(); + }); + render(); + } else if (row.premium.previewJpegDataUrl) { const img = document.createElement('img'); img.src = row.premium.previewJpegDataUrl; img.alt = row.title || 'Premium template preview'; img.style.cssText = 'max-width:100%;height:auto;border:1px solid var(--line);background:#fff'; wrap.appendChild(img); } + const note = document.createElement('p'); note.className = 'hint'; note.style.cssText = 'margin:0;text-align:center;max-width:520px';