mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
Add creator premium epoch refresh action
This commit is contained in:
1 parent
37d871431b
commit
b980215f15
3 files changed
+129
-4
No files matched your search
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user