Web apps

A web app is a page you host that opens inside the Banga app. The window.banga bridge connects it to the phone: who the user is, a signed token for your backend, a native button, and posting cards.

The manifest

{
  "contract": 1,
  "name": "Trip Planner",
  "description": "Plan a trip together.",
  "scopes": ["profile.name", "post.cards"],
  "web": {
    "url": "https://trips.example.com/banga",
    "origins": ["https://trips.example.com"],
    "launchMode": "sheet"
  }
}
FieldRules
web.urlThe page that opens, https.
web.originsUp to 10 https origins, such as https://trips.example.com, with no path. Must include the origin of web.url. The bridge exists only on pages from these origins. A link to anywhere else asks the user and opens in the browser.
web.launchModefull (the default) opens a full-screen page; sheet a tall bottom sheet; compact a short one. The page can ask to go full screen.

A web app can also have a webhook, commands and card templates. Its cards' open_app buttons open it, optionally at a path and with a payload.

The bridge

Every method returns a promise. A failed call rejects with {code, message?}.

<script>
  async function start() {
    if (!window.banga) {
      // Not inside the app, for example opened in a desktop browser: show a normal sign-in.
      return;
    }
    const context = await banga.getContext();
    const user = await banga.getUser();
    document.getElementById("hello").textContent = "Hello " + (user.name || "there");

    // Prove who the user is to your own backend.
    const token = await banga.requestAuthToken();
    await fetch("/api/session", { method: "POST", headers: { Authorization: "Bearer " + token } });

    await banga.mainButton.set({ text: "Share plan", visible: true });
    banga.mainButton.onClick(async () => {
      const { posted } = await banga.postCard({
        blocks: [{ type: "title", text: "Weekend in Lisbon" }, { type: "text", text: "Friday to Sunday" }],
        actions: [{ type: "open_app", label: "Open plan", path: "/plans/42" }]
      });
      if (posted) await banga.haptic("success");
    });
  }
  window.addEventListener("load", start);
</script>
MethodResult
banga.version"1", the bridge contract.
getContext(){appId, launch: {source, payload?}, viewport, theme: {scheme, accent, surface}, locale, roomType?}. launch.payload is the payload of the open_app button that opened the page. roomType is present when it was opened from a room; the room itself is never revealed.
getUser(){id, name?, language?}. id is the same per-app id your webhook receives. The rest follows the scopes. Use it for display; trust only a verified token.
requestAuthToken()A session token for your backend. See below.
postCard(card){posted}. Checks the card, shows the user a preview, lets them pick a room when the page was not opened from one, and posts it once they confirm. posted is false when they decline. Needs the post.cards scope.
mainButton.set({text, enabled?, progress?, visible?})Shows a native button at the bottom. mainButton.onClick(fn) runs fn when it is tapped.
viewport.expand()Turns a sheet into a full-screen page. viewport.onChange(fn) runs fn with the new mode.
openLink(url)Asks the user, then opens an https link in the browser.
close()Closes the page.
haptic(kind)A vibration: light, medium, success or error.
on("back", fn)Runs fn when the user presses back and your page goes back in its own history.
requestPayment({amount, currency, reference, description})Asks the user to pay your app (scope payments): a native sheet the page cannot draw or change, then a signed payment.completed event to your webhook. Resolves with {status: "completed" | "declined" | "expired", paymentId}; rejects with scope_denied, not_enabled or rate_limited (5 asks per launch). Amounts are strings; the reference is unique per user. See Payments. Arrives with the next app release; until then the method rejects with unsupported.

Error codes: unsupported (no such method), scope_denied, rate_limited, invalid_card, invalid_url, invalid_kind, token_failed, origin_denied and failed. Each time the page is opened, it may ask for a token up to 10 times (getUser() may use one) and post up to 20 cards.

On the web version of Banga, apps open in a new browser tab without the bridge. Check for window.banga and fall back to your own sign-in when it is missing.

Session tokens

requestAuthToken() returns a JSON Web Token, valid for 5 minutes and signed by the platform with Ed25519. Send it to your backend and verify it there before trusting who the user is.

ClaimValue
issbanga-apps
audYour app's id.
subThe user's per-app id.
scopeThe scopes the user granted, separated by spaces.
iat, expIssued and expiry times, in seconds.
jtiA unique id for the token.

The header is {"alg": "EdDSA", "typ": "JWT", "kid": ...}. The public keys are a standard JSON Web Key Set at https://dev.banga.chat/api/apps/signing-keys, which you may cache for a day. Any JOSE library verifies the token. In Node.js:

// npm install jose
import { createRemoteJWKSet, jwtVerify } from "jose";

const keys = createRemoteJWKSet(new URL("https://dev.banga.chat/api/apps/signing-keys"));

export async function verifySession(token) {
  const { payload } = await jwtVerify(token, keys, {
    issuer: "banga-apps",
    audience: process.env.BANGA_APP_ID,
    algorithms: ["EdDSA"]
  });
  return { userId: payload.sub, scopes: payload.scope ? payload.scope.split(" ") : [] };
}