Fix Tap to Pay payments using a wrong account after store switch#16676
Fix Tap to Pay payments using a wrong account after store switch#16676staskus wants to merge 6 commits intorelease/24.1from
Conversation
|
|
602fcb8 to
ecbf88a
Compare
When switching stores, CardPresentPaymentStore.reset() only disconnected the reader and cleared the service, but left the CommonReaderConfigProvider with a stale siteID and remote from the previous store. This caused the Stripe Terminal session to bind to the wrong merchant's Stripe account after reconnection, resulting in payment intents created on the wrong account and failed captures on the correct one. - Add resetContext() to CardReaderRemoteConfigLoading protocol - Clear paymentGatewayAccount, paymentCancellable, and refundCancellable during reset - Add unit tests for reset behavior
When a reader was already connected with the correct discovery method, preflight would reuse it without checking whether the payment gateway account's siteID matched the current store. After a store switch, this could allow a reader connected for Store A to be reused for Store B without going through discovery, which would skip the config provider update and result in payment intents on the wrong Stripe account. Now the preflight disconnects and reconnects if the siteID doesn't match, similar to existing handling for mismatched discovery methods.
An automatic Tap to Pay reconnection started for Store A could still be in progress when the user switches to Store B. The reconnection controller caches its siteID at creation time, so completing the reconnection after the switch would leave a reader connected with Store A's credentials. This adds cancelReconnection() to TapToPayReconnectionController and calls it from SwitchStoreUseCase.logOutOfCurrentStore() to ensure any in-flight reconnection is stopped before the store context changes.
…witch The DispatchGroup in SwitchStoreUseCase.logOutOfCurrentStore() called group.leave() immediately after dispatching CardPresentPaymentAction.reset, without waiting for the async disconnect to complete. This created a ~1-second window on every store switch where the reader was still connected with old store credentials while the app had already switched to the new store. Add an onCompletion handler to CardPresentPaymentAction.reset and wire group.leave() to it, so finalizeStoreSelection() only runs after the reader is fully disconnected and cleared.
Remove paymentCancellable, refundCancellable, and paymentGatewayAccount cleanup from reset() — these are unrelated to the wrong-Stripe-account bug. Keep only commonReaderConfigProvider.resetContext() which is the core of Fix 1.
…tate Both Bluetooth and TTP connect paths were missing a return after the connectionAttemptInvalidated check. After calling promise(.failure) and disconnect(), execution fell through to connectedReadersSubject.send() and promise(.success), publishing the reader as connected even though the connection was invalidated. Add return to both paths.
71f2990 to
fdb7e1d
Compare
🤖 Build Failure AnalysisThis build has failures. Claude has analyzed them - check the build annotations for details. |
|
The issue was very hard to reproduce. I essentially had to force some conditions to get the same error. However, I didn't jump onto the issue organically. It either means this is a super-rare case, which is possible since we haven't seen many reports about this. And/Or the user setup has some special conditions making the issue more likely to occur. And/Or I have missed a possible path it pay happen. In the end, I'm confident that the issue happens because we have user switching two stores both using TTP. Therefore, the fix is about making sure one TTP connection and config is cleared, reconnection canceled, and connection properly done when switching to a new store. |

Closes: WOOMOB-2197
Description
After switching between stores with separate Stripe accounts, Tap to Pay payments on the new store were charged to the previous store's Stripe account. The capture then failed with "No such payment_intent" because the PI didn't exist on the current store's backend.
Root cause
The Stripe Terminal SDK caches the connection token from when the reader was connected — it does not re-fetch it per payment. If the reader stays connected (or reconnects) with Store A's token after switching to Store B, every payment intent is created on Store A's Stripe account while the server-side capture targets Store B.
On trunk,
disconnect()duringreset()is the only protection against this. Multiple conditions can cause it to be ineffective:group.leave()fired immediately after dispatchingCardPresentPaymentAction.reset, completing the store switch ~1 second before the async disconnect finished. Any preflight starting in this window sees a stale connected reader.TapToPayReconnectionControllerwas not cancelled during store switch — it could reconnect with Store A's cached siteID after disconnect.discoveryMethod, notsiteID— it blindly reused any connected TTP reader, skipping discovery (the only code path that refreshes the connection token).CommonReaderConfigProvider.siteIDwas never cleared during store switch — SDK auto-reconnect would fetch a token for the wrong store.returnin connect paths: After detectingconnectionAttemptInvalidated, both Bluetooth and TTP connect paths calledpromise(.failure)but fell through toconnectedReadersSubject.send()andpromise(.success), publishing the reader as connected even though the connection was invalidated.Once the reader is connected with the wrong token, the bug is self-reinforcing: Path A skips discovery on every retry, so the token is never corrected until the reader is explicitly disconnected.
Bug was reproduced locally by simulating a failed disconnect during store switch, produced the exact same error the customer reports.
Changes
Fix 1: Reset config provider context during store switch
CardPresentPaymentStore.reset()now callscommonReaderConfigProvider.resetContext()to clear the stale siteID. Auto-reconnect after store switch will fail rather than succeed with wrong credentials.Fix 2: Validate siteID when reusing a connected reader in preflight
Preflight now checks that the connected reader's payment gateway siteID matches the current store. On mismatch, it disconnects and reconnects through full discovery, forcing a fresh connection token.
Fix 3: Cancel reconnection during store switch
logOutOfCurrentStore()now callscancelReconnection()to stop in-flight TTP reconnections before the store context changes.Fix 4: Wait for disconnect before completing store switch
CardPresentPaymentAction.resetnow has anonCompletioncallback.group.leave()is wired to it instead of firing immediately —finalizeStoreSelection()only runs after disconnect + clear completes.Fix 5: Return after invalidated connection attempt
Both Bluetooth and TTP connect paths were missing a
returnafter theconnectionAttemptInvalidatedcheck. After callingpromise(.failure)anddisconnect(), execution fell through to publish the reader as connected and callpromise(.success). Addedreturnto both paths so invalidated connections don't leak stale reader state.Testing
Multi-store TTP
Single-store regression
Unit tests
test_cancelReconnection_resets_isReconnectingtest_reset_clears_config_provider_contexttest_reset_disconnects_card_readerVideo - Bug was reproduced locally by simulating a failed disconnect during store switch, produced the exact same error the customer reports.
ScreenRecording_02-16-2026.10-54-53_1.MP4
RELEASE-NOTES.txtif necessary.