Skip to content

fix: offline gating for onramp, staking, card & add-asset surfaces [PERA-4584]#1023

Open
fmsouza wants to merge 12 commits into
mainfrom
fmsouza/pera-4584
Open

fix: offline gating for onramp, staking, card & add-asset surfaces [PERA-4584]#1023
fmsouza wants to merge 12 commits into
mainfrom
fmsouza/pera-4584

Conversation

@fmsouza

@fmsouza fmsouza commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

PERA-4543 Part B for the onramp, staking, card, and add-asset surfaces: every remote-only action attempted offline now yields a readable localized error or a disabled state — never an eternal spinner, never a fake success. Builds on PERA-4573 (fail-fast mutations → NoConnectionError) and PERA-4574 (typed PeraNetworkError + getNetworkErrorMessageKeys), applying the canonical precedence from docs/OFFLINE_PAUSED_STATE.md: data → offline (isPaused || (isError && !hasInternet)) → error → loading → empty.

No new i18n keys — all copy reuses existing common.offline_mode, common.offline_refresh_body, errors.network.no_connection.*, common.error.*, common.retry.label, staking.retry.

Acceptance criteria → commits

  • Onramp offline-uncached → localized offline state with retry; cached pairs render staleac6cc0a98: useOnrampScreen gains pairsState: 'ready' | 'loading' | 'offline' | 'error' + handleRetryPairs; the Fund tab's bare ActivityIndicator fork now has offline/error EmptyView branches.
  • Ramp order offline → prompt localized error, confirm button returns actionablead7cf84f1: isConnectivityError branch in the create/cancel/quote catches (the existing finally resets isConfirming); shared predicate added in d54f98130.
  • Staking offline-uncached → offline state; cached TVL renders stale; isError retry branch reachable fake-online (both tested)cce851a27 (hook: isPaused = paused AND uncached; paused no longer folded into isLoading) + fb0689bb8 (screen: offline EmptyView with retry that short-circuits while offline; error branch untouched and still reachable online).
  • Card actions offline → localized error, spinners reset, no optimistic frozen-state divergence144a46bbc (useCardErrorToast connectivity short-circuit → errors.network.no_connection.*) + 83124c2e0 (set-PIN/freeze rows and reveal button disabled while offline via isOffline = !hasInternet || status paused; new regression pin useFreezeCardMutation.offline.spec.tsx proves an offline freeze rejection leaves the cached status untouched).
  • packages/card/src/hooks/types.ts exposes isPaused (typed error was already carried), consumed by usePeraCardDetails gating144a46bbc + 83124c2e0.
  • Add-asset search offline surfaces an offline/error state instead of silent empty results; opt-in copy uses the shared offline message on connectivity failuree2ccb7063 (useAssetSearchQuery.isPaused, useGlobalSearch.isRemoteError/isRemotePaused threading) + fa747361b (isError: false hardcode removed; ListEmptyComponent forks offline/error/no-results; assertOnline() precheck in useAssetOptInMutation throws NoConnectionError before any algod call, which useErrorToast now renders as the shared offline copy via d54f98130).
  • Hook unit tests per surface simulating both regimes — every surface has true-offline (onlineManager.setOnline(false), restored in afterEach) and fake-online (error result + useNetworkStatusStore toggling) cases.
  • Online behavior unchanged — no networkMode changes anywhere; all new branches require paused/offline/error signals that are false while online-and-healthy. All pre-existing tests pass unmodified.

Bare awaited-mutation sweep (required by ticket)

All 26 mutateAsync call sites across the onramp/card modules audited (staking and assets have none — their mutations use named wrapper methods, covered above): 26/26 have catch coverage; no fixes needed. Notable by-design cases:

  • useCardOnboardingAddressScreen.ts:294 linkConsent — documented best-effort swallow (.catch(() => undefined)); a link hiccup must not strand a registered user.
  • useCardFundingDelegation.ts / useEscrowCardCreation.ts — composable primitives that intentionally propagate rejections to the caller (caller-side try/catch; the former has no consumer yet).
  • useCardAddFundsScreen.tsx:167 deposit — every rejection routes to showComingSoon() per the TODO(card) (no real Baanx backend yet).
