diff --git a/src/index.html b/src/index.html index 4c84095..09740fd 100644 --- a/src/index.html +++ b/src/index.html @@ -7422,6 +7422,22 @@ document.querySelectorAll(`[data-event-id="${eventId}"]`).forEach((el) => el.remove()); } catch (err) { console.warn('[zap-gated] markUnlocked failed', err); } } + function _premiumClaimUntil(row) { + return Number( + row?.premium?.envelope?.softgate?.claimUntil + || premiumEventTagValue(row?.e, 'claim-until') + || 0 + ) || 0; + } + function _premiumClaimStatusForRow(row) { + const helpers = window.ArtstrPremium2; + const address = row?.premium?.address || (row?.e ? premiumAddressCoord(row.e) : ''); + return helpers?.claimStatus?.({ + claimUntil: _premiumClaimUntil(row), + owned: !!(row?.zapGate?.unlocked || isUnlockedLocally(row?.e?.id, address)), + selfAuthored: !!(row?.e?.pubkey && normalizePubkey(row.e.pubkey) === normalizePubkey(state.community?.myPubkey || '')), + }) || 'claimable'; + } // Relay fast-path — on feed load, query for zap receipts we've already // sent for gated templates. Matching receipts auto-unlock without @@ -7607,6 +7623,11 @@ async function openUnlockConfirm(row) { const eventId = row?.e?.id; if (!eventId) return; + const claimState = _premiumClaimStatusForRow(row); + if (claimState === 'expired' || claimState === 'closed') { + toastError('This premium design is no longer claimable from the public storefront. Already-purchased private copies still open from your vault.'); + return; + } if (!nwc.isConnected()) { toastError('Connect a Lightning wallet first (Profile → Settings → ⚡ Lightning wallet).'); // Open Settings + expand the wallet panel so the user can act on @@ -34611,10 +34632,17 @@ // only on importing into the editor. const locked = row.zapGate?.gated && !row.zapGate?.unlocked; if (locked) { - btn.textContent = `⚡ Unlock for ${row.zapGate.min} sats`; - btn.className = 'primary'; - btn.title = `Premium template — zap to unlock (creator + ${(PLATFORM_FEE_BPS / 100).toFixed(0)}% Artstr platform fee).`; - btn.addEventListener('click', e => { e.stopPropagation(); openUnlockConfirm(row); }); + const claimState = _premiumClaimStatusForRow(row); + if (claimState === 'expired' || claimState === 'closed') { + btn.textContent = 'Claim period ended'; + btn.disabled = true; + btn.title = 'This public premium storefront is no longer claimable. Purchased private copies still open from the vault.'; + } else { + btn.textContent = `⚡ Unlock for ${row.zapGate.min} sats`; + btn.className = 'primary'; + btn.title = `Premium template — zap to unlock (creator + ${(PLATFORM_FEE_BPS / 100).toFixed(0)}% Artstr platform fee).`; + btn.addEventListener('click', e => { e.stopPropagation(); openUnlockConfirm(row); }); + } } else { btn.textContent = 'Use/Fork'; btn.title = 'Fork this design — opens it in the editor so you can save your own version'; @@ -35605,13 +35633,20 @@ const forkBtn = document.createElement('button'); forkBtn.type = 'button'; if (lockedPremium) { - forkBtn.textContent = `⚡ Unlock for ${row.zapGate?.min || 100} sats`; - forkBtn.className = 'primary'; - forkBtn.title = `Premium template — zap to unlock (creator + ${(PLATFORM_FEE_BPS / 100).toFixed(0)}% Artstr platform fee).`; - forkBtn.addEventListener('click', (e) => { - e.stopPropagation(); - openUnlockConfirm(row); - }); + const claimState = _premiumClaimStatusForRow(row); + if (claimState === 'expired' || claimState === 'closed') { + forkBtn.textContent = 'Claim period ended'; + forkBtn.disabled = true; + forkBtn.title = 'This public premium storefront is no longer claimable. Purchased private copies still open from the vault.'; + } else { + forkBtn.textContent = `⚡ Unlock for ${row.zapGate?.min || 100} sats`; + forkBtn.className = 'primary'; + forkBtn.title = `Premium template — zap to unlock (creator + ${(PLATFORM_FEE_BPS / 100).toFixed(0)}% Artstr platform fee).`; + forkBtn.addEventListener('click', (e) => { + e.stopPropagation(); + openUnlockConfirm(row); + }); + } } else { forkBtn.textContent = 'Use/Fork'; forkBtn.title = 'Fork this design — opens it in the editor so you can save your own version'; diff --git a/tests/premium/premium2-index-integration.test.mjs b/tests/premium/premium2-index-integration.test.mjs index b1600f5..6a13428 100644 --- a/tests/premium/premium2-index-integration.test.mjs +++ b/tests/premium/premium2-index-integration.test.mjs @@ -48,6 +48,19 @@ test('soft-gate crypto supports the active Premium 2.0 policy epoch', async () = assert.match(html, /const CURRENT_EPOCH = '2026-07'/); }); +test('index page blocks expired first-time premium claims but keeps owned paths separate', async () => { + const html = await indexHtml(); + + assert.match(html, /function _premiumClaimUntil\(row\)/); + assert.match(html, /function _premiumClaimStatusForRow\(row\)/); + assert.match(html, /owned: !!\(row\?\.zapGate\?\.unlocked \|\| isUnlockedLocally\(row\?\.e\?\.id, address\)\)/); + assert.match(html, /const claimState = _premiumClaimStatusForRow\(row\);\n\s+if \(claimState === 'expired' \|\| claimState === 'closed'\)/); + assert.match(html, /This premium design is no longer claimable from the public storefront/); + assert.match(html, /btn\.textContent = 'Claim period ended'/); + assert.match(html, /forkBtn\.textContent = 'Claim period ended'/); + assert.match(html, /Purchased private copies still open from the vault/); +}); + test('vault item stubs preserve Premium 2.0 private-copy fields', async () => { const html = await indexHtml(); const stubStart = html.indexOf('function _vaultItemStub(item)');