Skip to content

Support card reader auto-reconnection#16586

Open
staskus wants to merge 56 commits intotrunkfrom
woomob-2028-support-card-reader-auto-reconnection
Open

Support card reader auto-reconnection#16586
staskus wants to merge 56 commits intotrunkfrom
woomob-2028-support-card-reader-auto-reconnection

Conversation

@staskus
Copy link
Copy Markdown
Contributor

@staskus staskus commented Jan 29, 2026

WOOMOB-2028

Description

Implements Stripe Terminal SDK auto-reconnection delegate methods for Bluetooth card readers. When a reader unexpectedly disconnects, the SDK automatically attempts to reconnect. Previously, the app did not handle these events, leaving the app state out-of-sync with the card reader connection.

Android PR: woocommerce/woocommerce-android#15047

The PR:

  • Updates the card payment infrastructure to support reconnection state and reconnection cancellation.
  • Observes reconnection states to show ("Reconnecting...") UI in POS Floating Button, POS Payment, and POS Settings
  • Observes connection states to show ("Reconnecting...") UI in Card Reader Settings

I didn't make any changes to the IPP payment flow. The way it's structured is that the payment would just fail if the card reader is disconnected. User can just dismiss it, turn on / reconnect the reader, and start a card reader flow again.

Tests

Tested on

  • M2
  • Chipper
  • Wisepad 3

Scenarios

I implemented explicit handling in these scenarios:

  • IPP Payment
  • POS Payment: Products & Bookings
  • POS Floating Button
  • POS Card Reader Settings
  • IPP Payment Card Reader Settings

Do you think there are any other places I should explicitly handle reconnection state? Otherwise, it happens in the background and immediately reconnects if the card reader connects again.

Cases

In different scenarios, attempt to connect, and then disconnect (turn off) the reader.

  • Confirm that the reconnection state starts
  • Confirm that the reconnection state can be canceled
  • Confirm that if the reader is turned on, it is automatically reconnected
  • Confirm that the reconnection state eventually automatically cancels

Videos

POS Floating Button

POS.-.Floating.bar.MP4

POS Settings

ScreenRecording_01-30-2026.18-09-38_1.MP4

POS Payment

reconnection.in.payment.MP4

Payment -> Manage Card Reader

ScreenRecording_01-30-2026.17-37-54_1.MP4

POS Bookings

pos.bookings.reconnection.MP4

POS Payment Flow with disconnection and reconnection during the payment

ScreenRecording_03-10-2026.16-23-27_1.MP4

IPP Payment flow with disconnection and manual reconnection

It's not handled explicitly. The PR is large enough, but most importantly, the flow is not blocked.

IPP.Flow.mov

  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

- Add CardReaderReconnectionState enum with idle, reconnecting, succeeded, and failed states
- Add reconnectionEvents publisher and cancelReconnection() to CardReaderService protocol
- Add reconnectionCancellation error case to CardReaderServiceError
- Implement reconnection delegate methods in StripeCardReaderService
- Add no-op implementations to NoOpCardReaderService
- Add observeCardReaderReconnectionState action to publish reconnection state changes
- Add cancelReconnection action to cancel in-progress auto-reconnection
- Add CardReaderReconnectionState typealias to Model.swift
- Implement action handlers in CardPresentPaymentStore
- Add reconnecting case to CardPresentPaymentReaderConnectionStatus
- Add cancelReconnection() to CardPresentPaymentFacade protocol
- Implement no-op cancelReconnection in CardPresentPaymentPreviewService
- Handle reconnectionCancellation error in CardPresentPaymentsRetryApproach
- Subscribe to reconnection state publisher and map to connection status
- Implement cancelReconnection() to dispatch cancel action
- Cancel any ongoing reconnection before starting manual connection
- Add reconnecting case to CardReaderConnectionStatusView with spinner and cancel menu
- Handle reconnecting case in TotalsView payment UI logic
- Add cancelReconnection() method to PointOfSaleAggregateModel
- Add reconnection simulation methods to MockCardReaderService
- Add tests for observeCardReaderReconnectionState action
- Add test for cancelReconnection action
- Update MockCardPresentPaymentService with cancelReconnection
- Add cancelReconnection test to PointOfSaleAggregateModelTests
- Update CardPresentPaymentServiceScreenshotMock
@staskus staskus added type: task An internally driven task. feature: POS labels Jan 29, 2026
@dangermattic
Copy link
Copy Markdown
Collaborator

dangermattic commented Jan 29, 2026

1 Warning
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.

Generated by 🚫 Danger

@staskus staskus changed the title POS: Support card reader auto-reconnection Support card reader auto-reconnection Jan 29, 2026
@staskus staskus added the feature: mobile payments Related to mobile payments / card present payments / Woo Payments. label Jan 29, 2026
@wpmobilebot
Copy link
Copy Markdown
Collaborator

wpmobilebot commented Jan 29, 2026

App Icon📲 You can test the changes from this Pull Request in WooCommerce iOS Prototype by scanning the QR code below to install the corresponding build.

App NameWooCommerce iOS Prototype
Build Numberpr16586-17f408c
Version24.3
Bundle IDcom.automattic.alpha.woocommerce
Commit17f408c
Installation URL2foi3p3kpv5r0
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

