# Embed the PlayerPal widget

The PlayerPal widget is a small capsule that sits on a game provider's page. It tells the player whether PlayerPal is connected ("Earning points") or not ("Connect to earn"), and, where the provider runs the Link, lets the player connect without leaving the game. It never shows Points balances, ledger details or other partners' data; those stay on PlayerPal's own pages. If the widget fails to load, the game plays normally.

[In development]

:::info[In development]
The widget runs today in PlayerPal's reference provider integration. The release format and the loading rules below are built; the launch-context contract may still change. Contact PlayerPal before building against it.
:::

## How it works

```mermaid
sequenceDiagram
  participant O as Operator backend
  participant PB as Provider backend
  participant G as Provider game page
  participant W as PlayerPal widget
  participant PP as PlayerPal
  O->>PB: Create a provider launch session (no PlayerPal call)
  O-->>G: Single-use bootstrap in a POST body
  G->>PB: Consume bootstrap
  PB-->>G: Verified session context
  G->>W: playerpal:session-context event
  W->>PB: Link status, via your own origin
  W-->>G: playerpal:alias-ready (provider alias)
```

The operator stays in charge of the wallet, the provider owns the game and its rounds, and PlayerPal receives only [activity](./sending-activity.mdx) after settlement. Launch tokens, signed assertions and aliases never appear in the game URL.

## Read the release manifest

Each widget release is published at a fixed version path and never changes. Its manifest names the exact file and its integrity digest.

- The file name carries a fingerprint of its contents, and `integrity` is a SHA-384 digest.
- PlayerPal verifies the digest again before serving a release.
- Release files are served with a one-year immutable cache, `Access-Control-Allow-Origin: *`, `Cross-Origin-Resource-Policy: cross-origin` and `X-Content-Type-Options: nosniff`.

Request:

```
GET /assets/playerpal-widget/0.1.0/manifest.json
```

manifest.json:

```
{
  "name": "@playerpal/widget",
  "version": "0.1.0",
  "file": "playerpal-widget.<fingerprint>.iife.js",
  "url": "https://<playerpal-asset-host>/assets/playerpal-widget/0.1.0/playerpal-widget.<fingerprint>.iife.js",
  "integrity": "sha384-<base64-digest>"
}
```

## Pin the script with SRI

Copy `url` into `src` and `integrity` into `integrity`. The browser refuses the script if a single byte differs.

- `crossorigin="anonymous"` is required for the integrity check on a cross-origin script.
- Load it `async`: the game never waits for the widget.
- Load the release from PlayerPal. Do not copy, rebuild or bundle the widget into your own build.
- To upgrade, read the new version's manifest and update both `src` and `integrity`.

The script loads with no integrity error in the console. Change one character of `integrity` and the browser blocks it while the game still plays.

Your game page:

```
<script
  src="https://<playerpal-asset-host>/assets/playerpal-widget/0.1.0/playerpal-widget.<fingerprint>.iife.js"
  integrity="sha384-<base64-digest>"
  crossorigin="anonymous"
  async></script>
```

## Place and configure the element

Add one `<playerpal-widget>` element to the page.

| Attribute | Required | Meaning |
|---|---|---|
| `game-id` | Yes | The game this page runs. Session context for another game is ignored |
| `allowed-integration` | Yes | Your integration ID. Context for any other integration is ignored |
| `portal-origin` | Yes | The PlayerPal player-portal origin you were given. Link pages from any other origin are refused |
| `hidden` | No | Hides the widget while present |

| Method on the element | Use |
|---|---|
| `handleGameplayStarted()` | Call when a round starts. The widget collapses, then tucks itself to the edge of the screen |
| `revealManual()` | Call from your own menu or info button to bring the hidden widget back |

Only one widget is active per page. It renders in a shadow root, respects reduced-motion settings, and supports keyboard focus and Escape to close its panels.

Element:

