> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upside.win/llms.txt
> Use this file to discover all available pages before exploring further.

# Frontend Integration

Build the iframe bridge that receives Upside player context and controls parent-shell modals, toasts, balances, and match history.

# Frontend Integration

Your game frontend runs inside an Upside iframe. It should be a normal web app with one extra responsibility: exchange trusted `postMessage` events with the Upside parent shell.



## Responsibilities

**Do**

    - Listen for parent messages from `https://upside.win`.
    - Send the player JWT only to your own backend.
    - Let the backend decide outcomes and call Upside settlement APIs.
    - Ask the parent shell to refresh balance and match history after settlement.
  **Avoid**

    - Putting HMAC credentials in browser code.
    - Trusting messages from unknown origins.
    - Deciding final game outcomes only on the client.
    - Rendering arbitrary modal HTML from untrusted user-generated content.

## Parent Context

Upside sends the iframe a snapshot with:

| Field | Type | Meaning |
| --- | --- | --- |
| `balance` | `string` | Estimated player WIN balance in atomic units. WIN uses 18 decimals. |
| `token` | `string` | Player JWT. Forward this to your backend as `Authorization: Bearer {token}`. |
| `locale` | `string` | Active Upside locale, also appended to the iframe URL as `?locale=...`. |

Upside also sends `{ type: "localeChange", locale }` when the player changes language.

## React Bridge Hook

```tsx useUpsideBridge.tsx
import { useCallback, useEffect, useMemo, useState } from "react";

const UPSIDE_ORIGINS = new Set(["https://upside.win", "https://www.upside.win"]);

type UpsideContext = {
  balance: string;
  token: string | null;
  locale: string;
};

type ToastVariant = "success" | "error" | "warning" | "info";

export function useUpsideBridge() {
  const [parentOrigin, setParentOrigin] = useState("https://upside.win");
  const [context, setContext] = useState<UpsideContext>({
    balance: "0",
    token: null,
    locale: "en",
  });

  useEffect(() => {
    const onMessage = (event: MessageEvent) => {
      if (!UPSIDE_ORIGINS.has(event.origin)) return;

      setParentOrigin(event.origin);
      const data = event.data ?? {};

      if (data.type === "localeChange" && typeof data.locale === "string") {
        setContext(current => ({ ...current, locale: data.locale }));
        return;
      }

      setContext(current => ({
        balance: typeof data.balance === "string" ? data.balance : current.balance,
        token: typeof data.token === "string" ? data.token : current.token,
        locale: typeof data.locale === "string" ? data.locale : current.locale,
      }));
    };

    window.addEventListener("message", onMessage);

    for (const origin of UPSIDE_ORIGINS) {
      window.parent.postMessage({ type: "ready" }, origin);
    }

    return () => window.removeEventListener("message", onMessage);
  }, []);

  const postToParent = useCallback(
    (payload: Record<string, unknown>) => {
      window.parent.postMessage(payload, parentOrigin);
    },
    [parentOrigin],
  );

  return useMemo(
    () => ({
      ...context,
      showWinModal: (wins: string, htmlContent?: string) =>
        postToParent({ type: "showWinModal", wins, htmlContent }),
      showLossModal: (loss: string, htmlContent?: string) =>
        postToParent({ type: "showLossModal", loss, htmlContent }),
      showCustomModal: (htmlContent: string) =>
        postToParent({ type: "showCustomModal", htmlContent }),
      showToast: (message: string, options?: { description?: string; duration?: number; variant?: ToastVariant }) =>
        postToParent({ type: "showToast", message, ...options }),
      refetchBalance: () => postToParent({ type: "refetchBalance" }),
      refreshMatchHistory: () => postToParent({ type: "refreshMatchHistory" }),
    }),
    [context, postToParent],
  );
}
```

## Calling Your Backend

The iframe uses the player token only to call your game backend. The backend is responsible for signing Upside API requests.

```tsx PlayButton.tsx
import { useState } from "react";
import { useUpsideBridge } from "./useUpsideBridge";

const ONE_WIN = "1000000000000000000";

export function PlayButton() {
  const upside = useUpsideBridge();
  const [loading, setLoading] = useState(false);

  async function play(prediction: "heads" | "tails") {
    if (!upside.token || loading) return;
    setLoading(true);

    try {
      const response = await fetch("/api/play/coin-flip", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${upside.token}`,
        },
        body: JSON.stringify({ prediction, betAmount: ONE_WIN }),
      });

      if (!response.ok) throw new Error(await response.text());

      const result = await response.json();

      if (result.outcome === "win") {
        upside.showWinModal(result.payoutAmount);
      } else {
        upside.showLossModal(result.betAmount);
      }

      upside.refetchBalance();
      upside.refreshMatchHistory();
    } catch (error) {
      upside.showToast("Game failed", {
        description: error instanceof Error ? error.message : "Unknown error",
        variant: "error",
      });
    } finally {
      setLoading(false);
    }
  }

  return (
    <div>
      <p>WIN balance: {upside.balance}</p>
      <button disabled={!upside.token || loading} onClick={() => play("heads")}>Heads</button>
      <button disabled={!upside.token || loading} onClick={() => play("tails")}>Tails</button>
    </div>
  );
}
```

## Parent Commands

| Command | Payload | Effect |
| --- | --- | --- |
| `ready` | `{ type: "ready" }` | Tells Upside to resend `balance`, `token`, and `locale`. |
| `showWinModal` | `{ type, wins, htmlContent? }` | Opens the Upside win modal and refreshes match history. |
| `showLossModal` | `{ type, loss, htmlContent? }` | Opens the Upside loss modal and refreshes match history. |
| `showCustomModal` | `{ type, htmlContent }` | Opens a custom Upside modal. `htmlContent` must be a string. |
| `showToast` | `{ type, message, description?, duration?, variant? }` | Shows a parent-shell toast. Variant can be `success`, `error`, `warning`, or `info`. |
| `refetchBalance` | `{ type: "refetchBalance" }` | Asks Upside to reload the player's aggregate WIN balance. |
| `refreshMatchHistory` | `{ type: "refreshMatchHistory" }` | Asks Upside to reload recent sessions in the game sidebar. |

## Formatting WIN

Upside balances are atomic WIN strings with 18 decimals. Use `viem` or `ethers` for display formatting.

```ts
import { formatUnits } from "viem";

export function formatWin(amountWei: string) {
  return `${Number(formatUnits(BigInt(amountWei), 18)).toLocaleString()} WIN`;
}
```

## Accessibility Notes

- Keep focus inside your iframe during gameplay.
- Use the parent modal for settlement confirmation, but show the result inside your iframe as well.
- Do not rely only on color for win/loss states.
- Keep buttons disabled while a bet or payout is pending to avoid duplicate player actions.

## Next Steps

**[Iframe Protocol](/upside/iframe-protocol)**

Review the exact message contract from both directions.
  **[Backend Integration](/upside/backend-integration)**

Wire the signed game-actions API from your game worker.