- Observe reconnection state in BluetoothCardReaderSettingsConnectedViewModel
- Keep showing connected reader view during reconnection
- Disable update/disconnect buttons and show spinner during reconnection
Cancel any ongoing auto-reconnection before initiating a new
Bluetooth reader discovery to prevent conflicts.
Add cancelReconnection action handling to MockCardPresentPaymentsStoresManager
and RefundSubmissionUseCaseTests to prevent test timeouts.
Update MainTabBarControllerTests to handle the .observeCardReaderReconnectionState action in the test action handler. Otherwise, initialization of CardPresentPaymentService stalls the test.
Configure the Stripe Terminal SDK to automatically attempt
reconnection when a Bluetooth reader unexpectedly disconnects.
Update the didStartReconnect delegate method to use the new Stripe
SDK signature that includes the disconnect reason parameter. Also
clear connected readers when reconnection starts so the UI correctly
shows the reconnecting state instead of connected.
When canceling reconnection, if the Stripe SDK returns error code
cancelFailedAlreadyCompleted (1010), treat it as success rather than
an error. This race condition occurs when reconnection completes
naturally just before the cancel request is processed. Since the
user's intent to stop reconnection was effectively achieved, there's
no need to log an error.
Stripe: Adjust reconnection cancellation handler to avoid clearing connectedReaders when cancellation failed due to already-completed reconnection; only clear readers when cancellation actually succeeded or failed for other reasons, and ensure reconnectionState is set to .idle in the appropriate branches.

Yosemite store: Replace .subscribe(Subscribers.Sink(...)) with .sink(...) and store the returned AnyCancellable in the cancellables set to retain the subscription.

WooCommerce adaptor: Remove the preemptive await cancelReconnection() call from connectReader to avoid unnecessarily cancelling/interrupting concurrent reconnection logic before starting a manual connection.
- Show "Reconnecting to card reader..." status in connected view
- Add "Cancel Reconnection" button during reconnection
- Preserve reader info (name, battery, firmware) during reconnection
- Prevent searching view from showing during reconnection
- Refactor button states using enums for cleaner code
- Show reader info during reconnection via PointOfSaleSettingsController
- Add reconnecting menu button with cancel option
- Disable firmware update during reconnection
- Add test for reconnecting state providing reader info
- Update mock to support connection status directly
Show "Reconnecting reader..." message when reader is reconnecting during
checkout. This prevents the confusing "Scanning for readers" popup by not
auto-starting payment collection during reconnection.
@staskus staskus modified the milestone: 24.2 Feb 2, 2026
@staskus staskus requested a review from joshheald February 4, 2026 17:00
@staskus staskus modified the milestones: 24.1, 24.2 Feb 5, 2026
@staskus staskus modified the milestones: 24.2, 24.3 Feb 27, 2026
@wpmobilebot wpmobilebot modified the milestones: 24.3, 24.4 Mar 6, 2026
@wpmobilebot
Copy link
Copy Markdown
Collaborator

Version 24.3 has now entered code-freeze, so the milestone of this PR has been updated to 24.4.

staskus added 3 commits March 9, 2026 14:28
Resolve merge conflicts between trunk's POSPaymentModel refactoring
and the reconnection feature. Keep trunk's delegation pattern while
preserving reconnection support (cancelReconnection, reconnecting
UI state, and reconnection tests). Add reconnection handling to
POSCardPaymentContentView and fix missing .reconnecting case in
POSPaymentModel's switch statement.
…ment

Cancel active reconnection before cancelling payment to prevent
UI hangs when reader is in reconnecting state.
@staskus staskus marked this pull request as draft March 9, 2026 13:09
staskus added 12 commits March 10, 2026 11:29
- Add cancelReconnection() to POSPaymentModel so both cart and
  bookings flows can cancel reconnection
- Pass cancelReconnectionAction in POSPaymentContentView
- Delegate cancelReconnection through paymentModel in aggregate model
- Add @mainactor since calling paymentModel synchronously requires it
Set connected reader and reconnecting status before creating the
aggregate model to avoid needing Task.sleep for state propagation.
…y async

Merge the two separate Task blocks into one sequential block so
cancellation completes before preflight starts. Use withCheckedContinuation
to actually await the store action completion.
Replace inline TotalsViewHelper() and POSPaymentViewHelper() instantiations
inside computed properties with existing stored properties to avoid
redundant allocations on every SwiftUI body evaluation.
Set connectionStatus before creating the SUT so the initial publisher
value is already .connected when the subscription starts, removing
the need for a fragile 100ms sleep.
…edViewModel

Log the error and reset readerReconnectionCancellationInProgress on
failure to prevent the UI from being stuck in a permanent cancelling state.
Use .onChange(of: cardReaderConnectionStatus) so the flag resets whenever
the connection status transitions, not only when the view appears.
Change from title case "Cancel Reconnection" to sentence case
"Cancel reconnection" for consistency with all other button labels
in the reconnection flow. Update localization key accordingly.
A no-op cancel should succeed rather than fail, consistent with how
the real service treats cancellation when no reconnection is in progress.
All call sites provide a non-nil value, so the optional adds no safety
and silently no-ops if accidentally nil. Matches connectCardReaderAction.
Only log the error when a reconnection was actually in progress to
avoid noisy log output from expected SDK callbacks.
@staskus staskus marked this pull request as ready for review March 10, 2026 15:06
@staskus
Copy link
Copy Markdown
Contributor Author

staskus commented Mar 10, 2026

@joshheald @iamgabrielma I integrated the new pos payment model changes, retested everything, and made a few fixes.

@wpmobilebot wpmobilebot modified the milestones: 24.4, 24.5 Mar 20, 2026
@wpmobilebot
Copy link
Copy Markdown
Collaborator

Version 24.4 has now entered code-freeze, so the milestone of this PR has been updated to 24.5.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature: mobile payments Related to mobile payments / card present payments / Woo Payments. feature: POS type: task An internally driven task.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants