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
-
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).
-
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.
-
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.
-
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.
-
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.
-
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.
| identifier | Where | Purpose |
|---|---|---|
native-shell-signin | Sign-in screen | First launch + after sign-out |
native-shell-signin-email | Email field | Sign-in input |
native-shell-signin-password | Password field | Sign-in input |
native-shell-signin-submit | Submit button | Submits sign-in |
native-shell-event-picker | Event list | Post-signin landing |
native-shell-event-row | Each event in picker | One per event; carries event-id |
native-shell-event-shell | Event-day shell | After event tap; contains module tabs |
native-shell-roster-progress | Roster sync progress | Visible during initial sync |
native-shell-tab-checkin | Check-in module tab | Always visible inside event shell |
native-shell-tab-walkin | Walk-in module tab | Visible when access types support walk-in |
native-shell-tab-print | Badge print tab | Visible when bluetooth printer is paired |
native-shell-tab-efx | EFx modules tab | Visible when event has EFx modules |
native-shell-offline-banner | Offline indicator | Visible while offline; carries pending count |
native-shell-pending-sync-banner | Pending-sync indicator | Visible while offline queue has items |
native-shell-camera-permission-prompt | Camera-needed prompt | Visible when camera permission missing |
native-shell-signout | Sign-out button | In settings or profile menu |
native-shell-signout-pending-warning | Pending-data warning on sign-out | Visible only when offline queue non-empty |
native-shell-app-update-banner | Storage-migrated banner | Visible on first launch after app update |
native-shell-storage-migration-error | Migration failure banner | Visible only on storage migration failure |
native-shell-resume-skeleton | Resume placeholder | Visible 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:
- 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.
- 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.