Skip to content

Guest app

The guest app is the primary activation channel. It is a single home screen: sign in, see the membership card and points balance, open a check-in pass for the gate, jump to booking, and play with the engagement partner’s widgets. Rewards, raffles and games are the partner’s widgets, not app screens, so they can change without an app release.

Repository: app.

Framework
Flutter 3.47Dart 3.13
Targets
iOSAndroidWeb (Cloudflare Workers, static)
State & navigation
ChangeNotifierListenableBuilderNavigator 1.0No state-management or router package; dependencies wired by hand in main_prod.dart.
Sign-in
oidc 5oidc_default_storePKCEHardened secure storage on mobile (Keychain / Keystore).
Networking
http
Partner widgets
webview_flutter (mobile)sandboxed iframe (web)
Other
pretty_qr_codeurl_launcherpackage_info_plus
Look
ManropeInstrument Seriflight theme: aqua on ink, on sand
Build
flutter_flavorizr (prod, test)GitHub Actions → Cloudflare (web)
flowchart TB
  classDef ours fill:#13686C,stroke:#0B4A4D,color:#ffffff
  classDef area fill:#E3F4F3,stroke:#13686C,color:#12191B
  classDef ext fill:#F3ECDF,stroke:#B9A88B,color:#12191B

  home["<b>HomeScreen</b><br/>membership card · Check in · Book · widget slot"]:::ours

  subgraph features["Feature modules (lib/)"]
    direction LR
    auth["auth/<br/>CustomerAuth<br/>OIDC session"]:::area
    corec["core/<br/>CoreClient<br/>401 → refresh → retry once"]:::area
    points["points/<br/>PointsBalanceController<br/>polls every 3 s"]:::area
    checkin["check_in/<br/>QrScreen<br/>rotating one-time QR"]:::area
    book["booking/<br/>BookingLink<br/>handoff code → browser"]:::area
    widgets["partner_widgets/<br/>launcher · slot · protocol v1"]:::area
  end

  home --> auth & points & checkin & book & widgets
  points & checkin & book & widgets --> corec
  auth --> kc["Keycloak"]:::ext
  corec --> core["Core"]:::ours
  widgets --> sota["Sota Platforms<br/>launch API + widget page"]:::ext
  book --> site["Booking website"]:::ext
Module Calls What it does
auth/ Keycloak Sign-in with PKCE, token refresh, offline session on mobile, sign-out
core/ Core /api/v1/app/* Adds the access token; on 401 refreshes once and retries
points/ GET /points Polls the balance every 3 s in the foreground; animates a “+10” when it changes
check_in/ POST /qrs Shows a QR that rotates before its 60 s lifetime ends
booking/ POST /handoffs Gets a one-time code and opens the booking site in the browser
partner_widgets/ POST /widget-launches, partner’s /api/v1/launch Launches, renews and hosts the partner’s widgets
Guest (signed out) Member (signed in)
Welcome card with Sign in Greeting and membership card: name, email, points balance
Book tile (plain booking site) Check in and Book tiles
Partner widgets, guest mode (“sign in to start earning”) Partner widgets, signed in

The layout switches to two columns from 880 px, so the same build works on a phone and a desktop browser.

The widget host is the most involved part of the app. It is described for partners in the Customer engagement section; internally, the key behaviours are:

stateDiagram-v2
  direction LR
  [*] --> Launching: identity known
  Launching --> Loading: launch OK
  Launching --> Collapsed: launch failed
  Loading --> Live: page ready
  Loading --> Collapsed: not ready in 5 s
  Live --> Renewing: expiry near or token-expired
  Renewing --> Live: new token sent
  Renewing --> Collapsed: expired
  Live --> Paused: covered or backgrounded
  Paused --> Live: back on top
  Collapsed --> Launching: retry with back-off
  Live --> [*]: identity changes
  • Back-off after failures: 5 s, 15 s, 60 s, then every 5 minutes.
  • One launcher per identity (guest or customer:<sub>). Signing in or out discards the page and clears the web view’s storage.
  • The token travels as a message, never in a URL: the page says hello, the app answers init {token, …}.
  • Renewal happens at expiresIn − min(30 s, lifetime/4), only while the slot is visible and the app is in the foreground.
  • Points stay consistent without sharing data: after a widget posts points it sends changed, and the app re-reads its own balance from core (and vice versa).
  • The slot survives the check-in screen being pushed on top; a covered page reporting height 0 is ignored rather than collapsing the slot.
Mobile (iOS, Android) Web
Browser Ephemeral system browser session (e.g. ASWebAuthenticationSession on iOS) Redirect back through redirect.html
Prompt prompt=login: password each sign-in Keycloak SSO session (30 min idle, 10 h max)
Staying signed in Offline refresh token (~30 days idle), works offline at start-up Keycloak session
Token storage Keychain / Keystore (hardened) Browser storage
Sign-out Core /logout, then revoke refresh and access tokens Core /logout, then end the Keycloak session

Environments are chosen at build time with --dart-define: FINNS_CORE_URL, FINNS_AUTH_ISSUER, FINNS_BOOKING_URL, FINNS_WIDGET_URL. The prod and test flavors only change the app’s name and bundle id, so both can be installed side by side.

  • The app never sends its own token to a partner; it hands over one-time codes from core.
  • The widget web view is locked to the partner’s origin, and messages are exchanged only with that exact origin.
  • Android backups are off (restored encrypted tokens couldn’t be decrypted anyway). Cleartext HTTP is allowed only in debug builds.

Internal and confidential. Share partner sections only as partner builds (pnpm build:partner).