# Mobile handoff examples Mobile consumes only the integrating partner's backend projection. It never imports `@chiahealth/partner` and never receives a Chia API key, webhook secret, authenticated asset URL, raw catalog object, or patient/order status. ## Platform snippets | Platform | System-browser implementation | |---|---| | iOS / UIKit | [`ios/Handoff.swift`](https://partners.chia.health/source/mobile/ios/Handoff.swift.txt) | | Android | [`android/Handoff.kt`](https://partners.chia.health/source/mobile/android/Handoff.kt.txt) | | React Native | [`react-native/Handoff.tsx`](https://partners.chia.health/source/mobile/react-native/Handoff.tsx.txt) | | Flutter | [`flutter/handoff.dart`](https://partners.chia.health/source/mobile/flutter/handoff.dart.txt) | Each snippet opens HTTPS with the OS external-browser API and includes an optional generic return handler that ignores query and fragment data. The executable web reference and closed backend projection live in `examples/partner-reference-backend/src/renderer-assets.ts` and `renderer-contract.ts`. Every supported shell uses the same reviewed [`renderer-conformance.v1.json`](https://partners.chia.health/source/mobile/renderer-conformance.v1.json) decision, interaction, accessibility, and forbidden-behavior cases. Run them from the bundle root with: ```bash python3 scripts/partner_contracts/check_renderer_conformance.py ``` ## Input contract from the partner's backend The backend must already have: 1. fetched/cached the catalog on a schedule; 2. verified and mirrored the image at a neutral partner-owned URL; 3. derived fresh authenticated partner account service-state evidence; 4. called `resolveCatalogHandoff`; 5. removed hidden items and suppressed the session action for `neutral_only`. Mobile receives presentation fields plus one closed handoff decision: ```ts type MobileHandoff = | { mode: "neutral_only"; heading: string; body: string; actionLabel: "Continue to Chia"; url: string; } | { mode: "share_or_neutral"; heading: string; body: string; recipient: { legal_name: string; privacy_contact: string }; dataShared: readonly string[]; purpose: string; compensationDisclosure: string; policyUrls: Record; primary: { actionLabel: "Share selection and continue"; sessionCreationPath: "/v2/handoff-sessions"; disclosureVersion: string; }; secondary: { actionLabel: "Continue without sharing"; url: string; }; }; ``` Do not generalize the action-label literals or add an arbitrary URL field. Refresh the backend projection at tap time if local service-state evidence or the publication may have expired. ## Tap sequence 1. Render every required catalog field before the CTA in `presentation_contract.block_order`. 2. On CTA tap, fetch/resolve fresh partner-local eligibility without calling or preloading Chia. 3. Render the API-supplied JIT heading, body, recipient/data/purpose/compensation/policy links, and exact actions. 4. For no-share, pass the neutral URL directly to the platform snippet. For share, send the catalog ID, disclosure version, `affirmed:true`, and affirmative timestamp to the partner's authenticated backend `/handoff-session` endpoint with a persisted random UUIDv4 idempotency key. 5. Open only the short-lived URL returned to that initiating response; never store it. 6. Let the OS browser own the Chia experience. Do not observe it. Disable both actions synchronously before starting the share request. Duplicate taps join that one in-flight operation. Keep the exact body and UUIDv4 after a network error, `429`, or server error; discard them after a definitive client rejection and fetch a new projection before another attempt. On app foreground or restored navigation, fetch a current projection and render it again without using focus timing as an outcome signal. Never persist the returned URL. For `share_or_neutral`, `Share selection and continue` and `Continue without sharing` must both remain usable. For `neutral_only`, no session-creation action may exist. The generated handoff URL exists only after affirmative share consent, expires in 15 minutes, and must never enter durable mobile state or telemetry. ## Forbidden mobile behavior - WebView, custom-tab JavaScript bridge, or embedded browser; - `fetch`, `HEAD`, preview, prefetch, preconnect, link expansion, or availability probing; - redirector, link shortener, URL rewrite, custom header, query parameter, or added fragment; - persistence, clipboard capture, analytics, attribution, crash breadcrumb, screenshot tooling, or logs containing a selection/handoff URL; - inference from app focus, browser return timing, cookies, or a generic return; - any Chia SDK, API key, webhook secret, finance data, authenticated asset URL, or lifecycle/status call. Treat the opaque `phs_…` URL as sensitive short-lived routing context, even though it is not an authentication credential. The OS browser receives it only after an explicit user action; no status lookup or patient/order callback exists. ## Generic return If separately approved and registered, a single generic partner Universal Link/App Link may return the user to marketplace home. The handler validates only the registered return host, ignores the entire query and fragment, and never displays or records a completion status. The return action must be identical regardless of abandonment, eligibility, payment, prescription, or fulfillment. There is no automatic callback. A production share URL enters Chia's live co-branded journey; a generic return, when separately approved, always returns to marketplace home without communicating whether the user completed identity, intake, payment, care, or fulfillment. ## Mobile test matrix - all required card fields fit supported accessibility text sizes without truncation; - `hidden` never renders, `neutral_only` has one action, `share_or_neutral` has two equal-access actions; - stale/missing/unsupported service-state evidence fails safely; - each supported platform opens an external OS browser, never an embedded view; - instrumentation/traffic capture shows no request before tap and no URL in telemetry; - invalid/expired context remains generic Chia and produces no partner callback; - generic return ignores crafted query/fragment values and routes only to marketplace home. - background and restored views refresh the projection without inferring an outcome; - double taps produce one session request and one external navigation, and an uncertain retry reuses the exact body and idempotency key; - 200% text size, screen-reader order, keyboard/switch access, reduced motion, and a 320 CSS-pixel viewport retain full untruncated copy and every action. See [Catalog, assets, and handoff](https://partners.chia.health/docs/catalog-and-handoff/) for the full publication and JIT contract.