← All stories

TRUNK · native-app-shell

Native App Shell

Trunk Native iOS + Android Roots: ~9 branches across check-in apps and EFx station modules

The iOS/Android native surface for event-day staff. Different physics from the web shell — offline by default, intermittent network, hardware integrations (camera, bluetooth printer, NFC reader), background app states, OS-level permission lifecycles. Branches that root here describe behavior the web harness cannot drive; runner is Detox/XCUITest, not Playwright.

Scope

The native app shell covers: install, sign-in, event roster sync, navigation between built-in modules (check-in, walk-in add, leave-behind, kiosk, EFx stations), and the lifecycle of the app across phone-locked, backgrounded, force-quit, and OS-update states. Branches rooted here include EF-061 (native app), EF-062 (offline roster), EF-063 (walk-in), EF-064 (leave-behind), EF-065 (kiosk mode), EF-067 (badge printing), EF-069 (Listed app for ticket-block users), and the native-station components for EFx access control + lead retrieval + product pickup.

What's NOT in scope: the web kiosk mode (that's the day-of-operations trunk's territory), the web check-in console, anything the staff does from the desktop admin.

Preconditions

  • The signed iOS app is installed via TestFlight or App Store. The signed Android app is installed via Play Store internal testing or production. Sideloaded builds are not in scope for parity proof.
  • The user has an account on the tenant with role ∈ {organizer, support, check-in-staff}.
  • The device clock is correct to within 60 seconds of server clock (NTP-synced).
  • The event the user is checking into has at least one published Access Type and at least one registered guest.

Launch & navigation

  1. Install + first launch.

    App launches to a sign-in screen. No "guest mode" or anonymous use. Sign-in supports email/password + SSO (where the tenant has it configured).

  2. Sign-in persists.

    Auth token stored in iOS Keychain / Android EncryptedSharedPreferences. Token rotation happens silently in background; the user re-signs only when the refresh token is invalidated. Sign-in session survives app backgrounding, force-quit, and OS reboot.

  3. Event picker.

    After sign-in, user lands on a list of events they have access to. Filter by date (today, this week, all). Tap an event to enter its event-day surface.

  4. Roster sync at event entry.

    Tapping an event downloads the full guest roster + access types + ticket-block scopes. Progress shown via ui-progress-bar equivalent. Once synced, the event surface is usable offline. Roster refreshes via background fetch + on-pull-down-to-refresh.

  5. Module nav.

    Inside an event, tabs (or bottom nav on mobile) for: Check-In, Walk-Ins, Notes, Print (when printer paired), EFx (when EFx modules enabled). Each module is a branch story rooted here.

  6. Sign-out.

    Sign-out clears the auth token, the local roster cache, and any pending offline check-ins. Confirmation prompt mentions pending check-ins explicitly when present.

Failure modes

Network drop mid-event

Trigger: WiFi or LTE drops while user is checking guests in. App must remain functional.

Roster is read from local encrypted store. New check-in scans queue locally with a timestamp + device ID + idempotency key. Status banner shows "Offline — N pending sync". On reconnect, queued check-ins replay to the server one at a time; server dedupes by idempotency key. Replay is in chronological order to preserve the audit log.

App backgrounded for 30+ minutes during event

Trigger: staff puts phone in pocket, OS suspends the app, brings it back later expecting state intact.

State persists across suspension. On resume, app silently fetches roster delta + replays pending offline check-ins. Camera and printer connections must be re-established (OS may have reclaimed them). Resume must be ≤ 1.5s to interactive (no full re-launch sequence).

Force-quit during pending offline sync

Trigger: user force-quits the app while offline check-ins are queued; relaunches and the queue should be intact.

Pending queue persists to encrypted storage on every mutation, not just on app pause. Relaunch reads the queue and resumes sync attempts when online. Harness flow: offline + 5 check-ins queued, force-quit, relaunch, assert queue length=5 still pending.

App-updated-while-event-running

Trigger: store auto-update runs during the event; user opens the new version and pending offline data is in the old version's storage layout.

Storage migrations run on every launch BEFORE the UI mounts. Old-format pending data is migrated to the new format. Failed migrations are surfaced as a diagnostic banner with "Contact support" not silently dropped.

Two-staff race on the same guest

Trigger: two staff devices scan the same guest within seconds; both succeed locally; both queue a check-in; server receives both.

Server-side: idempotency-key on check-in (deviceId + guestId + scanTimestamp) dedupes within a tolerance window (default 30s). Client surfaces second device's "already checked in by Alex at 19:41" as a non-error info display. The second staff sees the guest's check-in time + by whom; not a conflict.

Camera permission revoked between launches

Trigger: user denied camera in OS settings between sessions; app launches and the QR scan tab assumes the permission is still granted.

App checks camera permission on every camera entry, not just on first install. If revoked, shows a clear screen: "Camera access required for QR check-in. Open Settings > [App] > Camera." Manual entry option (typed code) remains available throughout.

