Skip to content

The widget page

Your widget page is a web page the app shows inside its home screen: a WebView on iOS and Android, a sandboxed iframe on the web. It never signs anyone in. Everything it knows comes from the app, as messages.

flowchart TB
  classDef ours fill:#13686C,stroke:#0B4A4D,color:#ffffff
  classDef ext fill:#F3ECDF,stroke:#B9A88B,color:#12191B
  classDef msg fill:#ffffff,stroke:#D9CFBE,color:#12191B

  subgraph app["FINNS app · home screen"]
    direction TB
    card["Membership card · balance"]:::ours
    subgraph slot["Slot 'home' · height set by your page"]
      page["Your page: /w/home<br/>widgets: points, banner, …"]:::ext
    end
    tiles["Check in · Book"]:::ours
  end

  page -- "hello · ready · resize · empty · error<br/>token-expired · changed · scroll · scroll-end" --> host["App widget host"]:::ours
  host -- "init · token · context · changed" --> page

Every message is a JSON object { "v": 1, "type": …, … }, at most 16 KB. Unknown types, other versions and bad payloads are ignored, so either side can add messages without breaking the other.

Page → app (reports, never commands) Meaning
hello I’m loaded; send init
ready {height} Show me at this height (1–600 logical px)
resize {height} My content changed size
empty Nothing to show; collapse the slot
error {code} I can’t work; collapse the slot
token-expired Your API said 401; launch again now
changed {topics: ["points"]} I changed the member’s points; re-read them
scroll {dy} / scroll-end {velocity} A swipe or wheel I didn’t use; scroll the app
App → page Meaning
init {token, expiresIn, slot, widgets, context} Start. token and expiresIn are null for a guest
token {token, expiresIn} Same member, new token
context {theme?, locale?, textScale?} Presentation changed
changed {topics: ["points"]} Points changed elsewhere (check-in, booking, staff); re-read them
sequenceDiagram
  autonumber
  box rgb(227,244,243) FINNS app
    participant APP as Widget host
  end
  box rgb(246,241,233) Sota Platforms
    participant W as Your page
  end
  W->>APP: {v:1, type:"hello"}
  APP->>W: {v:1, type:"init", token, expiresIn, slot:"home", widgets, context}
  W->>APP: {v:1, type:"ready", height:146}
  Note over APP,W: nowrap:…member taps "Collect"…
  W->>APP: {v:1, type:"changed", topics:["points"]}
  Note over APP: nowrap:Re-reads balance from core
  Note over APP,W: nowrap:…gate check-in adds 10 points…
  APP->>W: {v:1, type:"changed", topics:["points"]}
  Note over W: nowrap:Re-reads balance from your backend

changed carries no values: each side re-reads from its own source, and core is the record. Send changed only after a posting of your own succeeds, never after a read or in reply to the app’s changed, so nothing ping-pongs.

// Page → app
FinnsHost.postMessage(JSON.stringify({ v: 1, type: 'ready', height: 146 }));
// App → page: the app calls this function with a JSON string literal
window.finnsWidget = {
receive(json) {
const msg = JSON.parse(json);
// handle init, token, context, changed
},
};

The app keeps the WebView on your origin: navigation elsewhere is blocked, and messages are only delivered while the page is on your origin.

Opened anywhere else (a plain browser tab), the page should say “Open this in the FINNS app”.

Token in memory only

No cookies, no localStorage, sessionStorage or IndexedDB. The token arrives by message and dies with the page.

Strict CSP

default-src 'none', scripts and connections to your own origin, Trusted Types, and frame-ancestors limited to FINNS’s app origins. No innerHTML.

Height ≥ 1

Never report a height of 0. While the app covers the slot, a hidden page measures 0; ignore that. Nothing to show? Send empty.

Handle 401 gracefully

Keep the pending action and its idempotency key, send token-expired, wait (up to ~15 s) for token, then retry with the same key.

Forward unused gestures

The app can’t see touches over your page. Report swipes and wheels you don’t use as scroll/scroll-end. Elements that use their own gestures (a scratch card) mark themselves data-gestures="own".

Guests are real users

Render a useful guest view. Protection is on your backend: a guest has no token, so member actions fail there.

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