From 27f1783264c8097681743069146d222641af113d Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sat, 31 Jan 2026 20:12:03 -0500 Subject: [PATCH 1/2] Prevent re-downloading claimed exhale notes on mobile --- src/pages/VerifyPage.tsx | 131 +++++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 59 deletions(-) diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index 12fd9f3a..bc4be422 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -3328,76 +3328,89 @@ React.useEffect(() => { zkMeta?.zkPoseidonHash, ]); -const onDownloadNotePng = useCallback(async () => { - if (!noteSvgFromPng || noteClaimed) return; + const onDownloadNotePng = useCallback(async () => { + if (!noteSvgFromPng || noteClaimed) return; - try { - const payloadBase = noteSendPayloadRaw - ? { ...noteSendPayloadRaw } - : noteSendMeta + const claimedPulse = currentPulse ?? getKaiPulseEternalInt(new Date()); + + try { + const payloadBase = noteSendPayloadRaw + ? { ...noteSendPayloadRaw } + : noteSendMeta + ? { + parentCanonical: noteSendMeta.parentCanonical, + amountPhi: noteSendMeta.amountPhi, + amountUsd: noteSendMeta.amountUsd, + childCanonical: noteSendMeta.childCanonical, + } + : null; + + const nextNonce = genNonce(); + + const noteSendPayload = payloadBase ? { - parentCanonical: noteSendMeta.parentCanonical, - amountPhi: noteSendMeta.amountPhi, - amountUsd: noteSendMeta.amountUsd, - childCanonical: noteSendMeta.childCanonical, + ...payloadBase, + parentCanonical: payloadBase.parentCanonical || noteSendMeta?.parentCanonical, + amountPhi: noteSendMeta?.amountPhi ?? payloadBase.amountPhi, + amountUsd: noteSendMeta?.amountUsd ?? payloadBase.amountUsd, + transferNonce: nextNonce, } : null; - const nextNonce = genNonce(); + if (noteSendPayload && "childCanonical" in noteSendPayload) { + delete (noteSendPayload as { childCanonical?: unknown }).childCanonical; + } - const noteSendPayload = payloadBase - ? { - ...payloadBase, - parentCanonical: payloadBase.parentCanonical || noteSendMeta?.parentCanonical, - amountPhi: noteSendMeta?.amountPhi ?? payloadBase.amountPhi, - amountUsd: noteSendMeta?.amountUsd ?? payloadBase.amountUsd, - transferNonce: nextNonce, - } - : null; + const nonce = nextNonce ? `-${nextNonce.slice(0, 8)}` : ""; + const filename = `☤KAI-NOTE${nonce}.png`; - if (noteSendPayload && "childCanonical" in noteSendPayload) { - delete (noteSendPayload as { childCanonical?: unknown }).childCanonical; - } + const png = await svgStringToPngBlob(noteSvgFromPng, 2400); - const nonce = nextNonce ? `-${nextNonce.slice(0, 8)}` : ""; - const filename = `☤KAI-NOTE${nonce}.png`; + const noteSendJson = noteSendPayload ? JSON.stringify(noteSendPayload) : ""; + const entries = [ + noteProofBundleJson ? { keyword: "phi_proof_bundle", text: noteProofBundleJson } : null, + sharedReceipt?.bundleHash ? { keyword: "phi_bundle_hash", text: sharedReceipt.bundleHash } : null, + sharedReceipt?.receiptHash ? { keyword: "phi_receipt_hash", text: sharedReceipt.receiptHash } : null, + noteSendJson ? { keyword: "phi_note_send", text: noteSendJson } : null, + { keyword: "phi_note_svg", text: noteSvgFromPng }, + ].filter((entry): entry is { keyword: string; text: string } => Boolean(entry)); - const png = await svgStringToPngBlob(noteSvgFromPng, 2400); - - const noteSendJson = noteSendPayload ? JSON.stringify(noteSendPayload) : ""; - const entries = [ - noteProofBundleJson ? { keyword: "phi_proof_bundle", text: noteProofBundleJson } : null, - sharedReceipt?.bundleHash ? { keyword: "phi_bundle_hash", text: sharedReceipt.bundleHash } : null, - sharedReceipt?.receiptHash ? { keyword: "phi_receipt_hash", text: sharedReceipt.receiptHash } : null, - noteSendJson ? { keyword: "phi_note_send", text: noteSendJson } : null, - { keyword: "phi_note_svg", text: noteSvgFromPng }, - ].filter((entry): entry is { keyword: string; text: string } => Boolean(entry)); + if (entries.length === 0) { + triggerDownload(filename, png, "image/png"); + } else { + const bytes = new Uint8Array(await png.arrayBuffer()); + const enriched = insertPngTextChunks(bytes, entries); + const finalBlob = new Blob([enriched as BlobPart], { type: "image/png" }); + triggerDownload(filename, finalBlob, "image/png"); + } - if (entries.length === 0) { - triggerDownload(filename, png, "image/png"); - return; + if (noteSendPayload) { + const rotatedMeta = buildNoteSendMetaFromObjectLoose(noteSendPayload); + if (rotatedMeta) { + markNoteClaimed(rotatedMeta.parentCanonical, rotatedMeta.transferNonce, { + childCanonical: rotatedMeta.childCanonical, + claimedPulse, + }); + setRegistryTick((prev) => prev + 1); + } + } + } catch (err) { + const msg = err instanceof Error ? err.message : "Note download failed."; + setNotice(msg); + } finally { + // ✅ ALWAYS flip claim + force UI refresh (mobile-safe) + confirmNoteSend(); } - - const bytes = new Uint8Array(await png.arrayBuffer()); - const enriched = insertPngTextChunks(bytes, entries); - const finalBlob = new Blob([enriched as BlobPart], { type: "image/png" }); - triggerDownload(filename, finalBlob, "image/png"); - } catch (err) { - const msg = err instanceof Error ? err.message : "Note download failed."; - setNotice(msg); - } finally { - // ✅ ALWAYS flip claim + force UI refresh (mobile-safe) - confirmNoteSend(); - } -}, [ - confirmNoteSend, - noteClaimed, - noteProofBundleJson, - noteSendMeta, - noteSendPayloadRaw, - noteSvgFromPng, - sharedReceipt, -]); + }, [ + confirmNoteSend, + currentPulse, + noteClaimed, + noteProofBundleJson, + noteSendMeta, + noteSendPayloadRaw, + noteSvgFromPng, + sharedReceipt, + ]); const onDownloadVerifiedCard = useCallback(async () => { if (!verifiedCardData) return; From 0fa6d23c94af0bb1b621bc0dd0c8121d2d9fc6eb Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sat, 31 Jan 2026 20:17:34 -0500 Subject: [PATCH 2/2] Avoid claiming rotated fresh exhale notes --- src/pages/VerifyPage.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index bc4be422..4635bc07 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -3331,8 +3331,6 @@ React.useEffect(() => { const onDownloadNotePng = useCallback(async () => { if (!noteSvgFromPng || noteClaimed) return; - const claimedPulse = currentPulse ?? getKaiPulseEternalInt(new Date()); - try { const payloadBase = noteSendPayloadRaw ? { ...noteSendPayloadRaw } @@ -3384,16 +3382,6 @@ React.useEffect(() => { triggerDownload(filename, finalBlob, "image/png"); } - if (noteSendPayload) { - const rotatedMeta = buildNoteSendMetaFromObjectLoose(noteSendPayload); - if (rotatedMeta) { - markNoteClaimed(rotatedMeta.parentCanonical, rotatedMeta.transferNonce, { - childCanonical: rotatedMeta.childCanonical, - claimedPulse, - }); - setRegistryTick((prev) => prev + 1); - } - } } catch (err) { const msg = err instanceof Error ? err.message : "Note download failed."; setNotice(msg); @@ -3403,7 +3391,6 @@ React.useEffect(() => { } }, [ confirmNoteSend, - currentPulse, noteClaimed, noteProofBundleJson, noteSendMeta,