feat(ios): type into the Apple Pay sheet by serving PassbookUIService in place - #2639
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved review findings remain around registry consistency and payment-host coverage.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds in-place Apple Pay support on iOS by registering PassbookUIService as a payment system-surface host, enabling snapshots and text entry within the Apple Pay sheet.
Changes:
- Adds payment-host registry parity across TypeScript, Swift, and fixtures.
- Generalizes surface disclosures and transition metadata.
- Adds Apple Pay and flattened-input test-app fixtures.
- Updates documentation and dependency configuration.
File summaries
| File | Description |
|---|---|
src/mcp/command-output-schemas.ts |
Updates system-surface transition schema wording. |
src/daemon/system-surface-disclosure.ts |
Adds kind-aware iOS disclosures. |
src/daemon/__tests__/system-surface-disclosure.test.ts |
Updates disclosure tests. |
src/daemon/__tests__/generic-settle.test.ts |
Updates transition tests. |
src/commands/interaction/runtime/post-action-surface.ts |
Uses shared app-surface identity. |
src/commands/interaction/runtime/post-action-surface.test.ts |
Updates post-action tests. |
src/commands/interaction/runtime/interactions.test.ts |
Updates interaction evidence tests. |
packages/platform-apple/src/system-surface-presence.test.ts |
Tests probing across registered hosts. |
packages/platform-apple/src/runner/snapshot-presentation.ts |
Canonicalizes snapshot host provenance. |
packages/platform-apple/src/interactor.ts |
Emits kind-aware warnings. |
packages/platform-apple/src/core/app-launch.ts |
Uses unified host lookup. |
packages/contracts/src/ios-system-surface.ts |
Defines the payment host and disclosure registry. |
packages/contracts/src/ios-system-surface.test.ts |
Tests registry and wording parity. |
packages/contracts/src/interaction.ts |
Exports shared app-surface identity. |
examples/test-app/src/screens/AutomationLabScreen.tsx |
Adds Apple Pay and flattened-input fixtures. |
examples/test-app/README.md |
Documents the new fixtures. |
examples/test-app/pnpm-workspace.yaml |
Pins the Expo JSI dependency. |
examples/test-app/pnpm-lock.yaml |
Updates the dependency lockfile. |
examples/test-app/modules/apple-pay-lab/ios/ApplePayLabModule.swift |
Implements the Apple Pay fixture module. |
examples/test-app/modules/apple-pay-lab/ios/ApplePayLab.podspec |
Defines the native module pod. |
examples/test-app/modules/apple-pay-lab/expo-module.config.json |
Registers the native module. |
examples/test-app/app.config.js |
Adds the Apple Pay entitlement. |
docs/adr/0004-ios-snapshot-backend-strategy.md |
Documents the payment-host decision. |
contracts/fixtures/ios-system-surface-hosts.json |
Adds canonical payment-host metadata. |
apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSystemSurfaceHostPolicy.swift |
Mirrors the payment host in Swift. |
Review details
Files not reviewed (1)
- examples/test-app/pnpm-lock.yaml: Generated file
Suppressed comments (3)
packages/contracts/src/ios-system-surface.ts:79
surfaceNounbypasses the newiosSystemSurfaceHosthelper and readsHOST_BY_BUNDLE_IDdirectly, so there are now two registry lookup paths despite this helper being the single lookup boundary. If lookup behavior or normalization changes later, wording can diverge from the wire reader and open guard; useiosSystemSurfaceHost(bundleId)here and keep the existing throw for an unknown host.
function surfaceNoun(bundleId: string): string {
const host = HOST_BY_BUNDLE_ID.get(bundleId);
if (!host) throw new Error(`${bundleId} is not a registered iOS system surface host`);
return SURFACE_NOUN[host.kind];
packages/contracts/src/ios-system-surface.ts:36
- Adding this host makes the presence probe depend on its
processExecutable, but every presence test that returns a match supplies the Safari path and exits on the first registry entry. There is no test that a Safari miss followed by a device-scopedPassbookUIServicematch returns the payment host, so a bad executable pattern or host-specific routing regression could pass the parity tests while Apple Pay never reaches the runner; add that closest negative/positive case.
bundleId: 'com.apple.PassbookUIService',
kind: 'payment' as const,
processExecutable: 'PassbookUIService.app/PassbookUIService',
packages/platform-apple/src/system-surface-presence.test.ts:65
- The added host is not exercised by the positive-path tests:
PRESENTis stillIOS_SYSTEM_SURFACE_HOSTS[0]andstubProbesreturns the same Safari-shaped process output for everypgrep. A typo inPassbookUIService.app/PassbookUIServiceor its returned host identity would therefore pass. Add a case where the Safari scan is absent and only the payment host scan is scoped to this device, assertingkind: 'payment'.
// One process-table scan per registered host and not a single environment read.
expect(mockRunCmd).toHaveBeenCalledTimes(IOS_SYSTEM_SURFACE_HOSTS.length);
expect(mockRunCmd.mock.calls.every(([command]) => command === 'pgrep')).toBe(true);
- Files reviewed: 24/25 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (!isRecord(value) || typeof value.bundleId !== 'string') return undefined; | ||
| // The shared registry is the authority for both fields: an unknown bundle id is dropped rather | ||
| // than surfaced, mirroring the wire-reader discipline elsewhere in this module, and the kind is | ||
| // read from the registry rather than trusted from the wire. | ||
| const host = iosSystemSurfaceHost(value.bundleId); | ||
| return host && { bundleId: host.bundleId, kind: host.kind }; |
There was a problem hiding this comment.
Added a reader test in snapshot-presentation.test.ts: a PassbookUIService payload stamped web-auth reads back as payment, and an unregistered bundle id is dropped.
497cb21 to
417d9d7
Compare
|
Reviewed The payment host reuses the existing serve-in-place routing without adding a special text-entry path. The runner's served-surface stamp remains authoritative for capture identity, including when a different registered host process lingers. Kind-aware disclosure and registry-derived wire provenance are consistent with that ownership. Validation: built this head and ran 44 focused tests across registry parity, snapshot presentation, host presence, post-action surface transitions, and daemon disclosure; all passed. I reviewed the PR's reported live Apple Pay evidence but did not rerun device verification locally. Code looks good. GitHub reports no conflicts; seven checks are still queued/running, so hold merge until the remaining CI completes successfully. |
There was a problem hiding this comment.
🔵 Needs a closer look
Add host-aware positive presence-probe coverage for PassbookUIService and assert the payment host is returned.
Review details
Files not reviewed (1)
- examples/test-app/pnpm-lock.yaml: Generated file
Suppressed comments (2)
packages/contracts/src/ios-system-surface.ts:42
- The new payment host is covered by registry parity and wire canonicalization, but not by a positive presence-probe test: the existing
system-surface-presence.test.tsstubs everypgrepidentically and always returns the first Safari host. A regression in this entry's executable pattern or host-specific selection could therefore send Apple Pay captures to the AX bridge while the current tests stay green. Add a host-aware probe case that returnscom.apple.PassbookUIServiceand asserts the result carrieskind: 'payment'.
Object.freeze({
bundleId: 'com.apple.PassbookUIService',
kind: 'payment' as const,
processExecutable: 'PassbookUIService.app/PassbookUIService',
}),
packages/platform-apple/src/system-surface-presence.test.ts:65
- The new Passbook host is only covered here by the registry-length assertion; every positive probe case still uses
IOS_SYSTEM_SURFACE_HOSTS[0]and a hard-coded Safari process string. A regression in the second host'sprocessExecutableor returned host would therefore pass. Add a positive probe case that makes only the Passbook process match and asserts that host is returned.
expect(mockRunCmd).toHaveBeenCalledTimes(IOS_SYSTEM_SURFACE_HOSTS.length);
expect(mockRunCmd.mock.calls.every(([command]) => command === 'pgrep')).toBe(true);
- Files reviewed: 25/26 changed files
- Comments generated: 0 new
- Review effort level: Lite
417d9d7 to
2d064f0
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The presence-probe test does not exercise or validate the new PassbookUIService host.
Get a fresh assessment by requesting another Copilot review.
Review details
Files not reviewed (1)
- examples/test-app/pnpm-lock.yaml: Generated file
- Files reviewed: 25/26 changed files
- Comments generated: 1
- Review effort level: Lite
|
Reviewed at 2d064f0. The design stays small: each layer adds one registry entry for PassbookUIService, and disclosure now reads the same registry. A few things are left before merge. Could CHANGELOG.md has no Unreleased entry. Three changes are user-visible: The positive presence test at system-surface-presence.test.ts#L40 uses The PR body says the live check used a runner built from this branch, but it names no commit, and 2d064f0 is a single force-pushed commit. Can you name the SHA of that run, or rerun A few doc lines are not wrapped at 100 columns, for example system-surface-disclosure.ts#L10, the ADR 0004 amendment and the test-app README paragraph. Both Smoke Tests jobs and Android Release are still running. They build the runner, the test app's new native module and the lockfile change, so a failure there may be related. There are no conflicts. Next: name or rerun the live Apple Pay check, and add the CHANGELOG entry and the Passbook presence test. |
…ple Pay change Key the Simulator presence-probe test's pgrep stub on each host's processExecutable instead of answering every pgrep call alike, and add a case where SafariViewService is absent and only a device-scoped PassbookUIService pid is present, asserting the resolved host by registry lookup (kind/bundleId) rather than array index. Without this the positive-presence test always matched SafariViewService by construction and could not tell the two hosts apart. Add the three Unreleased CHANGELOG entries for callstack#2639: bare type/fill now works on the Apple Pay sheet, Simulator captures also probe for a lingering PassbookUIService host, and the iOS system-surface disclosure sentence now names which kind of sheet is on screen. Rewrap the unwrapped comment/doc lines this PR added to the surrounding 100-column style in system-surface-disclosure.ts, ADR 0004, and the test-app README, with no wording changes.
|
The video attached to the PR covers the live Apple Pay check, so that point is settled. I pushed e6a68ce with the small items. It adds CHANGELOG entries for Apple Pay typing, the PassbookUIService lingering probe and the new disclosure wording. The presence test now answers One question is still open: can |
|
Reviewed at e6a68ce. The small items from the 2d064f0 review are done: CHANGELOG entries, a per-host On the open question: yes, One earlier question is still open: can other PassKit calls, like The PR has a merge conflict with main, and no checks ran on this head. Next: rebase on main, then decide whether |
… in place
Bare `type` on iOS types into the first responder of the process the runner
addresses, gated on that process showing a keyboard. The Apple Pay sheet and
its shipping/contact forms live in com.apple.PassbookUIService, so the runner
saw no keyboard in the session app and refused with TEXT_INPUT_NOT_FOCUSED.
Register com.apple.PassbookUIService as a second system surface host (kind
`payment`) next to the web-auth host, in the golden fixture and both the TS
and Swift registries. The existing serve-in-place machinery then routes
snapshots to the runner, addresses the host for interactions, and the
synthesized first-responder route types into the sheet.
Make the disclosure wording kind-aware: `iosSystemSurfaceDisclosure(bundleId)`
replaces the single web sign-in constant, the transition disclosure takes the
`{ from, to }` surface pair, and the wire reader takes the surface kind from
the registry instead of trusting the runner payload.
Test app: add an Apple Pay lab native module and an `accessible={true}`
flattened TextInput fixture to the Automation lab; pin expo-modules-jsi to
56.0.13 so the app builds under Xcode 27.
…ple Pay change Key the Simulator presence-probe test's pgrep stub on each host's processExecutable instead of answering every pgrep call alike, and add a case where SafariViewService is absent and only a device-scoped PassbookUIService pid is present, asserting the resolved host by registry lookup (kind/bundleId) rather than array index. Without this the positive-presence test always matched SafariViewService by construction and could not tell the two hosts apart. Add the three Unreleased CHANGELOG entries for callstack#2639: bare type/fill now works on the Apple Pay sheet, Simulator captures also probe for a lingering PassbookUIService host, and the iOS system-surface disclosure sentence now names which kind of sheet is on screen. Rewrap the unwrapped comment/doc lines this PR added to the surrounding 100-column style in system-surface-disclosure.ts, ADR 0004, and the test-app README, with no wording changes.
e6a68ce to
aec682a
Compare
|
Rebased onto main as aec682a. The only conflict was CHANGELOG.md, and both sides are kept. The code is unchanged from e6a68ce, and |
There was a problem hiding this comment.
🔵 Needs a closer look
The broad iOS runner, native-module, entitlement, and dependency changes require final human review.
Review details
Files not reviewed (1)
- examples/test-app/pnpm-lock.yaml: Generated file
- Files reviewed: 26/27 changed files
- Comments generated: 0 new
- Review effort level: Lite
|
Checked at aec682a. The rebase onto main is identical to e6a68ce apart from CHANGELOG.md, where both sides are kept, so the code verdict from the e6a68ce review still holds. The conflict is gone. Two questions from that review are still open for @okwasniewski: should Smoke Tests, Coverage and Android Release are still running on aec682a. Next: a human review, with those two answers. |
Summary
Bare
typeon iOS types into the first responder of the process the runner addresses, gated on that process showing a keyboard. The Apple Pay sheet and its billing address and contact forms are hosted out of process incom.apple.PassbookUIService, so the runner looked for a keyboard in the session app, found none, and refused withTEXT_INPUT_NOT_FOCUSED.This registers
com.apple.PassbookUIServiceas a second iOS system surface host (kindpayment) next to the existing web-auth host (com.apple.SafariViewService, #2438). The existing serve-in-place machinery does the rest: the presence probe routes captures to the XCTest runner, the runner addresses the host for interactions, and the synthesized first-responder route types into the sheet. No text-entry branch changed.Changes
paymenthost; both parity tests updated.iosSystemSurfaceDisclosure(bundleId)replaces the single web sign-in constant,iosSystemSurfaceTransitionDisclosuretakes the{ from, to }surface pair (withAPP_SURFACEmoved to the contracts package), and the noun table is exhaustive over the kind.iosSystemSurfaceHostis the single registry lookup; the runner snapshot wire reader takes the surface kind from the registry instead of trusting the payload.pgrepper registered host.examples/test-app): newmodules/apple-pay-labnative module presentingPKPaymentAuthorizationControllerrequiring a billing address plus contact email and phone, thein-app-paymentsentitlement, an "Apple Pay sheet" and a "Flattened input" (accessible={true}) fixture in the Automation lab, README docs.expo-modules-jsipinned to 56.0.13 via a workspace override so the app compiles under Xcode 27.Verification
Live on a simulator (iPhone 17 Pro Max, iOS 26.2), runner built from this branch:
typeinto the sheet's search field failed withTEXT_INPUT_NOT_FOCUSED.snapshot -ishows the Wallet tree with the Apple Pay disclosure;presson the search field thentypelands in the sheet (TEXT_ENTRY_ROUTE route=synthesized-first-responder);fill id=given-nameon the billing address form works (lower fields need ascroll downfirst, the keyboard-occlusion guard refuses them otherwise); baretypeinto the phone and email rows (no text field in the tree until typed) works;Pay with Passcodeauthorizes and the app canary readsauthorized.typewith nothing focused still refuses withTEXT_INPUT_NOT_FOCUSED.pnpm test:unit,typecheck,lint,format:check,check:packaged-runner-swift,check:xctest-selectionpass.Recording of the full flow driven by the CLI (open sheet, add billing address by selector, add phone and email by bare
type, authorize):https://github.com/okwasniewski/agent-device/raw/pr-assets/apple-pay-flow/apple-pay-flow.mp4
Notes
PassbookUIServicekeeps running after the sheet closes, so later Simulator captures take the runner with the existingsystem-surface-host-lingeringwarning until the process exits. Same trade-off the web-auth host already carries.