```
<playerpal-widget
  game-id="your-game-id"
  allowed-integration="pp_int_XXXX"
  portal-origin="https://<playerpal-player-portal>"></playerpal-widget>
```

Gameplay hooks:

```
const widget = document.querySelector('playerpal-widget')

// When a round starts
widget?.handleGameplayStarted?.()

// From your own menu or info button
widget?.revealManual?.()
```

## Hand over the launch session

Once your backend has consumed the launch bootstrap, your page dispatches the verified context as a same-document event. If the widget loads later, it asks for the context again with `playerpal:request-session-context`, so answer that event too.

The context names the launch, the game, your integration, the linking mode, your own origin as the Link broker, the player-portal origin and your play-session token. The widget accepts it only if every value matches the attributes above and the page's own origin. The play-session token is sent only to your backend, never to PlayerPal. Parent-frame `postMessage` is not used for launch context.

When the player is connected, the widget dispatches `playerpal:alias-ready` with the provider alias. Your page can dispatch the same event to hand the widget an alias it already holds.

| Linking mode | What the widget does |
|---|---|
| `pre-linked` | The provider alias already exists; the widget shows "Earning points" |
| `operator-led` | The operator linked the player before launch and passed only the provider alias. Without one, the game continues without PlayerPal |
| `provider-led` | "Connect to earn" opens the PlayerPal link page in a popup. Your backend creates and exchanges the provider-led Link with your integration credentials |

In provider-led mode the widget calls only your own origin. Your backend then calls `POST /v1/link-intents` and `POST /v1/link-results/exchange` as described in [Link players](./linking-players.mdx). The popup times out after five minutes.

After you dispatch the context, the capsule leaves its loading state and shows "Earning points" or "Connect to earn".

Publish context and receive the alias:

```
const publish = () =>
  window.dispatchEvent(
    new CustomEvent('playerpal:session-context', { detail: context }),
  )

publish()
window.addEventListener('playerpal:request-session-context', publish)

window.addEventListener('playerpal:alias-ready', (event) => {
  const { launch_id, integration_id, player_id } = event.detail
  // player_id is the provider alias, pp_plr_prv_…
})
```

Session context (detail):

```
{
  "type": "operator-game:launch-context",
  "version": 1,
  "launch_id": "<launch-id>",
  "demo_session_id": "<session-id>",
  "game_id": "your-game-id",
  "integration_id": "pp_int_XXXX",
  "linking_mode": "provider-led",
  "api_base": "<your page origin>",
  "player_portal_origin": "https://<playerpal-player-portal>",
  "session_token": "ps_XXXX"
}
```

:::note
`demo_session_id` carries your game session's ID. The launch contract is in development, so field names may change.
:::

## Set your Content Security Policy

| Directive | Allow |
|---|---|
| `script-src` | The PlayerPal asset origin. Integrity checking also needs `crossorigin="anonymous"` on the tag |
| `style-src` | `'unsafe-inline'` today, because the widget injects its styles into its shadow root |
| `connect-src` | Your own origin (`'self'`). The widget never calls PlayerPal directly |
| Popups | Do not sandbox away popups. If you send `Cross-Origin-Opener-Policy`, use `same-origin-allow-popups` so the link page can message back |

The widget keeps a short-lived flag and the provider alias in `sessionStorage` for the current tab; storage being unavailable does not break it.

The browser console shows no CSP violations while the widget loads, checks link status and, in provider-led mode, opens and closes the link popup.

Response headers for your game page:

```
Content-Security-Policy: script-src 'self' https://<playerpal-asset-host>; style-src 'self' 'unsafe-inline'; connect-src 'self'
Cross-Origin-Opener-Policy: same-origin-allow-popups
```

## Related

  - [Link players](./linking-players.mdx): 
    The link intent and exchange your backend runs.
  
  - [Player experience](../product/player-experience.mdx): 
    What players see across PlayerPal.
  
  - [Send activity](./sending-activity.mdx): 
    Report settled rounds after play.
