Skip to content

feat(payment): take card payments on the Switchio Pay terminal - #94

Merged
pepakriz merged 3 commits into
mainfrom
switchio
Sep 24, 2026
Merged

pepakriz merged 3 commits into
mainfrom
switchio

Conversation

@pepakriz

Copy link
Copy Markdown
Contributor

Summary

  • Card payments via SwitchioPay (Android only). A local Capacitor plugin (SwitchioPlugin.java) drives the SwitchioPay app's ECR v8 intent API; src/core/native/switchio.ts wraps it behind SwitchioTerminalDep. The method is hidden outside the native runtime, because an intent:// launch from a browser/PWA returns its result to the browser, not to the page.
  • Domain mirrors cash. paymentCardSwitchio stores the terminal result, a new cardSwitchio account receives the account transaction, and a reconciliation claim marks the payment paid. The claim is written before the terminal's trace fields.
  • No double charges.
    • The terminal is only asked to charge a payment that is still pending. Another method on the same payment may already have settled it.
    • The ECR request id is persisted before the intent leaves, as transactionId and unresolvedTransactionId. While the last attempt's outcome is unknown (unreadable result, bridge failure, app killed), another attempt is refused until staff confirms having checked SwitchioPay.
    • Only a missing SwitchioPay app counts as "unavailable". Every other bridge failure is reported as an unknown outcome, never as "declined".
  • Restored results. If Android kills the app while SwitchioPay is in the foreground, the result arrives as a Capacitor appRestoredResult event carrying the echoed request id. SwitchioRestoredResult settles it.
  • Tip. ECR adds tipAmount to the amount it receives, so the request sends the stored total minus the tip.
  • Settings. The card method is enabled under payment accounts. It is not offered in onboarding.
  • Separate commit: kiosk mode on Android. The system bars are hidden and re-hidden on window focus. WebView does not expose them through env(safe-area-inset-*), so the navigation bar was covering the page.

Test plan

  • bun run check:ts
  • bun run check:tests: unit tests for the paid/canceled guards, the unresolved-attempt block and explicit retry, restored-result settlement, and the tip split
  • On an Android device with SwitchioPay: an approved payment, a declined payment, and a payment cancelled on the terminal
  • With "Don't keep activities" enabled: a card payment still settles after the app is restored
  • SwitchioPlugin.java compiles in the Android build (it was not compiled locally)

🤖 Generated with Claude Code

https://claude.ai/code/session_01BK6n6yMEXHuB1LsHBmX4Pa

pepakriz and others added 2 commits September 24, 2026 14:00
Edge-to-edge alone leaves the navigation bar covering the bottom of the
page: Android WebView maps only the display cutout onto CSS
`env(safe-area-inset-*)`, never the system bars, so the web layer cannot
pad around them. Hiding the bars (transiently revealed by a swipe) and
re-hiding them whenever the window regains focus keeps the app a
fullscreen kiosk for a whole shift, including after returning from
another activity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BK6n6yMEXHuB1LsHBmX4Pa
Adds a card payment method backed by the SwitchioPay app's ECR v8 intent
API. The protocol has no HTTP or local-socket interface, so the method is
Android-only and stays hidden outside the Capacitor runtime: an
`intent://` launch from a browser or PWA returns its result to the
browser, not to the page.

The native half is a local Capacitor plugin (`SwitchioPlugin.java`,
registered in `MainActivity`); `src/core/native/switchio.ts` wraps it
behind `SwitchioTerminalDep` and interprets the result. A payload that
cannot be read on a success result code, or a bridge failure other than a
missing SwitchioPay app, is reported as an unknown outcome rather than a
decline — the card may well have been charged, so it must never be shown
as "declined".

Domain-side this mirrors cash: `paymentCardSwitchio` records the terminal
result, a new `paymentCardSwitchio` account kind receives the account
transaction, and a reconciliation claim is what makes the payment read as
paid. The claim is written before the terminal's trace fields so a crash
leaves a paid payment with a thinner audit trail rather than a charged
card that still shows as unpaid.

The terminal is only asked to charge a payment that is still pending: the
methods can coexist on one payment, and the wait screen's own paid check
trails the database, so without the guard a payment already settled over
Lightning could be charged again by card. The ECR request id is persisted
before the intent leaves, both as `transactionId` and as
`unresolvedTransactionId`; the latter is cleared on a definite answer and
blocks another attempt while the last outcome is unknown, until staff
confirms having checked SwitchioPay. When Android kills the app while the
terminal is in the foreground, the result comes back as a Capacitor
`appRestoredResult` event carrying the echoed request id, and
`SwitchioRestoredResult` settles it.

ECR adds `tipAmount` to the `amount` it is given (its result documents
`amount` as the total including the tip, its request does not), so
`calculatePaymentBaseAmount` takes the tip back out of the stored total
before the request is built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BK6n6yMEXHuB1LsHBmX4Pa
@vercel

vercel Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
payky Ready Ready Preview Sep 24, 2026 3:38pm UTC

The payment accounts page already warned outside the native app, but on
Android a missing SwitchioPay app only surfaced when a card payment was
attempted. The plugin now probes the exact ECR payment intent it launches,
declared in the manifest's <queries> so Android 11+ package visibility lets
it resolve, and the card row shows a warning when nothing handles it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BK6n6yMEXHuB1LsHBmX4Pa
@pepakriz pepakriz added the run-e2e Runs the Playwright e2e suite (production build) in CI for this PR label Sep 24, 2026
@pepakriz
pepakriz merged commit 33a1fb2 into main Sep 24, 2026
7 checks passed
@pepakriz
pepakriz deleted the switchio branch September 24, 2026 15:59

This branch was successfully deployed

1 active deployment
Preview — 08bce5d3 Deployed Sep 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-e2e Runs the Playwright e2e suite (production build) in CI for this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant