Skip to content

feat(ios): type into the Apple Pay sheet by serving PassbookUIService in place - #2639

Merged
thymikee merged 2 commits into
callstack:mainfrom
okwasniewski:oskar/ios-apple-pay-system-surface
Sep 15, 2026
Merged

thymikee merged 2 commits into
callstack:mainfrom
okwasniewski:oskar/ios-apple-pay-system-surface

Conversation

@okwasniewski

@okwasniewski okwasniewski commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Summary

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 billing address and contact forms are hosted out of process in com.apple.PassbookUIService, so the runner looked for a keyboard in the session app, found none, and refused with TEXT_INPUT_NOT_FOCUSED.

This registers com.apple.PassbookUIService as a second iOS system surface host (kind payment) 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

  • Fixture, TS registry, and Swift registry gain the payment host; both parity tests updated.
  • Disclosure wording is kind-aware: iosSystemSurfaceDisclosure(bundleId) replaces the single web sign-in constant, iosSystemSurfaceTransitionDisclosure takes the { from, to } surface pair (with APP_SURFACE moved to the contracts package), and the noun table is exhaustive over the kind.
  • iosSystemSurfaceHost is the single registry lookup; the runner snapshot wire reader takes the surface kind from the registry instead of trusting the payload.
  • Presence probe test asserts one pgrep per registered host.
  • ADR 0004 amendment notes the payment host and why text entry needs it.
  • Test app (examples/test-app): new modules/apple-pay-lab native module presenting PKPaymentAuthorizationController requiring a billing address plus contact email and phone, the in-app-payments entitlement, an "Apple Pay sheet" and a "Flattened input" (accessible={true}) fixture in the Automation lab, README docs. expo-modules-jsi pinned 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:

  • Before the registry change, bare type into the sheet's search field failed with TEXT_INPUT_NOT_FOCUSED.
  • After: snapshot -i shows the Wallet tree with the Apple Pay disclosure; press on the search field then type lands in the sheet (TEXT_ENTRY_ROUTE route=synthesized-first-responder); fill id=given-name on the billing address form works (lower fields need a scroll down first, the keyboard-occlusion guard refuses them otherwise); bare type into the phone and email rows (no text field in the tree until typed) works; Pay with Passcode authorizes and the app canary reads authorized.
  • Bare type with nothing focused still refuses with TEXT_INPUT_NOT_FOCUSED.
  • The flattened RN input already worked and is now covered by a fixture.
  • Swift registry parity tests pass on the simulator; pnpm test:unit, typecheck, lint, format:check, check:packaged-runner-swift, check:xctest-selection pass.

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

PassbookUIService keeps running after the sheet closes, so later Simulator captures take the runner with the existing system-surface-host-lingering warning until the process exits. Same trade-off the web-auth host already carries.

Copilot AI lite review requested due to automatic review settings September 15, 2026 15:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

  • surfaceNoun bypasses the new iosSystemSurfaceHost helper and reads HOST_BY_BUNDLE_ID directly, 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; use iosSystemSurfaceHost(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-scoped PassbookUIService match 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: PRESENT is still IOS_SYSTEM_SURFACE_HOSTS[0] and stubProbes returns the same Safari-shaped process output for every pgrep. A typo in PassbookUIService.app/PassbookUIService or 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, asserting kind: '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.

Comment on lines +58 to +63
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 };

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/contracts/src/interaction.ts Outdated
Copilot AI review requested due to automatic review settings September 15, 2026 15:51
@okwasniewski
okwasniewski force-pushed the oskar/ios-apple-pay-system-surface branch from 497cb21 to 417d9d7 Compare September 15, 2026 15:51
@thymikee

Copy link
Copy Markdown
Member

Reviewed 417d9d74f99bbef0d37f007640156aee5db4ba67: no actionable code findings.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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.ts stubs every pgrep identically 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 returns com.apple.PassbookUIService and asserts the result carries kind: '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's processExecutable or 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

@okwasniewski
okwasniewski force-pushed the oskar/ios-apple-pay-system-surface branch from 417d9d7 to 2d064f0 Compare September 15, 2026 16:10
Copilot AI review requested due to automatic review settings September 15, 2026 16:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread packages/platform-apple/src/system-surface-presence.test.ts
@thymikee

