Add creator premium epoch refresh action

This commit is contained in:
thePR0M3TH3AN committed 2026-07-04 16:39:42 -04:00
1 parent 37d871431b
commit b980215f15
3 files changed
+129 -4

No files matched your search

+22 -4
View File
@@ -547,10 +547,28 @@ Recommended defaults:
Claim windows should never be described as cryptographic expiry.
If an epoch is manually closed and a creator wants an old public listing to
be purchasable again, the creator should update/republish the listing under
the current active policy. The new public event gets the new active epoch and
becomes a fresh storefront. Existing buyers keep their older private purchase
copies as snapshots of what they bought.
be purchasable again, the creator should use the owner-only `Refresh epoch`
action or otherwise update/republish the listing under the current active
policy. The new public event reuses the same replaceable-event `d` tag, gets
the new active epoch, and becomes the current storefront for that address.
Existing buyers keep their older private purchase copies as snapshots of what
they bought.
The `Refresh epoch` action does not silently publish. It:
1. decrypts the creator's existing premium payload;
2. loads it into the editor without requiring design edits;
3. enters normal edit/update mode for the original `d` tag;
4. enables Premium with the existing price;
5. opens the standard publish confirmation;
6. publishes through the normal Premium path, which stamps the current admin
policy and encrypts with the current soft-gate epoch.
If old epoch crypto has been reverse-engineered, that old derivation should
not unlock the refreshed listing because the refreshed event has a newly
encrypted payload under the active epoch. This is still a static-client
soft gate, not hard DRM, but it prevents old-epoch bypass tooling from
automatically carrying forward to the refreshed storefront.
---
+95
View File
@@ -31413,6 +31413,77 @@
});
}
async function resolvePremiumPayloadForRepublish(row) {
if (!row?.premium && !isPremiumEvent(row?.e)) {
throw new Error('This is not a Premium 2.0 design.');
}
const address = row?.premium?.address || premiumAddressCoord(row.e);
const cached = findUnlockedEntry(row.e?.id, address);
if (cached?.payload) return cached.payload;
const envelope = row?.premium?.envelope || JSON.parse(row.e?.content || '{}');
return await Premium.decryptPayload(envelope, {
creatorPubkey: row.e?.pubkey || '',
dTag: row?.premium?.dTag || premiumEventDTag(row.e),
schemaVersion: envelope?.payloadSchemaVersion || 5,
});
}
async function republishPremiumUnderCurrentEpoch(row) {
if (!rowIsOwnedByMe(row) || !rowDTag(row)) {
toastError('Only the creator can refresh a premium listing epoch.');
return;
}
if (!row?.premium && !isPremiumEvent(row?.e)) {
toastError('This is not a premium listing.');
return;
}
const ok = confirm(
'Republish this premium listing under the current Artstr premium epoch?\n\n' +
'This replaces the public storefront event using the latest admin policy. Existing buyers keep their private purchased snapshots.'
);
if (!ok) return;
let payload;
try {
payload = await resolvePremiumPayloadForRepublish(row);
} catch (err) {
toastError(`Could not decrypt this premium design for republish: ${err?.message || err}`);
return;
}
const mode = normalizeTemplateMode(payload);
try {
if (mode === 'disc-design') {
importDiscDesignFromPayload(payload, 'a', { openDesigner: true });
} else {
loadProjectFromText(JSON.stringify(payload));
}
} catch (err) {
toastError(`Could not load this design for republish: ${err?.message || err}`);
return;
}
enterEditMode({
dTag: rowDTag(row),
kind: row.e?.kind || 30078,
eventId: row.e?.id || '',
title: row.title || payload?.meta?.title || 'Untitled design',
});
state.visibility = 'public';
state.publish = {
...(state.publish || {}),
zapGate: true,
zapMin: getZapMin(row.e) || state.publish?.zapMin || 100,
};
$('nostrModal')?.classList.remove('open');
const profileModalEl = $('profileModal');
if (profileModalEl && profileModalEl.getAttribute('aria-hidden') === 'false') {
closeProfilePage({ popState: false });
}
toast('Premium listing loaded. Review and publish the epoch refresh.', { duration: 5000 });
setTimeout(() => {
const publishBtn = mode === 'disc-design' ? $('designerPublishBtn') : $('communityPublishBtn');
publishBtn?.click();
}, 0);
}
// ---- Publish-confirm modal: preview + metadata review before posting ----
const MODE_LABELS_PRETTY = {
@@ -35187,9 +35258,11 @@
}
const tipBtn = makeFeedCardTipButton(row);
const editBtn = makeFeedCardEditButton(row);
const refreshEpochBtn = makeFeedCardRefreshEpochButton(row);
const isMine = rowIsOwnedByMe(row);
actions.append(makeReactionBtn(row), btn);
if (editBtn) actions.appendChild(editBtn);
if (refreshEpochBtn) actions.appendChild(refreshEpochBtn);
const moreMenu = makeActionMenu([
blockAuthorAction(row, 'card'),
reportTemplateAction(row)
@@ -35225,6 +35298,19 @@
return btn;
}
function makeFeedCardRefreshEpochButton(row) {
if (!rowIsOwnedByMe(row) || !rowDTag(row) || !row?.premium) return null;
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = 'Refresh epoch';
btn.title = 'Republish this premium listing under the current Artstr premium policy without changing the design';
btn.addEventListener('click', e => {
e.stopPropagation();
republishPremiumUnderCurrentEpoch(row);
});
return btn;
}
function makeFeedCardTipButton(row) {
const pk = row?.e?.pubkey;
if (!pk) return null;
@@ -36078,6 +36164,15 @@
editBtn.addEventListener('click', () => startEditFromRow(row));
actionsDiv.insertBefore(editBtn, saveJsonBtn);
if (row.premium) {
const refreshEpochBtn = document.createElement('button');
refreshEpochBtn.type = 'button';
refreshEpochBtn.textContent = 'Refresh epoch';
refreshEpochBtn.title = 'Republish this premium listing under the current Artstr premium policy without changing the design';
refreshEpochBtn.addEventListener('click', () => republishPremiumUnderCurrentEpoch(row));
actionsDiv.insertBefore(refreshEpochBtn, saveJsonBtn);
}
// Owner-only Visibility toggle — flip a published design between
// public (feed-listed) and private (encrypted) in place.
const makePrivate = !row.private;
@@ -153,6 +153,18 @@ test('index page refreshes the Purchased tab after a successful Premium 2.0 purc
assert.match(html, /refreshPurchasedTabIfVisible\(\{ force: false \}\)/);
});
test('index page exposes creator premium epoch refresh for existing listings', async () => {
const html = await indexHtml();
assert.match(html, /async function republishPremiumUnderCurrentEpoch\(row\)/);
assert.match(html, /async function resolvePremiumPayloadForRepublish\(row\)/);
assert.match(html, /makeFeedCardRefreshEpochButton\(row\)/);
assert.match(html, /btn\.textContent = 'Refresh epoch'/);
assert.match(html, /state\.publish = \{\n\s+\.\.\.\(state\.publish \|\| \{\}\),\n\s+zapGate: true,/);
assert.match(html, /publishBtn\?\.click\(\)/);
assert.match(html, /Existing buyers keep their private purchased snapshots/);
});
test('index page exposes Premium 2.0 purchase repair workflow', async () => {
const html = await indexHtml();