Repository navigation
ENG-703: offer the card charge when the invoice API is unreachable - #92
Merged
Merged
Conversation
NO_RETRY_CONTEXT sends a request once. The shared RetryLink still retries every other operation up to five attempts. Refs ENG-703 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WxkKbrg2Xohf3mQj7BLrs6
When createInvoice fails with no server answer (offline, DNS, timeout) or a 5xx, show "Can't reach Flash: card payments only" and go to CashuCardCharge, where satAmount is already stored. A 4xx or GraphQL error keeps the existing invoice error. The invoice request is sent once so the card is offered without 5-9 s of retries. SAT entry with no price now says why the invoice was skipped, and fiat entry with no local-currency price shows the price-loading message instead of "Please enter a valid amount." Refs ENG-703 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WxkKbrg2Xohf3mQj7BLrs6
Review fix for ENG-703. The keypad's cold-start refetchPrice went through the shared RetryLink with retries on, so an offline SAT entry waited out 5 attempts (up to ~9 s) with no spinner before the card was offered. The RealtimePrice query now carries NO_RETRY_CONTEXT; refetch reuses the query options, and the 20 s poll is its own retry. Adds a hook test against the app client: with fetch rejecting, the mount query and refetchPrice each send one request and refetchPrice resolves 0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WxkKbrg2Xohf3mQj7BLrs6
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.
Problem
Offline, the keypad sent every amount above a cent to
createInvoice. That request fails offline, and the catch block always showed "Unable to create invoice". It never offered the Flashcard charge, even though the charge needs no API. On the Pixel repro, 7 and 8 sats reached the card (they are under a cent and skip the invoice), while 65 sats hit the invoice path and failed.Change
src/utils/apolloErrors.ts(new):isApiUnreachable(err)is true when the request got no server answer (fetch threw) or the server failed (5xx), with no GraphQL errors. A 4xx (for example an expired session) and any GraphQL error are real answers and keep the existing toast.src/screens/Keypad.tsx,onCreateInvoice:CashuCardCharge.satAmountis already in the store, so the charge reads the right amount.NO_RETRY_CONTEXT, so the card is offered after one request rather than after 5 RetryLink attempts (5-9 s offline).NaN) now shows "Bitcoin price is still loading. Please try again in a moment." instead of "Please enter a valid amount.", and never storesNaNsats.src/graphql/retryPolicy.ts(new) +src/graphql/ApolloClient.ts: RetryLink getsretryIf: shouldRetryOperation. An operation whose context setsnoRetrygoes out once. Every other operation still retries up to 5 attempts.src/utils/amounts.ts: the price-loading copy is exported asPRICE_LOADING_MESSAGEand reused (same text).These stay as they were: the validation refusals (too small, too large, no price), the GraphQL business-error toast, online success going to
Invoice, and the card's own refusals inCashuCardCharge(over balance,CHANGE_NEEDS_MINT). The 65-sat repro with 61 sats on the card will now reach the charge screen and be refused there for insufficient balance. That outcome is correct.The offline signal is the failed request itself (Apollo
networkError). NetInfo is not a dependency, adding it would need a native rebuild, and its reachability flag lags right after an airplane-mode toggle.How it was tested
New tests, plus their results on
718c700(the code before this change):__tests__/utils/apolloErrors.test.ts: all 6 cases__tests__/graphql/retryPolicy.test.ts: no-retry sends once; others retry 5; the app client sends a no-retry mutation once offlineApolloClient.tswiring is revertedsetSatAmount('1300')Invoice, mutation called once withcontext: {noRetry: true}NaNconversion → price-loading toast, noNaNstoredisApiUnreachableis removed (checked locally)The regression guards assert behaviour that must not change, so they pass on the old code by design.
Commands, all green:
yarn test --runInBand: 73 suites passed, 1 skipped; 1559 tests passed, 9 skipped (baseline 1539 passed, so +20)yarn typecheckyarn lint: 0 errors. The 1 warning (no-shadow, line 75 of an untouched file) was already on mainnode_modules/.bin/prettier --checkon the new and changed test and util files. The pre-existing formatting drift inKeypad.tsxandApolloClient.tsis untouched, and the new lines are prettier-cleantyposon all changed filesNo Maestro flow was added. The e2e job runs a simulator build against the real TEST API, and making the API disappear partway through a flow would need DNS or firewall changes on the CI runner. Suggested on-device check: Pixel in airplane mode, charge an amount above a cent that is under the card balance (for example 30 sats with 61 on the card), and confirm the charge completes.
Follow-ups
useSatPrice/useRealtimePricekeep the price only in memory. After an offline relaunch, fiat entry refuses with the price-loading message, while SAT entry still reaches the card.infotoast uses the library's default renderer, becausetoastConfigdefines onlysuccessanderror.🤖 Generated with Claude Code
https://claude.ai/code/session_01WxkKbrg2Xohf3mQj7BLrs6