Sign-out leaves pending offline data behind

Trigger: user signs out without realizing they have 12 pending check-ins; data is lost.

Sign-out flow checks for pending data and prompts: "12 check-ins haven't synced. Sign out anyway?" with options: (a) Wait for sync, (b) Sign out and discard, (c) Sign out keeping pending (resync on next sign-in). Default is (a) — never silently discard.

Stable test attributes

Native test attributes use the platform-native accessibility identifier — accessibilityIdentifier on iOS, contentDescription on Android (with content-desc-as-test-id fallback). Naming convention mirrors the web stories' data-test for consistency.

identifierWherePurpose
native-shell-signinSign-in screenFirst launch + after sign-out
native-shell-signin-emailEmail fieldSign-in input
native-shell-signin-passwordPassword fieldSign-in input
native-shell-signin-submitSubmit buttonSubmits sign-in
native-shell-event-pickerEvent listPost-signin landing
native-shell-event-rowEach event in pickerOne per event; carries event-id
native-shell-event-shellEvent-day shellAfter event tap; contains module tabs
native-shell-roster-progressRoster sync progressVisible during initial sync
native-shell-tab-checkinCheck-in module tabAlways visible inside event shell
native-shell-tab-walkinWalk-in module tabVisible when access types support walk-in
native-shell-tab-printBadge print tabVisible when bluetooth printer is paired
native-shell-tab-efxEFx modules tabVisible when event has EFx modules
native-shell-offline-bannerOffline indicatorVisible while offline; carries pending count
native-shell-pending-sync-bannerPending-sync indicatorVisible while offline queue has items
native-shell-camera-permission-promptCamera-needed promptVisible when camera permission missing
native-shell-signoutSign-out buttonIn settings or profile menu
native-shell-signout-pending-warningPending-data warning on sign-outVisible only when offline queue non-empty
native-shell-app-update-bannerStorage-migrated bannerVisible on first launch after app update
native-shell-storage-migration-errorMigration failure bannerVisible only on storage migration failure
native-shell-resume-skeletonResume placeholderVisible during resume warm-up (≤ 1.5s budget)

Agent test plan

Native runner is Detox (iOS+Android) or XCUITest+Espresso. The current Playwright harness cannot drive these flows — these stories are spec-only until the native runner is wired. Hardware-dependent probes (bluetooth printer, NFC scan, real-camera QR) require a Gate-7-style physical-device evidence packet.

Probe list
- launch-cold-shows-signin: device-state=clean, launch app, native-shell-signin visible
- signin-persists-across-relaunch: signin, force-quit, relaunch, lands on event-picker (not signin)
- signin-persists-across-os-reboot: signin, OS reboot, relaunch, still signed in
- token-refresh-silent: simulate token expiry, observe silent refresh, no signin prompt
- event-picker-shows-tenant-events: signed-in user with 3 events, native-shell-event-row count=3
- event-tap-syncs-roster: tap event, native-shell-roster-progress visible, completes within timeout
- offline-roster-readable: device-state=offline, event shell still shows roster
- offline-checkin-queues: device-state=offline, scan QR, pending-sync-banner increments by 1
- reconnect-replays-queue: device-state goes online, queue drains, pending-sync-banner hides
- replay-preserves-chronological-order: 3 offline check-ins, replay arrives in original order server-side
- two-staff-idempotent: two devices scan same guest within 30s, only 1 check-in row server-side
- backgrounded-resume-fast: background 30min, resume, native-shell-resume-skeleton hidden within 1500ms
- force-quit-preserves-queue: 5 pending offline, force-quit, relaunch, queue length=5 persists
- app-update-migration: storage v1→v2 migration runs, native-shell-app-update-banner visible briefly
- app-update-migration-failure: simulate failed migration, native-shell-storage-migration-error visible
- camera-permission-revoked: revoke camera in OS, launch, camera-permission-prompt visible on scan tab
- camera-permission-prompt-deeplink-settings: prompt has button that opens OS settings for app
- signout-with-pending-warns: 5 pending offline, tap signout, signout-pending-warning visible
- signout-with-clean-immediate: 0 pending, tap signout, lands on signin without prompt
- bluetooth-printer-pair-survives-relaunch: pair printer, relaunch, native-shell-tab-print visible
- local-storage-encrypted-rest: storage file inspected, contents are not readable plaintext

Runner notes

This trunk's stories are not runnable by the current Playwright harness. Two paths to make them runnable:

  1. Detox for iOS + Android with a single test file format. Closest to the existing harness pattern; same JSON-LD probe shape can be adapted with a Detox-specific selector adapter.
  2. XCUITest + Espresso separately. Maximum native fidelity but two test suites to maintain.

Until either is wired, branches rooted here are documentation + spec for hand-driven QA passes against signed builds. Gate 7 evidence packets (signed TestFlight build, physical device scan, server-side check-in row visible in admin) substitute for automated runs.