← All stories

BRANCH · ef-061-native-checkin-app

Native iOS & Android Check-In App

EF-061 Persona: Event-day staff Stage: Day-of (native surface) Roots in: native-app-shell Runner: Detox/XCUITest (not yet wired) — Gate 7 manual evidence

The signed iOS + Android check-in app, distributed via the App Store + Play Store. EF-061 is the synthesis story: when a signed build passes the native-app-shell trunk's full failure-mode catalog AND the cluster-specific check-in-flow modes below on physical hardware, EF-061 ships green. Until the Detox/XCUITest runner is wired, evidence is a Gate 7 packet (signed build + screenshots + server-side row).

Preconditions

  • Inherits native-app-shell trunk: signed build installed, user auth, event roster synced.
  • Event has at least one access type with confirmed registrations.
  • iOS minimum: 16.0. Android minimum: API 30 (Android 11). Older versions show a forced-upgrade screen on launch.
  • Camera permission granted (or manual-entry fallback available).

Happy path

  1. Staff taps Check-In tab.

    Inherits native-app-shell tab navigation. Tab opens the camera viewfinder by default with a "Manual entry" toggle in the top corner.

  2. Camera scans guest QR.

    QR decode + payload validation happens locally. If valid + recognized for this event, transitions to confirmation screen showing guest name, access type, status, any check-in notes (EF-047). Staff taps "Confirm check-in" or "Cancel."

  3. Confirm writes locally + sends to server.

    Local write goes immediately (works offline). Network call queues if offline (per native-app-shell trunk's queue contract). Confirmation screen shows green check + auto-dismisses after 1.5s, returning to viewfinder for the next scan.

  4. Manual entry path.

    Toggle to manual entry — text field for typing the guest's confirmation code OR an autocomplete picker over the local roster (search by name/email). Same confirmation screen + same write path as QR.

Failure modes

Force-upgrade on too-old OS

Trigger: launch on iOS 15 or Android 10 (below minimums).

App shows a forced-upgrade screen with App Store / Play Store deep-link. No bypass — older versions can't access the data layer reliably. Staff handed an older device should swap it. Harness: launch on emulator at OS < minimum, assert force-upgrade screen visible, no other navigation accessible.

Two-staff idempotent same-guest

Trigger: two staff devices scan the same guest within 30s of each other.

Inherits native-app-shell trunk's two-staff-idempotent contract. The second staff's confirmation screen surfaces "Checked in by [first staff name] at [time]" — informational, not error. Server-side check-in count stays at 1. Harness: physical 2-device test, scan same guest, server row count = 1, second device shows informational banner.

QR for wrong event

Trigger: staff scans a QR from a different event (guest brought the wrong code).

Local payload validation catches event-id mismatch. Confirmation screen replaced with red banner: "This QR is for a different event. Ask the guest to confirm." Anti-probing — the banner does NOT name the other event (could leak across tenants). Harness: scan cross-event QR, banner visible, no API call to wrong event.

Tampered QR payload

Trigger: scanned QR has a forged or mutated signature.

QR signature validation fails locally. Same red banner as wrong-event ("This QR is invalid"). Identical UI shape — anti-probing prevents an attacker from distinguishing forgery from cross-event confusion. Harness: synthetic forged QR, banner visible, no API call.

Camera permission revoked between launches

Trigger: user denies camera in OS settings between sessions.

App detects on tab entry (per trunk contract) and shows the camera-needed prompt with deeplink to settings. Manual-entry tab remains accessible — never blocks the flow on camera alone. Harness: revoke camera, tap Check-In, prompt visible, manual entry still works.

Network flaky during confirm

Trigger: spotty WiFi during confirm tap; request hangs.

Local write commits immediately (idempotent by deviceId + guestId + scanTimestamp). Confirmation screen advances with a small "Syncing..." indicator. Pending sync banner increments. Same idempotency-key prevents duplicates if the request later succeeds. Harness: stub network with 50% packet loss, confirm 10 scans, assert all 10 land server-side exactly once.

Battery drain pacing

Trigger: staff uses the app for 6+ hours of continuous check-in at a long event.

Camera viewfinder uses adaptive frame rate (15 fps idle, 30 fps when motion detected). Background sync uses jittered backoff. Battery drain target: ≤ 30% over 6h of moderate use on a 2022+ device. Harness: physical device 6h burn-in test with battery telemetry, assert drain ≤ threshold.

Device clock skew

Trigger: staff device clock is 5 minutes ahead of server.

Local check-in timestamps use device clock for ordering, but server reconciles using server-clock at receive time. The audit log records both client_ts + server_ts. UI-displayed times use server_ts. Harness: stub device clock +5min, scan, assert server row's check-in timestamp is server's now, not device's now.

Already-checked-in guest scanned again

Trigger: staff scans a guest who already checked in earlier today.

Confirmation screen shows the prior check-in time + "Already checked in" status. Staff can choose: dismiss (no second check-in row) OR "Re-confirm" if they need to update notes. Re-confirm doesn't create a duplicate row; it appends to a check-in events list. Harness: scan same guest twice, second shows already-checked-in screen, server has 1 check-in row + 1 check-in event row.

App update mid-event preserves pending queue

Trigger: store auto-update pushes new version while staff has 12 unsynced check-ins.

Inherits trunk's app-update-while-event-running contract. Storage migrations run before UI mounts. Pending queue is preserved through the update. Harness: simulate mid-event update with pending queue, post-update queue length unchanged.

Cross-tenant guest scan

Trigger: a staff token from tenant A scans a QR signed by tenant B (theoretically impossible with proper token scoping, but defense-in-depth).

Server returns 404 (anti-probing — same as wrong-event). Audit log captures the attempt for monitoring. Harness: forge cross-tenant QR + auth, attempt confirm, server 404 with no leak.

Stable test attributes

identifierWherePurpose
checkin-camera-viewfinderCheck-In tab defaultQR scan surface
checkin-manual-entry-toggleTop corner of Check-In tabSwitches to manual entry mode
checkin-manual-entry-inputManual entry modeCode or roster autocomplete
checkin-confirmation-screenAfter valid scanGuest details + confirm/cancel
checkin-confirm-buttonConfirmation screenCommits the check-in
checkin-cancel-buttonConfirmation screenReturns to viewfinder without commit
checkin-already-checked-inVisible if guest already checked inShows prior time + by whom
checkin-cross-event-bannerVisible on wrong-event QRGeneric "different event" message
checkin-tampered-bannerVisible on forged QRIdentical shape to cross-event banner
checkin-force-upgrade-screenVisible on too-old OSApp Store / Play Store deeplink
checkin-syncing-indicatorAfter confirm, while offline-queuedSmall "Syncing..." badge

Agent test plan

Native runner not yet wired. Until then, evidence is Gate 7 packet: signed TestFlight + Play Store internal-track build, physical-device scans, screenshots of each probe state, server-side row inspection.

Probe list
- (manual) launch-on-min-os: install on iOS 16.0 + Android API 30, no force-upgrade
- (manual) launch-on-too-old-os: emulator iOS 15 + API 28, force-upgrade-screen visible
- (manual) scan-valid-guest: physical QR scan, confirmation screen shows correct guest
- (manual) two-staff-idempotent: 2 devices scan same guest, server row count=1, second shows informational
- (manual) wrong-event-banner: scan QR from different event, cross-event-banner visible
- (manual) tampered-qr-banner: synthetic forged QR, tampered-banner visible (identical shape)
- (manual) camera-revoked-manual-entry-works: revoke camera, manual entry remains
- (manual) network-flaky-no-duplicates: physical device with simulated packet loss, 10 scans, server has exactly 10 rows
- (manual) battery-drain-6h: physical 6h burn, ≤30% drain on 2022+ device
- (manual) clock-skew-server-ts-wins: device clock +5min, server row uses server_ts
- (manual) already-checked-in-screen: scan same guest 2x, second shows already-checked-in
- (manual) app-update-preserves-queue: mid-event store update, pending queue intact
- (manual) cross-tenant-qr-404: forged cross-tenant QR, server 404
- (when wired) detox-or-xcuitest-run: full test plan executes via native runner