From 65a6842302c1db8dc9eb83157cad12da48bee9fa Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Wed, 18 Mar 2026 07:33:40 +0000 Subject: [PATCH] [#284] Fix false recovery intents from wallet rejection Move intent save to after writeContractAsync succeeds instead of before. If the wallet rejects (any error message, any wallet), no intent is ever written to localStorage. Removes brittle isUserRejection() pattern matching entirely. Fixes #284 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/hooks/usePublish.ts | 38 +++++++++----------------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/src/hooks/usePublish.ts b/src/hooks/usePublish.ts index 5672cd75..4ffbbf74 100644 --- a/src/hooks/usePublish.ts +++ b/src/hooks/usePublish.ts @@ -42,20 +42,6 @@ interface PublishOptions { onIndexed?: () => void; } -/** - * Returns true if the error is a user-rejected transaction (wallet popup dismissed). - */ -function isUserRejection(err: unknown): boolean { - const message = - err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase(); - return ( - message.includes("user rejected") || - message.includes("user denied") || - message.includes("rejected the request") || - message.includes("cancelled") - ); -} - /** * Shared publishing state machine for StoryFactory write flows. * @@ -100,14 +86,6 @@ export function usePublish() { cachedCid.current = { cid, contentHash }; } - // Save intent before wallet confirmation - opts.onIntentSave?.({ - content: opts.content, - metadata: opts.metadata ?? {}, - indexerRoute: opts.indexerRoute, - uploadKeyPrefix: opts.uploadKeyPrefix, - }); - // 2. Submit tx to wallet setState("confirming"); const writeCall = opts.buildWriteCall(cid, contentHash); @@ -115,8 +93,15 @@ export function usePublish() { const hash = await writeContractAsync(writeCall); setTxHash(hash); - // Persist tx hash immediately after broadcast (before receipt polling) - // so recovery works even if receipt polling fails + // Save intent + tx hash only after wallet signs (not before). + // This avoids false recovery intents when the wallet rejects — + // no intent exists if writeContractAsync throws. + opts.onIntentSave?.({ + content: opts.content, + metadata: opts.metadata ?? {}, + indexerRoute: opts.indexerRoute, + uploadKeyPrefix: opts.uploadKeyPrefix, + }); opts.onTxConfirmed?.(hash); // 3. Wait for tx confirmation @@ -149,11 +134,6 @@ export function usePublish() { err instanceof Error ? err.message : "Unknown error"; setError(message); setState("error"); - - // User rejected tx — clear intent (no recovery needed) - if (isUserRejection(err)) { - opts.onIndexed?.(); // reuse onIndexed to clear intent - } } }, [writeContractAsync],