Copy link
Copy Markdown
Member

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 IosSystemSurfaceProvenance.kind go? The reader now derives it from the registry. If callers carried the resolved host instead of a bundle id, the throw in surfaceNoun would be unreachable by type.

CHANGELOG.md has no Unreleased entry. Three changes are user-visible: type and fill into the Apple Pay sheet now work; every Simulator capture runs one more pgrep, and the runner can go through system-surface-host-lingering while PassbookUIService stays alive after the sheet closes, in any app that showed the sheet; and the web sign-in disclosure sentence changed. Can you add an entry for these?

The positive presence test at system-surface-presence.test.ts#L40 uses IOS_SYSTEM_SURFACE_HOSTS[0], and every pgrep stub answers the same, so it always matches SafariViewService. Can the stub key on each host's processExecutable, with a case where only a PassbookUIService pid is present that expects the payment entry?

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 type into the sheet on 2d064f0 and show the TEXT_ENTRY_ROUTE route=synthesized-first-responder line? Also, do other PassKit calls, like canMakePayments or Wallet pass views, start PassbookUIService? That would widen who sees the lingering-host path.

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.

thymikee added a commit to okwasniewski/agent-device that referenced this pull request Sep 15, 2026
…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.
Copilot AI review requested due to automatic review settings September 15, 2026 17:15
@thymikee

Copy link
Copy Markdown
Member

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 pgrep per host executable, and a new case with only a PassbookUIService pid resolves to the payment entry. That case fails if the registered Passbook executable changes. The new doc lines are rewrapped to 100 columns with no wording change. pnpm check:affected --run passes on e6a68ce.

One question is still open: can IosSystemSurfaceProvenance.kind go now that the reader derives it from the registry?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@thymikee

Copy link
Copy Markdown
Member

Reviewed at e6a68ce. The small items from the 2d064f0 review are done: CHANGELOG entries, a per-host pgrep presence test with a PassbookUIService-only case, and rewrapped doc lines. The video on the PR covers the live Apple Pay check.

On the open question: yes, IosSystemSurfaceProvenance.kind can go. readSystemSurfaceProvenance at snapshot-presentation.ts#L63 copies it from the registry, and no TypeScript code reads it after that. Snapshot state keeps only the bundle id, and every disclosure sentence looks the kind up again through surfaceNoun(bundleId), which keeps a throw for a bundle id the reader never produces. Could the provenance, snapshot state and disclosure inputs carry the registry host instead, so kind is deleted and surfaceNoun takes the host and cannot throw? The Swift runner still writes kind on the wire, and that can stay for now. A follow-up issue is fine if you prefer not to widen this PR.

One earlier question is still open: can other PassKit calls, like canMakePayments or Wallet pass views, start PassbookUIService? That would widen which apps reach the lingering-host path.

The PR has a merge conflict with main, and no checks ran on this head. Next: rebase on main, then decide whether kind goes here or in a follow-up.

okwasniewski and others added 2 commits September 15, 2026 20:02
… 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.
@thymikee
thymikee force-pushed the oskar/ios-apple-pay-system-surface branch from e6a68ce to aec682a Compare September 15, 2026 18:13
Copilot AI review requested due to automatic review settings September 15, 2026 18:13
@thymikee

Copy link
Copy Markdown
Member

Rebased onto main as aec682a. The only conflict was CHANGELOG.md, and both sides are kept. The code is unchanged from e6a68ce, and pnpm check:affected --run passes on aec682a. What is left is the author's call: remove IosSystemSurfaceProvenance.kind here or in a follow-up issue, and whether other PassKit calls also start PassbookUIService.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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

@thymikee

Copy link
Copy Markdown
Member

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 IosSystemSurfaceProvenance.kind go here or in a follow-up issue, and can other PassKit calls, like canMakePayments or Wallet pass views, start PassbookUIService?

Smoke Tests, Coverage and Android Release are still running on aec682a. Next: a human review, with those two answers.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 15, 2026
@thymikee
thymikee merged commit 6ce8657 into callstack:main Sep 15, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants