From 170756f72227020d270408450153328515a7fe87 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:03:16 -0400 Subject: [PATCH] Wire Premium 2.0 private copy unlock path --- src/index.html | 139 ++++++++++++++++-- .../premium2-index-integration.test.mjs | 41 ++++++ 2 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 tests/premium/premium2-index-integration.test.mjs diff --git a/src/index.html b/src/index.html index 625cd10..7f58805 100644 --- a/src/index.html +++ b/src/index.html @@ -3921,6 +3921,10 @@ cp noble-bundle.min.js /src/ --> +
@@ -7685,17 +7689,19 @@ let vaultErr = null; if (row.premium && decryptedPayload) { if (statusToast?.dismiss) statusToast.dismiss(); - statusToast = toast('Unlocked. Syncing to your purchase vault…', { duration: 30000 }); - const vaultItem = _buildVaultItemForUnlock({ - row, - totalSats: min, - creatorSats: result.creatorSats, - platformSats: result.platformSats, - creatorPreimage: result.creatorPreimage, - platformPreimage: result.platformPreimage, - payload: decryptedPayload, - }); + statusToast = toast('Unlocked. Saving your private purchased copy…', { duration: 30000 }); try { + const vaultItem = await _publishPurchaseCopyAndBuildVaultItem({ + row, + totalSats: min, + creatorSats: result.creatorSats, + platformSats: result.platformSats, + creatorPreimage: result.creatorPreimage, + platformPreimage: result.platformPreimage, + payload: decryptedPayload, + }); + if (statusToast?.dismiss) statusToast.dismiss(); + statusToast = toast('Private copy saved. Syncing purchase vault…', { duration: 30000 }); await upsertPurchaseVaultItem(vaultItem); vaultSynced = true; } catch (err) { @@ -7824,13 +7830,20 @@ // fields — the per-item event carries those. function _vaultItemStub(item) { return { + schema: item.schema, a: item.a, eventId: item.eventId, + privateCopy: item.privateCopy, + privateCopyEventId: item.privateCopyEventId, creatorPubkey: item.creatorPubkey, title: item.title, mode: item.mode, unlockedAt: item.unlockedAt, amountSats: item.amountSats, + claimEpoch: item.claimEpoch, + softgateEpoch: item.softgateEpoch, + licenseType: item.licenseType, + receiptSummary: item.receiptSummary, }; } @@ -8013,6 +8026,93 @@ return await _publishVault(next); } + function _parseAddressCoord(coord) { + if (typeof coord !== 'string') return null; + const parts = coord.split(':'); + if (parts.length < 3) return null; + const kind = Number(parts[0]); + const pubkey = parts[1] || ''; + const dTag = parts.slice(2).join(':'); + if (!Number.isFinite(kind) || !pubkey || !dTag) return null; + return { kind, pubkey, dTag }; + } + + async function _loadPurchaseCopyPayload(privateCopyCoord) { + const parsed = _parseAddressCoord(privateCopyCoord); + if (!parsed) throw new Error('Purchase copy pointer is malformed.'); + const events = await queryRelays(_vaultRelayUnion(), { + kinds: [parsed.kind], + authors: [parsed.pubkey], + '#d': [parsed.dTag], + limit: 5, + }, { timeoutMs: 4500 }); + const latest = (events || []).filter((e) => e?.content) + .sort((a, b) => (b.created_at || 0) - (a.created_at || 0))[0]; + if (!latest) throw new Error('Private purchase copy was not found on relays.'); + const plaintext = await _selfDecrypt(latest.content); + const content = JSON.parse(plaintext); + if (content?.kind !== 'artstr-purchase-copy' || !content?.payload) { + throw new Error('Private purchase copy content was not valid.'); + } + return { copy: content, event: latest }; + } + + async function _publishPurchaseCopyAndBuildVaultItem({ row, totalSats, creatorSats, platformSats, creatorPreimage, platformPreimage, payload }) { + const helpers = window.ArtstrPremium2; + if (!helpers?.buildPurchaseCopyPlaintext || !helpers?.buildPurchaseCopyEventDraft || !helpers?.buildVaultV2Item || !helpers?.buildPurchaseCopyCoordinate) { + throw new Error('Premium 2.0 helper module did not load.'); + } + const source = { + a: row.premium?.address || '', + eventId: row.e?.id || '', + creatorPubkey: row.e?.pubkey || '', + title: row.title || '', + premiumMode: row.premium?.envelope?.premiumMode || premiumEventTagValue(row.e, 'premium-mode') || 'softgate-v1', + softgateEpoch: row.premium?.envelope?.softgate?.epoch || premiumEventTagValue(row.e, 'softgate-epoch') || '', + claimEpoch: row.premium?.envelope?.softgate?.claimEpoch || premiumEventTagValue(row.e, 'claim-epoch') || '', + }; + const purchase = { + buyerPubkey: state.community?.myPubkey || '', + amountSats: totalSats, + unlockedAt: Math.floor(Date.now() / 1000), + receipts: [ + { role: 'creator', recipientPubkey: row.e?.pubkey || '', amountSats: creatorSats, preimage: creatorPreimage || '' }, + { role: 'platform', recipientPubkey: platform.getPubkeyHex(), amountSats: platformSats, preimage: platformPreimage || '' }, + ], + }; + const license = { + type: row.premium?.envelope?.license?.type || 'personal-use-template', + snapshot: true, + sourceCanChange: true, + }; + const copyPlaintext = helpers.buildPurchaseCopyPlaintext({ + source, + purchase, + license, + payload, + payloadSchemaVersion: row.premium?.envelope?.payloadSchemaVersion || payload?.version || 5, + }); + const encryptedContent = await _selfEncrypt(JSON.stringify(copyPlaintext)); + const copyDraft = await helpers.buildPurchaseCopyEventDraft({ + sourceAddress: source.a, + encryptedContent, + }); + const signedCopy = await signNostrEvent(copyDraft); + await publishToRelays(signedCopy, _vaultRelayUnion()); + const privateCopy = await helpers.buildPurchaseCopyCoordinate({ + buyerPubkey: state.community?.myPubkey || '', + sourceAddress: source.a, + }); + return helpers.buildVaultV2Item({ + source, + privateCopy, + privateCopyEventId: signedCopy.id || '', + purchase, + mode: row.mode || payload?.templateMode || '', + licenseType: license.type, + }); + } + function _buildVaultItemForUnlock({ row, totalSats, creatorSats, platformSats, creatorPreimage, platformPreimage, payload }) { return { a: row.premium?.address || '', @@ -8044,15 +8144,26 @@ let unavailable = 0; for (const it of items) { if (!it?.eventId) continue; - if (!it?.payload) { unavailable++; continue; } + let payload = it.payload || null; + let previewJpegDataUrl = it.previewJpegDataUrl || undefined; + if (!payload && it.privateCopy) { + try { + const restored = await _loadPurchaseCopyPayload(it.privateCopy); + payload = restored.copy?.payload || null; + previewJpegDataUrl = restored.copy?.previewJpegDataUrl || previewJpegDataUrl; + } catch (err) { + console.warn('[vault] private purchase-copy restore failed', it.privateCopy, err); + } + } + if (!payload) { unavailable++; continue; } if (isUnlockedLocally(it.eventId, it.a)) continue; // Store under the LATEST event id from the vault, but include // the address so a future edit-in-place still picks this up. markUnlocked(it.eventId, { - source: 'vault', + source: it.privateCopy ? 'vault-private-copy' : 'vault', address: it.a || '', - payload: it.payload || undefined, - previewJpegDataUrl: it.previewJpegDataUrl || undefined, + payload, + previewJpegDataUrl, amountSats: it.amountSats, unlockedAt: it.unlockedAt, }); diff --git a/tests/premium/premium2-index-integration.test.mjs b/tests/premium/premium2-index-integration.test.mjs new file mode 100644 index 0000000..1d1a7cd --- /dev/null +++ b/tests/premium/premium2-index-integration.test.mjs @@ -0,0 +1,41 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import test from 'node:test'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const indexPath = join(__dirname, '..', '..', 'src', 'index.html'); + +async function indexHtml() { + return readFile(indexPath, 'utf8'); +} + +test('index page wires Premium 2.0 private-copy unlock flow', async () => { + const html = await indexHtml(); + + assert.match(html, /import \* as Premium2 from '\.\/premium2-helpers\.mjs';/); + assert.match(html, /window\.ArtstrPremium2 = Premium2;/); + assert.match(html, /async function _publishPurchaseCopyAndBuildVaultItem/); + assert.match(html, /helpers\.buildPurchaseCopyPlaintext/); + assert.match(html, /helpers\.buildPurchaseCopyEventDraft/); + assert.match(html, /helpers\.buildVaultV2Item/); + assert.match(html, /await _loadPurchaseCopyPayload\(it\.privateCopy\)/); + assert.match(html, /source: it\.privateCopy \? 'vault-private-copy' : 'vault'/); + assert.match(html, /Unlocked\. Saving your private purchased copy/); + assert.match(html, /Private copy saved\. Syncing purchase vault/); +}); + +test('vault item stubs preserve Premium 2.0 private-copy fields', async () => { + const html = await indexHtml(); + const stubStart = html.indexOf('function _vaultItemStub(item)'); + const stubEnd = html.indexOf('async function _publishVaultEvent', stubStart); + const stubBody = html.slice(stubStart, stubEnd); + + assert.ok(stubStart > -1, '_vaultItemStub should exist'); + assert.ok(stubEnd > stubStart, '_publishVaultEvent should follow _vaultItemStub'); + assert.match(stubBody, /privateCopy: item\.privateCopy/); + assert.match(stubBody, /privateCopyEventId: item\.privateCopyEventId/); + assert.match(stubBody, /claimEpoch: item\.claimEpoch/); + assert.match(stubBody, /receiptSummary: item\.receiptSummary/); +});