Skip to content

feat(frontend): prevent duplicate transfer submissions across retries and navigation - #295

Open
woahwhattheheck wants to merge 5 commits into
RemitFlow:mainfrom
woahwhattheheck:latch/remitflow-277-idempotent-send
Open

woahwhattheheck wants to merge 5 commits into
RemitFlow:mainfrom
woahwhattheheck:latch/remitflow-277-idempotent-send

Conversation

@woahwhattheheck

Copy link
Copy Markdown

Closes #277

Summary

One user intent creates at most one transfer. A canonical payload fingerprint is bound to an idempotency key; only a safe operation reference is persisted across navigation/refresh; an edited payload requires a new intent.

Design

  • fingerprintTransferPayload / idempotencyKeyFor (src/utils/transferIntent.js) derive a stable key from the exact transferable fields (recipient, currencies, amounts, fee, rate).
  • Safe operation reference in sessionStorage: key, fingerprint, transfer id, and operation status (submitting | unknown | succeeded | failed | dismissed). No secrets or raw form state.
  • createTransfer returns the prior record for the same idempotency key, including concurrent in-flight replays (timeout / double-submit race).
  • Send Money restores recoverable intents after navigation/refresh without auto-resubmitting. Dismissing the success dialog marks the op acknowledged. Timeout / connection-loss paths keep status unknown so a retry reuses the same key.
  • Changed payload → new fingerprint → new idempotency key → new intent.

Tradeoffs

  • Session-scoped persistence (not cross-tab durable storage) keeps the reference safe and short-lived; a full backend idempotency store remains the long-term authority.
  • Fee/rate are part of the fingerprint so a materially different quote is a new intent; cosmetic form edits that do not change the committed quote do not mint extras.
  • Additive Transfer contract field idempotencyKey (optional) preserves legacy records.

Compatibility

  • Additive only; existing flows and the in-memory submission lock remain.
  • Unrelated tests only gained sessionStorage.clear() in beforeEach so intent refs cannot leak across suites.

Test evidence

npx vitest run \
  test/unit/transfer-intent.test.js \
  test/unit/create-transfer-idempotency.test.js \
  test/integration/send-money-idempotency.test.jsx
# Test Files  3 passed | Tests  13 passed
npm run build  # ok

Coverage includes double-confirm, refresh restore, timeout key reuse, navigation reconcile, and changed-payload new key. Pre-existing failures on main (e.g. quote decimal formatting assertions) are untouched.

Out of scope

  • Broad rewrites, unrelated services, or cosmetic-only changes.

woahwhattheheck and others added 5 commits September 24, 2026 15:29
Bind an idempotency key to the exact transfer payload, persist only a safe
operation reference in sessionStorage, and return the prior transfer on
repeated submits with the same key. Refresh restores in-flight status;
an edited payload mintes a new intent.

Closes RemitFlow#277
Prevent duplicate transfer submissions across double-clicks, retries,
refresh, and navigation by persisting a safe operation reference and
reconciling in-flight intents without auto-resubmitting.
Prevent duplicate transfer submissions across double-clicks, retries,
timeouts, refresh, and navigation by fingerprinting the exact payload,
persisting only a safe operation reference, and reconciling in-flight
intents without auto-resubmitting. Changed payloads mint a new intent.

Closes RemitFlow#277
Canonicalize amounts without collisions, retain an opaque session fingerprint,
and give each acknowledged transfer a fresh key. Reject reused keys with a
conflicting payload and recover accepted transfers without a saved ID.

Closes RemitFlow#277
Copy initial records before appending so a cleared storage session starts clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(frontend): prevent duplicate transfer submissions across retries and navigation

1 participant