diff --git a/packages/stellar-wallet-snap/CHANGELOG.md b/packages/stellar-wallet-snap/CHANGELOG.md index 967809cf5..78da1743c 100644 --- a/packages/stellar-wallet-snap/CHANGELOG.md +++ b/packages/stellar-wallet-snap/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Return classic keyring balance metadata `limit` in integer stroop string ([#495](https://github.com/MetaMask/internal-snaps/pull/495)) - Keyring method `getAccountBalances` and the Keyring event `AccountBalancesUpdated` now return this `limit` in stroops. +### Fixed + +- Keep in-progress memo edits in `memoDraft` so Back does not commit unsaved memo text on send confirmation. ([#406](https://github.com/MetaMask/internal-snaps/pull/406)) + ## [1.3.0] ### Added diff --git a/packages/stellar-wallet-snap/locales/en.json b/packages/stellar-wallet-snap/locales/en.json index 23e13054a..b499a2491 100644 --- a/packages/stellar-wallet-snap/locales/en.json +++ b/packages/stellar-wallet-snap/locales/en.json @@ -127,6 +127,9 @@ "confirmation.memo.error.tooLong": { "message": "Memo must be 28 bytes or fewer" }, + "confirmation.memo.error.saveFailed": { + "message": "Could not save memo. Please try again." + }, "confirmation.asset": { "message": "Asset" }, diff --git a/packages/stellar-wallet-snap/locales/es.json b/packages/stellar-wallet-snap/locales/es.json index a0f6519c9..3ca0597ea 100644 --- a/packages/stellar-wallet-snap/locales/es.json +++ b/packages/stellar-wallet-snap/locales/es.json @@ -127,6 +127,9 @@ "confirmation.memo.error.tooLong": { "message": "Memo must be 28 bytes or fewer" }, + "confirmation.memo.error.saveFailed": { + "message": "Could not save memo. Please try again." + }, "confirmation.asset": { "message": "Asset" }, @@ -395,7 +398,7 @@ "message": "Function" }, "confirmation.transaction.param.deployer": { - "message": "Desplegador" + "message": "Deployer" }, "confirmation.transaction.param.salt": { "message": "Salt" diff --git a/packages/stellar-wallet-snap/messages.json b/packages/stellar-wallet-snap/messages.json index f9b276d09..46edb4658 100644 --- a/packages/stellar-wallet-snap/messages.json +++ b/packages/stellar-wallet-snap/messages.json @@ -125,6 +125,9 @@ "confirmation.memo.error.tooLong": { "message": "Memo must be 28 bytes or fewer" }, + "confirmation.memo.error.saveFailed": { + "message": "Could not save memo. Please try again." + }, "confirmation.asset": { "message": "Asset" }, diff --git a/packages/stellar-wallet-snap/src/ui/confirmation/api.ts b/packages/stellar-wallet-snap/src/ui/confirmation/api.ts index 09f683077..696146b1e 100644 --- a/packages/stellar-wallet-snap/src/ui/confirmation/api.ts +++ b/packages/stellar-wallet-snap/src/ui/confirmation/api.ts @@ -156,6 +156,8 @@ export type ConfirmationBaseProps = Partial & { memoScreen?: boolean; // Saved memo for the pending send (UI-owned; not on confirmSend RPC params). memo?: string; + // In-progress memo edit text (not committed until Save succeeds). + memoDraft?: string | null; // Locale key for memo validation errors on the edit screen. memoError?: LocalizedMessage | null; // When false, hide Add/Update memo controls. @@ -166,4 +168,9 @@ export type ConfirmationBaseProps = Partial & { * of stacking parallel refresh chains (e.g. MemoEdit Save vs open cron). */ backgroundEventId?: string; + /** + * Preference- and flow-enabled refresher keys resolved at dialog open. + * MemoEdit Save reuses this snapshot for restart. + */ + enabledRefresherKeys?: string[]; }; diff --git a/packages/stellar-wallet-snap/src/ui/confirmation/controller.tsx b/packages/stellar-wallet-snap/src/ui/confirmation/controller.tsx index 0479c56f7..272b3e2a7 100644 --- a/packages/stellar-wallet-snap/src/ui/confirmation/controller.tsx +++ b/packages/stellar-wallet-snap/src/ui/confirmation/controller.tsx @@ -239,10 +239,23 @@ export class ConfirmationUXController { tokenPrices, }; + // Preference/flow-enabled keys for MemoEdit Save (ungated). + const enabledRefresherKeys: ConfirmationContextRefresherKey[] = []; + if (enablePricing) { + enabledRefresherKeys.push(ConfirmationContextRefresherKey.Prices); + } + if (enableSecurityScan) { + enabledRefresherKeys.push(ConfirmationContextRefresherKey.Scan); + } + if (enableLocalSimulation) { + enabledRefresherKeys.push(ConfirmationContextRefresherKey.Transaction); + } + // 1. Initial context with loading state const context = { ...defaultContext, ...renderContext, + ...(enabledRefresherKeys.length > 0 ? { enabledRefresherKeys } : {}), }; // 2. Initial render with loading skeleton (always show loading if pricing enabled) @@ -264,38 +277,38 @@ export class ConfirmationUXController { return dialogPromise; } - // 5. Schedule background context refresh for enabled refreshers only. + // 5. Schedule background context refresh for this-tick keys only. // Skip Scan / Transaction when renderContext already marked them Error // (e.g. RequiresMemo on open): a pending open cron would race // MemoEdit's restart and double-hit Blockaid after the user saves a memo. // Read the Error overrides from `renderContext` (not merged `context`): // `defaultContext` only ever sets Fetched/Fetching, so TS narrows those // fields and rejects a comparison against Error on the merged object. - const refresherKeys: ConfirmationContextRefresherKey[] = []; + const scheduledKeys: ConfirmationContextRefresherKey[] = []; if (enablePricing) { - refresherKeys.push(ConfirmationContextRefresherKey.Prices); + scheduledKeys.push(ConfirmationContextRefresherKey.Prices); } if ( enableSecurityScan && renderContext.scanFetchStatus !== FetchStatus.Error ) { - refresherKeys.push(ConfirmationContextRefresherKey.Scan); + scheduledKeys.push(ConfirmationContextRefresherKey.Scan); } if ( enableLocalSimulation && renderContext.transactionsFetchStatus !== FetchStatus.Error ) { - refresherKeys.push(ConfirmationContextRefresherKey.Transaction); + scheduledKeys.push(ConfirmationContextRefresherKey.Transaction); } - if (refresherKeys.length > 0) { + if (scheduledKeys.length > 0) { const backgroundEventId = await RefreshConfirmationContextHandler.scheduleBackgroundEvent( { scope, interfaceId: id, interfaceKey, - refresherKeys, + refresherKeys: scheduledKeys, }, Duration.OneSecond, ); diff --git a/packages/stellar-wallet-snap/src/ui/confirmation/views/MemoEdit/events.test.tsx b/packages/stellar-wallet-snap/src/ui/confirmation/views/MemoEdit/events.test.tsx index f5d5b4822..9844bf08d 100644 --- a/packages/stellar-wallet-snap/src/ui/confirmation/views/MemoEdit/events.test.tsx +++ b/packages/stellar-wallet-snap/src/ui/confirmation/views/MemoEdit/events.test.tsx @@ -35,15 +35,27 @@ const buttonEvent = (name: string): UserInputEvent => ({ name, }); +const allRefresherKeys = [ + ConfirmationContextRefresherKey.Prices, + ConfirmationContextRefresherKey.Scan, + ConfirmationContextRefresherKey.Transaction, +]; + const scheduleArgs = { scope: 'stellar:pubnet', interfaceId: 'interface-id', interfaceKey: ConfirmationInterfaceKey.ConfirmSendTransaction, - refresherKeys: [ - ConfirmationContextRefresherKey.Transaction, - ConfirmationContextRefresherKey.Scan, - ConfirmationContextRefresherKey.Prices, - ], + refresherKeys: allRefresherKeys, +}; + +const liveSendContext = { + interfaceKey: ConfirmationInterfaceKey.ConfirmSendTransaction, + scope: 'stellar:pubnet', + transaction: 'xdr', + accountId: 'account-id', + transactionsFetchStatus: FetchStatus.Fetched, + enabledRefresherKeys: allRefresherKeys, + backgroundEventId: 'live-event', }; describe('MemoEdit event handlers', () => { @@ -78,6 +90,7 @@ describe('MemoEdit event handlers', () => { interfaceKey: ConfirmationInterfaceKey.ConfirmSendTransaction, errorMessage: 'confirmation.txnError.requiresMemo', backgroundEventId: 'live-event', + memoDraft: 'stale-draft', }, }); @@ -88,20 +101,27 @@ describe('MemoEdit event handlers', () => { expect.objectContaining({ memoScreen: true, memoError: null, + memoDraft: null, backgroundEventId: 'live-event', }), ); }); + + it('no-ops when context is missing', async () => { + await handlers[MemoEditFormNames.Open]?.({ + id: 'interface-id', + event: buttonEvent(MemoEditFormNames.Open), + context: null, + }); + + expect(updateInterfaceIfExists).not.toHaveBeenCalled(); + }); }); describe('Save', () => { - it('always restarts Transaction+Scan+Prices using the latest event id', async () => { + it('restarts open-time enabled refresher keys using the latest event id', async () => { jest.mocked(getInterfaceContextIfExists).mockResolvedValue({ - interfaceKey: ConfirmationInterfaceKey.ConfirmSendTransaction, - scope: 'stellar:pubnet', - transaction: 'xdr', - accountId: 'account-id', - transactionsFetchStatus: FetchStatus.Fetched, + ...liveSendContext, backgroundEventId: 'latest-event', }); @@ -109,10 +129,7 @@ describe('MemoEdit event handlers', () => { id: 'interface-id', event: formSubmitEvent('exchange-ref'), context: { - interfaceKey: ConfirmationInterfaceKey.ConfirmSendTransaction, - scope: 'stellar:pubnet', - transaction: 'xdr', - accountId: 'account-id', + ...liveSendContext, transactionsFetchStatus: FetchStatus.Error, errorMessage: 'confirmation.txnError.requiresMemo', backgroundEventId: 'stale-click-event', @@ -129,6 +146,7 @@ describe('MemoEdit event handlers', () => { 'RENDERED', expect.objectContaining({ memo: 'exchange-ref', + memoDraft: null, memoScreen: false, errorMessage: null, transactionsFetchStatus: FetchStatus.Fetched, @@ -138,15 +156,54 @@ describe('MemoEdit event handlers', () => { ); }); + it('schedules only preference-enabled keys from the open-time snapshot', async () => { + const transactionOnly = [ConfirmationContextRefresherKey.Transaction]; + jest.mocked(getInterfaceContextIfExists).mockResolvedValue({ + ...liveSendContext, + transactionsFetchStatus: FetchStatus.Error, + enabledRefresherKeys: transactionOnly, + }); + + await handlers[MemoEditFormNames.Form]?.({ + id: 'interface-id', + event: formSubmitEvent('memo-only-tx'), + context: { + ...liveSendContext, + transactionsFetchStatus: FetchStatus.Error, + }, + }); + + expect(scheduleSpy).toHaveBeenCalledWith( + { + ...scheduleArgs, + refresherKeys: transactionOnly, + }, + expect.anything(), + { replaceEventId: 'live-event' }, + ); + expect(updateInterfaceIfExists).toHaveBeenCalledWith( + 'interface-id', + 'RENDERED', + expect.objectContaining({ + memo: 'memo-only-tx', + transactionsFetchStatus: FetchStatus.Fetched, + backgroundEventId: 'new-event-id', + }), + ); + expect(updateInterfaceIfExists).toHaveBeenCalledWith( + 'interface-id', + 'RENDERED', + expect.not.objectContaining({ + scanFetchStatus: FetchStatus.Fetching, + }), + ); + }); + it('replaces the live event id during an in-flight tick so cron ownership aborts', async () => { // Click/open still see the in-flight tick id; Save must cancel that id and // persist a new one so the orphan tick observes ownership loss. jest.mocked(getInterfaceContextIfExists).mockResolvedValue({ - interfaceKey: ConfirmationInterfaceKey.ConfirmSendTransaction, - scope: 'stellar:pubnet', - transaction: 'xdr', - accountId: 'account-id', - transactionsFetchStatus: FetchStatus.Fetched, + ...liveSendContext, scanFetchStatus: FetchStatus.Fetched, memoScreen: true, backgroundEventId: 'in-flight-tick-event', @@ -156,11 +213,7 @@ describe('MemoEdit event handlers', () => { id: 'interface-id', event: formSubmitEvent('during-flight'), context: { - interfaceKey: ConfirmationInterfaceKey.ConfirmSendTransaction, - scope: 'stellar:pubnet', - transaction: 'xdr', - accountId: 'account-id', - transactionsFetchStatus: FetchStatus.Fetched, + ...liveSendContext, scanFetchStatus: FetchStatus.Fetched, memoScreen: true, backgroundEventId: 'in-flight-tick-event', @@ -185,28 +238,18 @@ describe('MemoEdit event handlers', () => { it('restarts when clearing a memo so Confirm cannot stay enabled on stale success', async () => { jest.mocked(getInterfaceContextIfExists).mockResolvedValue({ - interfaceKey: ConfirmationInterfaceKey.ConfirmSendTransaction, - scope: 'stellar:pubnet', - transaction: 'xdr', - accountId: 'account-id', - transactionsFetchStatus: FetchStatus.Fetched, + ...liveSendContext, scanFetchStatus: FetchStatus.Fetched, memo: 'exchange-ref', - backgroundEventId: 'live-event', }); await handlers[MemoEditFormNames.Form]?.({ id: 'interface-id', event: formSubmitEvent(''), context: { - interfaceKey: ConfirmationInterfaceKey.ConfirmSendTransaction, - scope: 'stellar:pubnet', - transaction: 'xdr', - accountId: 'account-id', - transactionsFetchStatus: FetchStatus.Fetched, + ...liveSendContext, scanFetchStatus: FetchStatus.Fetched, memo: 'exchange-ref', - backgroundEventId: 'live-event', }, }); @@ -227,7 +270,31 @@ describe('MemoEdit event handlers', () => { ); }); - it('persists the memo without rescheduling when there is no live refresh pipeline', async () => { + it('keeps a draft and shows validation error without committing memo or rescheduling', async () => { + await handlers[MemoEditFormNames.Form]?.({ + id: 'interface-id', + event: formSubmitEvent('é'.repeat(15)), + context: { + ...liveSendContext, + memo: 'committed-memo', + errorMessage: 'confirmation.txnError.requiresMemo', + transactionsFetchStatus: FetchStatus.Error, + }, + }); + + expect(scheduleSpy).not.toHaveBeenCalled(); + expect(updateInterfaceIfExists).toHaveBeenCalledWith( + 'interface-id', + 'RENDERED', + expect.objectContaining({ + memo: 'committed-memo', + memoDraft: 'é'.repeat(15), + memoError: 'confirmation.memo.error.tooLong', + }), + ); + }); + + it('shows a save error when context cannot restart refresh', async () => { await handlers[MemoEditFormNames.Form]?.({ id: 'interface-id', event: formSubmitEvent('optional-ref'), @@ -242,34 +309,89 @@ describe('MemoEdit event handlers', () => { 'interface-id', 'RENDERED', expect.objectContaining({ + memoScreen: true, + memoDraft: 'optional-ref', + memoError: 'confirmation.memo.error.saveFailed', + }), + ); + expect(updateInterfaceIfExists).toHaveBeenCalledWith( + 'interface-id', + 'RENDERED', + expect.not.objectContaining({ memo: 'optional-ref', - memoScreen: false, }), ); }); - it('keeps the memo and shows validation error without rescheduling', async () => { + it('shows a save error when scheduling fails', async () => { + jest + .mocked(getInterfaceContextIfExists) + .mockResolvedValue(liveSendContext); + scheduleSpy.mockRejectedValueOnce(new Error('schedule failed')); + await handlers[MemoEditFormNames.Form]?.({ id: 'interface-id', - event: formSubmitEvent('é'.repeat(15)), + event: formSubmitEvent('exchange-ref'), + context: liveSendContext, + }); + + expect(updateInterfaceIfExists).toHaveBeenCalledWith( + 'interface-id', + 'RENDERED', + expect.objectContaining({ + memoScreen: true, + memoDraft: 'exchange-ref', + memoError: 'confirmation.memo.error.saveFailed', + }), + ); + }); + + it('no-ops when context is missing', async () => { + await handlers[MemoEditFormNames.Form]?.({ + id: 'interface-id', + event: formSubmitEvent('exchange-ref'), + context: null, + }); + + expect(scheduleSpy).not.toHaveBeenCalled(); + expect(updateInterfaceIfExists).not.toHaveBeenCalled(); + }); + }); + + describe('Back', () => { + it('closes the editor without committing the draft memo', async () => { + await handlers[MemoEditFormNames.Back]?.({ + id: 'interface-id', + event: buttonEvent(MemoEditFormNames.Back), context: { interfaceKey: ConfirmationInterfaceKey.ConfirmSendTransaction, - errorMessage: 'confirmation.txnError.requiresMemo', - transaction: 'xdr', - accountId: 'account-id', - transactionsFetchStatus: FetchStatus.Error, + memo: 'committed-memo', + memoDraft: 'unsaved-draft', + memoError: 'confirmation.memo.error.tooLong', + memoScreen: true, }, }); - expect(scheduleSpy).not.toHaveBeenCalled(); expect(updateInterfaceIfExists).toHaveBeenCalledWith( 'interface-id', 'RENDERED', expect.objectContaining({ - memo: 'é'.repeat(15), - memoError: 'confirmation.memo.error.tooLong', + memo: 'committed-memo', + memoDraft: null, + memoError: null, + memoScreen: false, }), ); }); + + it('no-ops when context is missing', async () => { + await handlers[MemoEditFormNames.Back]?.({ + id: 'interface-id', + event: buttonEvent(MemoEditFormNames.Back), + context: null, + }); + + expect(updateInterfaceIfExists).not.toHaveBeenCalled(); + }); }); }); diff --git a/packages/stellar-wallet-snap/src/ui/confirmation/views/MemoEdit/events.tsx b/packages/stellar-wallet-snap/src/ui/confirmation/views/MemoEdit/events.tsx index 4745cb317..d5712c230 100644 --- a/packages/stellar-wallet-snap/src/ui/confirmation/views/MemoEdit/events.tsx +++ b/packages/stellar-wallet-snap/src/ui/confirmation/views/MemoEdit/events.tsx @@ -1,12 +1,21 @@ -import { FormSubmitEventStruct } from '@metamask/snaps-sdk'; import type { UserInputEvent } from '@metamask/snaps-sdk'; +import { FormSubmitEventStruct } from '@metamask/snaps-sdk'; +import { + array, + assert, + nonempty, + optional, + string, + type, +} from '@metamask/superstruct'; import type { Json } from '@metamask/utils'; -import type { ConfirmSendJsonRpcRequest } from '../../../../handlers/clientRequest/api'; +import { KnownCaip2ChainIdStruct } from '../../../../api'; import { getMemoValidationErrorKey } from '../../../../handlers/clientRequest/utils'; import type { ConfirmationDataContext } from '../../../../handlers/cronjob/refreshConfirmationContext'; import { ConfirmationContextRefresherKey, + ConfirmationContextRefresherKeyStruct, RefreshConfirmationContextHandler, } from '../../../../handlers/cronjob/refreshConfirmationContext'; import type { @@ -19,10 +28,20 @@ import { updateInterfaceIfExists, } from '../../../../utils'; import type { ConfirmationInterfaceKey } from '../../api'; -import { FetchStatus } from '../../api'; +import { ConfirmationInterfaceKeyStruct, FetchStatus } from '../../api'; import { renderConfirmationView } from '../render'; import { MemoEditFormNames } from './constants'; +/** + * Context fields required to save a memo and restart confirmation refresh. + */ +const MemoSaveContextStruct = type({ + scope: KnownCaip2ChainIdStruct, + interfaceKey: ConfirmationInterfaceKeyStruct, + backgroundEventId: optional(string()), + enabledRefresherKeys: nonempty(array(ConfirmationContextRefresherKeyStruct)), +}); + /** * Re-renders the interface with a patched context. * @@ -44,23 +63,6 @@ async function reRender( ); } -/** - * Whether this confirmation has a live refresh pipeline to restart. - * - * Hard pre-submit error dialogs have `request` but no transaction / - * localSimulation refresh — still allow saving the memo onto context. - * - * @param context - Confirmation interface context. - * @returns True when Save should cancel-and-replace Transaction+Scan+Prices. - */ -function canRestartRefresh(context: Record): boolean { - return ( - typeof context.transaction === 'string' && - typeof context.accountId === 'string' && - context.transactionsFetchStatus !== undefined - ); -} - /** * Opens the memo edit screen from send confirmation. * @@ -76,6 +78,7 @@ async function onOpenClick( await reRender(id, context, { memoScreen: true, memoError: null, + memoDraft: null, }); } @@ -95,9 +98,9 @@ function memoFromSubmitEvent(event: UserInputEvent): string { } /** - * Saves the memo onto confirmation context and always restarts validation + - * scan when a live refresh pipeline is present, so clearing a required memo - * cannot leave Confirm enabled on a stale success. + * Saves the memo onto confirmation context and restarts the open-time + * preference-enabled refreshers so clearing a required memo cannot leave + * Confirm enabled on a stale success. * * @param options - The user input handler context. */ @@ -112,86 +115,80 @@ async function onSaveSubmit( const memo = memoFromSubmitEvent(event); const memoValidationError = getMemoValidationErrorKey(memo); if (memoValidationError) { - // Keep the submitted text in `memo` so the input is not reset when we - // re-render with the validation error. + // Keep the submitted text in `memoDraft` so the input is not reset, without + // committing it as the saved memo (Back must not persist drafts). await reRender(id, context, { - memo, + memoDraft: memo, memoError: memoValidationError, }); return; } - const { interfaceKey } = context as { - interfaceKey?: ConfirmationInterfaceKey; - }; - if (!interfaceKey) { - return; - } - // Prefer the latest interface snapshot — click-time `context` can lag a // prices/scan write between Open and Save. const interfaceContext = await getInterfaceContextIfExists(id); const baseContext = interfaceContext ?? context; - const nextContext = { - ...baseContext, - memo, - memoScreen: false, - memoError: null, - }; + try { + assert(baseContext, MemoSaveContextStruct); - const { scope } = baseContext; - if (canRestartRefresh(baseContext) && typeof scope === 'string') { - let previousEventId: string | undefined; - if (typeof baseContext.backgroundEventId === 'string') { - previousEventId = baseContext.backgroundEventId; - } else if (typeof context.backgroundEventId === 'string') { - previousEventId = context.backgroundEventId; - } + const { scope, interfaceKey, backgroundEventId, enabledRefresherKeys } = + baseContext; - const refreshedContext = { - ...nextContext, + // Only reset slices that were preference-enabled at open. + const nextContext: Record = { + ...baseContext, + memo, + memoDraft: null, + memoScreen: false, + memoError: null, // Clear any prior banner; the restarted transaction refresher will set a // new error (e.g. RequiresMemo again) or leave it cleared on success. errorMessage: null, - transactionsFetchStatus: FetchStatus.Fetched, - scanFetchStatus: FetchStatus.Fetching, }; + if ( + enabledRefresherKeys.includes(ConfirmationContextRefresherKey.Transaction) + ) { + nextContext.transactionsFetchStatus = FetchStatus.Fetched; + } + if (enabledRefresherKeys.includes(ConfirmationContextRefresherKey.Scan)) { + nextContext.scanFetchStatus = FetchStatus.Fetching; + } - const backgroundEventId = + const nextBackgroundEventId = await RefreshConfirmationContextHandler.scheduleBackgroundEvent( { - scope: scope as ConfirmSendJsonRpcRequest['params']['scope'], + scope, interfaceId: id, interfaceKey, - refresherKeys: [ - ConfirmationContextRefresherKey.Transaction, - ConfirmationContextRefresherKey.Scan, - ConfirmationContextRefresherKey.Prices, - ], + refresherKeys: enabledRefresherKeys, }, Duration.OneSecond, - { replaceEventId: previousEventId }, + { replaceEventId: backgroundEventId }, ); - const contextWithEventId = { ...refreshedContext, backgroundEventId }; + + const contextWithEventId = { + ...nextContext, + backgroundEventId: nextBackgroundEventId, + }; await updateInterfaceIfExists( id, renderConfirmationView(interfaceKey, contextWithEventId), contextWithEventId, ); - return; + } catch { + await reRender(id, baseContext, { + memoScreen: true, + memoDraft: memo, + memoError: 'confirmation.memo.error.saveFailed', + }); } - - await updateInterfaceIfExists( - id, - renderConfirmationView(interfaceKey, nextContext), - nextContext, - ); } /** - * Returns from the memo edit screen to the confirmation view. + * Returns from the memo edit screen to the confirmation view without + * committing any in-progress draft. * * @param options - The user input handler context. */ @@ -205,6 +202,7 @@ async function onBackClick( await reRender(id, context, { memoScreen: false, memoError: null, + memoDraft: null, }); } diff --git a/packages/stellar-wallet-snap/src/ui/confirmation/views/render.tsx b/packages/stellar-wallet-snap/src/ui/confirmation/views/render.tsx index 62f6d4280..c9646fe56 100644 --- a/packages/stellar-wallet-snap/src/ui/confirmation/views/render.tsx +++ b/packages/stellar-wallet-snap/src/ui/confirmation/views/render.tsx @@ -50,7 +50,7 @@ export function renderConfirmationView( return ( );