Full 26-site sweep table
# File:line Call Verdict
1 useOnrampOrderDetails.tsx:94 cancelOrder Covered (try/catch → offline-aware toast)
2 useOnrampQuotes.ts:76 createQuote Covered (try/catch → offline-aware inline error)
3 useOnrampConfirm.tsx:86 createOrder Covered (try/catch/finally → offline-aware toast)
4 useCardOnboardingEmailVerifyScreen.ts:89 sendEmailVerification Covered
5 useCardOnboardingPasswordScreen.ts:134 verifyEmail Covered
6 useCardOnboardingPhoneVerifyScreen.ts:111 sendPhoneVerification Covered
7 useCardOnboardingPhoneVerifyScreen.ts:166 verifyPhone Covered
8 useCardAddFundsScreen.tsx:167 deposit Covered (.catch → showComingSoon, per TODO)
9 useCardOnboardingPhoneScreen.ts:124 sendPhoneVerification Covered
10 useCardOnboardingEmailScreen.ts:133 sendEmailVerification Covered
11 useCardOnboardingEmailScreen.ts:173 requestCountryAvailability Covered
12 useCardOnboardingAddressScreen.ts:273 submitConsent Covered (outer try/catch)
13 useCardOnboardingAddressScreen.ts:284 submitAddress Covered (outer try/catch)
14 useCardOnboardingAddressScreen.ts:294 linkConsent Covered (documented best-effort swallow)
15 useCardOnboardingPersonalDetailsScreen.ts:204 submitPersonalDetails Covered
16 useCardSignInScreen.ts:98 login Covered
17 useCardOnboardingStatusScreen.ts:203 connectFundingSourceAsync Covered (both call sites)
18 useCardOnboardingStatusScreen.ts (createPeraCard) createCard Covered
19 useCardOnboardingVerificationScreen.ts:82 startVerification Covered
20 usePeraCardDetails.tsx:225 cardDetails (reveal) Covered
21 usePeraCardDetails.tsx:299 setPin Covered
22 usePeraCardDetails.tsx:332 connectFundingSourceAsync Covered
23 useCardWithdrawConfirmationSheet.ts:82 withdraw Covered
24 useCardConfirmMutation.ts:67 shared freeze/unfreeze/report confirm Covered
25 useCardFundingDelegation.ts:44 updateDelegationAsync Covered by design (propagates; no consumer yet)
26 useEscrowCardCreation.ts:59 createEscrowCardAsync Covered by design (caller #18 catches)

Verification

  • pnpm build 47/47 tasks, pnpm pre-push --no-fail-on-error green, pnpm run format --check clean, pnpm test 554 files / 3989 tests passing.
  • Final whole-branch review: zero Critical/Important findings. Follow-up candidates (all minor, deliberately deferred): teach getNetworkErrorMessageKeys the connectivity branch and swap the literal-key call sites onto it; gate useGlobalSearch's new flags by shouldRunRemoteAssets for symmetry with its pagination flags; allow "Hide" on the revealed card while offline (pure local flip); add an 'empty' arm to onramp pairsState (pre-existing online edge, out of ticket scope).

Out of scope (per ticket)

Buy/sell webview checkout (PERA-4582); card status caching policy (PII exclusion stands); staking data persistence beyond existing keys.

🤖 Generated with Claude Code

@fmsouza
fmsouza requested a review from a team as a code owner July 24, 2026 16:19
Comment on lines +105 to +123
isOffline ? (
<EmptyView
title={t('common.offline_mode')}
body={t('common.offline_refresh_body')}
icon='globe'
/>
) : isError ? (
<EmptyView
title={t('common.error.title')}
body={t('common.error.body')}
icon='info'
/>
) : (
<EmptyView
title={t('add_asset.no_results')}
body=''
icon='magnifying-glass'
/>
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be cleaner (since I suspect I'm going to see a few of these) to have an or something that shows hte offline case if offline and shows the children otherwise?

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.

2 participants