Mobile Application
At a glance
A single React Native + Expo codebase delivers polished native apps for iOS and Android plus a fast PWA, tailored to four distinct roles — players, officials, club admins and spectators — with offline-first storage for licenses, matches and rankings, unified push notifications, a camera-based QR scanner for license verification and check-in, GPS-driven nearby venue discovery and full dark mode and accessibility settings baked in from day one.
How it works
The mobile application is built once in React Native + Expo and shipped to iOS, Android and the web from the same TypeScript source tree. A device-class hook (useDeviceClass) flips the layout between a mobile bottom-tab shell and a desktop sidebar shell at a 640 px cutoff, so the same screens render natively on a phone and adaptively on a tablet or browser. EAS OTA push updates ship JavaScript-only fixes within minutes, while native binaries follow a slower App Store / Play Store cadence.
Four role-based experiences live inside the same shell. Players get profile, competitions, results and rankings; officials get match management and score entry; club admins get member management and broadcast tools; spectators get live scores, schedules and venue info. Role detection is server-driven from the active tenant profile, so a single download adapts to whatever the user is allowed to do.
Offline-first is foundational: license cards, last viewed matches and current rankings are cached in encrypted local storage. Score entry written offline is queued and replayed when the device reconnects, with conflict resolution handled by the API. Push notifications use APNs, FCM and Web Push behind a unified subscription endpoint. The QR scanner uses Expo Camera for license verification and check-in, validating signed JWT bundles even with no network. A GPS-driven nearby finder lists clubs, venues and competitions sorted by distance, deep-linking into the relevant detail screens. Dark mode and accessibility settings (font scaling, high contrast, reduce motion, screen reader hints) are applied through a tenant-branded ThemeProvider that respects both OS-level preferences and in-app overrides.
Key capabilities
- Cross-platform from one codebase (iOS, Android, Web PWA) via Expo + EAS OTA updates
- Role-aware shell for player, official, club admin and spectator with adaptive mobile/desktop layout
- Offline cache for license, recent matches and rankings with queued score-entry sync
- Unified push notifications across APNs, FCM and Web Push
- Camera-based QR scanner for license verification and event check-in
- GPS-based nearby venue and competition finder with deep links
- Dark mode plus full accessibility settings respecting OS preferences
In practice
A new player downloads the app from the App Store, signs in once and lands on a hero home screen sized for their phone. They tap the license tab to see a fullscreen license card that loads instantly from cache, then scan a friend's QR badge at the club to verify membership — both work without a signal. On the way to a tournament they open the venue finder, get walking directions to the nearest courts and check in by scanning the entry QR.
During play, the official-mode user enters scores; if the boulodrome Wi-Fi drops, entries queue locally and sync the moment they reconnect. Back home, the same player opens the web app on a laptop and finds an identical experience reflowed into a desktop sidebar layout — same data, same login, no install needed.
Features in this subsystem
12| ID | Status | Features |
|---|---|---|
| F15.01.01 | Shipped | Cross-platform app (iOS, Android, Web) (React Native/Expo) ✅ PL-F1501a |
| F15.01.02 | Shipped | Player mobile app (profile, competitions, results, rankings) ✅ PL-F1501a |
| F15.01.03 | Shipped | Official mobile app (match management, score entry) ✅ PL-F1501a |
| F15.01.04 | Shipped | Club admin mobile app (member management, communication) ✅ PL-F1501a |
| F15.01.05 | Shipped | Spectator mobile app (live scores, schedule, venue info) ✅ PL-F1501a |
| F15.01.06 | Shipped | Offline mode (score entry without connectivity, sync later) ✅ PL-F1501b |
| F15.01.07 | Shipped | Push notifications ✅ PL-F1501b · Go-ombyggnad: transporten LEVERERAD i 4776ad23.m-1 (D-PUSH) — POST /v1/push/devices + POST /v1/push/dispatch (202-jobbväg för bred fan-out) mot APNs/FCM v1. app-sidan LEVERERAD i 4776ad23.m-3 (task 4776ad23.m-3.t-2): kontextuell behörighets-onboarding efter genomförd anmälan (aldrig vid kallstart), registrering med råa APNs-/FCM-token (getDevicePushTokenAsync), token-rotation och avregistrering vid logout före sessionsrivningen, skärmen Notiser & enheter (/notiser — maskerad token, avregistrering med bekräftelse) med permanent ingång i Inställningar, foreground-/kallstartsmottagning, fast kategorikatalog (packages/shared/src/push/categories.ts, inkl. Godkänn/Avslå), säker deep-link-navigering (endast interna routes, fallback Hem) och badge-nollställning vid appöppning — vy-spec specs/app/views/notiser-enheter.md, bevisad av L4-drivrutinen tools/gate/dpush-m3-app-e2e.mjs mot körande stack + riktig Postgres. Ärlig gräns (A-3/A-5): riktig OS-dialog, bakgrundsrendering och åtgärdsknappar på fysisk enhet verifieras först i molnet med skarpa APNs-/FCM-nycklar, och notis-preferenser/tysta tider/sammanfattningar ägs av D-COMM (ej byggda — vyn säger det rakt ut, ingen död knapp) |
| F15.01.08 | Shipped | QR code scanner (license verification, check-in) ✅ PL-F1501b · Go-ombyggnad: LEVERERAD i 3e1559c7.m-3 (D-COURTSIDE) — appens /pa-banan?verktyg=qr KOMPONERAR den befintliga anonyma licensverifieringen publicVerifyLicense (fyra utfall, tre kort) och tävlingsdisken /offline-incheckning (bakom registrations:write) med husets ENDA skanneradapter (QrCaptureView) och grammatik (parseScannedCheckInPayload); klassificeringen är den rena classifyScannedPayload i packages/shared/src/courtside. Ingen POST /mobile/qr/verify, ingen ny endpoint; spelarens kortlivade incheckningstoken känns igen men konsumeras inte (⊘ med ägare D-USERS/D-EVENTS). Domänregel: docs/domain/courtside/README.md §QR-kompositionen, BR33.09 |
| F15.01.09 | Shipped | GPS-based nearby venue/competition finder — NY HEMVIST: D-DISC GET /v1/discover/nearby (anonym cross-tenant radie-sök, PostGIS ST_DWithin mot GiST-indexet; radie default 10 km och tak 39 km härlett ur BR26.03:s areabudget, cap 100 med truncated, ordning på distance_km med en decimal, CDN-cache 300 s). Varje rad bär sina nästa tre drop-in-pass ur m-2:s enda expansion i platsens egen zon, och competitions-halvan är ett objekt med ett mätt källtillstånd (measured unavailable) ur D-COMP:s egen publika upptäcktssöm — aldrig en tom lista som ser ut att betyda »inga tävlingar«. Kontrakt: specs/api/endpoints/discover-nearby.md | ✅ PL-F1501b (legacy) · ✅ D-DISC 9e7fb823.m-3 |
| F15.01.10 | Shipped | Dark mode and accessibility settings ✅ PL-F1501b |
| F15.01.11 | Planned | Uppdatering över luft (OTA) i butiksappen — D-MOBILDIST c784b7d5.m-1 (skalet): expo-updates installerat och wirat i app; runtimeVersion följer fingerprint-policyn och adressen bär kanal/plattform/butiksversion (<PL_UPDATES_BASE_URL>/<kanal>/<ios\ android>/<version>/manifest.json, husets egen domän updates.test.petanque.life/updates.petanque.life), härledd per byggprofil i app/app.config.ts av den rena regeln app/updates/resolve-updates-config.ts (en native-profil med saknat värde stoppar bygget med variabelns namn); signerat från dag ett (publikt certifikat per miljö i app/updates/certs/, keyid pl-updates-<env>-v1, privata nycklar i driftvalvet); kontrollen ligger aldrig i startvägen (checkAutomatically: NEVER, fallbackToCacheTimeout: 0) — app/lib/updates/runtime.tsx hämtar i bakgrunden 15 s efter sessionsproben och vid återkomst till förgrunden (högst var 6:e timme), aldrig under ett arbetspass (scoring, runda, incheckning, studio, betalning, eller medan offline-kön synkar), och den hämtade bundlen börjar gälla först vid nästa kallstart (appen startar aldrig om sig själv). Expo Web-vägen är orörd. D-MOBILDIST c784b7d5.m-2 (publiceringsvägen): tools/mobile-updates/pl-updates.mjs publicerar signerade bundles som statiska filer på statichost.eu (pl-updates-test, updates.test.petanque.life) med expo-signature i en genererad _headers, vägrar med skalets egen bindningsregel (fingerprint, kanal, plattform), binder varje butiksversion till en fingerprint och en nyckel, återgår i en drop (mätt under 60 s), gallrar med --keep och roterar/retirerar nycklar (keyid ur certets CN); driftrutinen, ägarcheckistan och bevisprotokollet står i docs/engineering/runbooks/mobil-uppdateringar.md. D-MOBILDIST c784b7d5.m-3 (intern distribution, steg A): ägarens verktyg tools/mobile-dist/pl-dist.mjs (preflight → build → record-build → submit, status) bygger beta-profilen med EAS, bokför varje färdigt bygge i liggaren med EAS-byggets eget runtimeVersion innan det får lämnas in (första fingerprint-bindningen är inte längre tyst), lämnar in Android före iOS till Play internal testing och TestFlight (interna testare) och vägrar production; statusvyn /uppdateringar visar byggets identitet (app-uppdateringar-bygge); runbookens §4/§4a/§6 beskriver proceduren, testarkohorten och B-7/B-8. D-MOBILDIST c784b7d5.m-4 (steg B, butiksfärdig — inte publicerad): släppregistret app/store/ (listningar i de fem lanseringsspråken, integritetsdeklarationen härledd ur koden och prövad i båda riktningarna, åldersgränsens frågesvar, granskningsanteckningar, skärmbildsplan, release.json) renderas av pl-dist.mjs store render till privacy manifest/locales/android.permissions i app.json, EAS Metadata och Play-checklistorna; www bär app-policyn och sidan för kontoradering; production kräver --confirm-production och en öppnad släppgrind i liggaren (release-gate open), Android lämnas in först med stegvis utrullning. Kvar: Partial tills app/store/release.json store_urls bär butiksadresser — konsolformulären, granskningarna och publiceringen är ägarens (runbook §4 *Steg B*). Bevisets gräns: konton, ett riktigt EAS-bygge, installation via spåren, den riktiga värdens _headers, signaturverifiering på enhet och aktivering vid kallstart bevisas manuellt enligt bevisprotokollet (m-2/m-3) — ännu ej kört. D-UPPDATERANU 18248906.m-1 (»En ny version finns«): när en bundle är hämtad visar appen notisen med »Starta om nu« (aktiverar genom reloadAsync bakom den delade regeln activationDecision, aldrig under ett arbetspass) och »Senare« (döljer notisen för körningen; bundlen tas ändå in vid nästa kallstart); samma knapp i /uppdateringar — vy-spec specs/app/views/uppdateringsnotis.md. | 🟡 D-MOBILDIST c784b7d5.m-1 + c784b7d5.m-2 + c784b7d5.m-3 + c784b7d5.m-4 |
| F15.01.12 | Shipped | Arrangörsappen — den ANDRA butiksappen (ägarbeslut 2026-09-18: *»många har inte dator utan bara telefon«*), arrangor/ (*Petanque Life Arrangör*, life.petanque.arrangor). Byggd som KOMPOSITION av packages/shared, aldrig som kopia av app/: native bearer-session i expo-secure-store (Expo Web cookie i bevisläge), OTP-/provider-/dev-login-inloggning, tenantval, det delade adaptiva skalet med nav ur admins EGEN resolveAdminNav(caps), capability-grind fail-closed med neutral nekande-yta, hem med gruppkort och klientfel (surface: 'arrangor') från första bygget. D-MOBILDIST ärvd med egna cert (pl-arrangor-updates-<env>-v1), egen manifestadress och egen pl-dist-liggare. Vyerna flyttas i vågor ur admin (fältytorna först). Vy-spec: specs/arrangor/views/skal-och-inloggning.md. D-UPPDATERANU 18248906.m-1: samma notis *»En ny version finns«* med »Starta om nu«/»Senare« som spelarappen, aldrig på arbetspass-rutterna (ARRANGOR_WORK_PASS_ROUTES); reloadAsync bor i exakt arrangor/lib/updates/runtime.ts. ✅ D-ADMINAPP 94f74ebf.m-1.t-2 · D-UPPDATERANU 18248906.m-1 |