Add matured lock withdrawal flow#289
Merged
El-swaggerito merged 1 commit intoJul 23, 2026
Merged
Conversation
Give matured vault locks a real withdrawal path: eligibility, an explicit confirmation, a loading state, and distinct success and failure states. - Add `evaluateWithdrawalEligibility`, which derives maturity from the lock's unlock date rather than its stored status, so a lock that matures while the screen is open is not reported as locked. - Add `vaultStore.withdrawMaturedLock`, which re-checks eligibility at submission time, submits through the vault service when a contract is configured, and removes the lock only after the transfer resolves so a failure leaves the vault untouched. - Add `useMaturedLockWithdrawal`, a small state machine over idle / confirming / submitting / success / failed with cancel and retry. - Add `MaturedLockWithdrawalModal` rendering each of those steps, and wire it into the lock detail screen in place of the confirmation state that was set but never rendered. - Map failures to codes and fixed copy rather than raw RPC errors, and offer retry only where a second attempt can succeed. - State plainly in the confirmation and success states when no vault contract is configured and nothing moves on the network. - Document the flow's SDK assumptions, placeholder behaviour, and eligibility rules in docs/vault-integration-assumptions.md. Also fixes a duplicate `router` declaration in the vault tab that made the screen fail to parse, and repairs the vault screen test suite that the parse error had been masking.
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
closes #264
Summary
Matured vault locks had a "Withdraw Funds" button whose confirmation state was set but never rendered — the confirm handler that fed it was unreachable, so the button did nothing beyond flipping a boolean. This PR gives the lock a real withdrawal path with eligibility, an explicit confirmation, a loading state, and distinct success and failure states, structured so the SDK call is the only thing left to swap.
Design
The flow is split into three layers so each part is testable on its own.
src/features/vault/maturedLockWithdrawal.ts— pure rules.evaluateWithdrawalEligibility(lock, { publicKey, now })derives maturity from the lock'sunlockDaterather than its storedstatus.statusis only recomputed when locks are loaded, so a lock that matures while the detail screen is open would otherwise keep reporting itself as locked. It also blocks withdrawal when no wallet is loaded or the amount is not positive, and returns the plain-language reason to render.network,secret-unavailable,not-matured,lock-not-found, …) and mapped to fixed copy bydescribeWithdrawalError, so raw Soroban/RPC strings never reach the UI. Each mapping declares whether retry can help.Errorwith acodeproperty rather than anErrorsubclass, because subclassed built-ins do not survive transpilation reliably on the Babel/Hermes targets this app builds for.vaultStore.withdrawMaturedLock(lockId, { publicKey, getSecretKey })— the transfer.{ amount, hash, isPreview }.useMaturedLockWithdrawal+MaturedLockWithdrawalModal— the flow.idle → confirming → submitting → success | failed, with cancel, retry, and a guard against a double tap on confirm.Not implying a live withdrawal
When
EXPO_PUBLIC_VAULT_CONTRACT_IDis unset the flow runs throughmockWithdrawFromVault, and the confirmation and success states say directly that no vault contract is configured and no funds move on the network. No transaction hash is shown in that mode. With a contract configured, the flow goes through the existingwithdrawFromVaultservice unchanged.Documentation
docs/vault-integration-assumptions.mdgains a "Matured Lock Withdrawal" section covering the assumptions the flow encodes (whole-lock withdrawal, thewithdraw(to, amount)call shape, the contract as the real maturity authority, lock removal ordering,SUCCESSas terminal, mapped errors), what each would cost if it turns out wrong, the placeholder behaviour, and the eligibility rules.Incidental fixes
app/(tabs)/vault.tsxdeclaredconst router = useRouter()twice, which made the screen fail to parse — the vault entry point to this flow did not build onmain, and__tests__/vault.test.tsxhad been failing to run at all because of it. Removing the duplicate unmasked that suite, which then failed on a store mock missinglocks/loadLocksand on copy assertions that had drifted from the screen (Lock Funds (30 days)vsSet Aside for 30 Days, the lock action now recording a lock instead of alerting "not yet implemented"). Those 20 tests are repaired here.Acceptance criteria
Testing
49 tests, all passing, covering eligibility rules and error classification; the store action across configured and placeholder modes, including that the lock survives a network failure and an unreadable key; and the full UI flow including confirmation, loading, success, cancel, failure with retry, and a non-retryable failure.
Repository-wide,
mainfails 25 tests across 5 suites; this branch fails 15 across 4. The remaining failures (send,paymentSuccess,home.pullToRefresh,WalletResetConfirmModal) are pre-existing and untouched here. Typechecking reports no new errors.Follow-up, not included
app/vault-lock/[id].tsxis a separate legacy screen built on hardcoded mock data, reachable from the "Mock Active Locks" demo section of the vault tab. Its withdraw button still raises "Withdrawal not implemented for mock data." Its lock does not exist in the store, so it cannot use this flow as-is; removing or re-pointing that demo route is worth a follow-up.