# Gale Bridge Widget Integration Guide

Applies to `@galebridge/widget@0.1.0`. Updated October 15, 2026.

Embed the Gale Bridge cross-chain interface in your website. Your app supplies a container and the user's wallet Provider. The Widget handles asset selection, quotes, approval prompts, transaction submission and order tracking. npm and CDN expose the same SDK API; choose one loading method.

| Resource | URL |
| --- | --- |
| npm package | [@galebridge/widget](https://www.npmjs.com/package/@galebridge/widget) |
| CDN SDK | `https://galebridge.net/widget/v1/widget.js` |
| Hosted Widget | [https://galebridge.net/widget](https://galebridge.net/widget) |
| Available networks and tokens | [Integration Networks API](https://galebridge.net/api/v1/integration/networks) |

## Before you start

- Serve production pages over HTTPS. Use `http://localhost` or `http://127.0.0.1` for local development; do not open the HTML with `file://`.
- Create the DOM container before mounting. Use `width: 100%` and a maximum width around `500px`. The SDK adjusts the iframe height to its content.
- Use a modern browser with BigInt and `crypto.randomUUID()`. In React / Next.js, create the Widget after the client component mounts.
- Embedded transactions use wallet adapters supplied by the host. EVM uses an EIP-1193 Provider; TRON uses TronLink or a compatible Provider. Creating the Widget does not request a wallet connection or signature.
- Replace `your-project-web` with a stable project identifier. Use distinct identifiers for independent Widgets on the same host origin to separate pending transaction recovery. `clientId` is public and does not establish authentication or commission attribution.

Without a wallet adapter, the Widget still loads and offers a link to its standalone page. Transacting inside the iframe requires an adapter for the selected ecosystem.

## Option 1 npm integration

Install a fixed version:

```bash
npm install @galebridge/widget@0.1.0
```

Add a container and a status message to your page:

```html
<div id="bridge" style="width:100%;max-width:500px;margin:auto"></div>
<p id="bridge-status" role="status" aria-live="polite"></p>
```

Add the following to a client entry point built with Vite, Webpack or a similar tool:

```ts
import { createWidget, createEvmAdapter, type EVMProvider } from '@galebridge/widget'

const provider = (window as Window & { ethereum?: EVMProvider }).ethereum
const showStatus = (text: string) => {
  const element = document.querySelector('#bridge-status')
  if (element) element.textContent = text
}

const widget = createWidget({
  container: '#bridge',
  clientId: 'your-project-web',
  theme: 'light',
  lang: 'en',
  wallets: provider ? { evm: createEvmAdapter(provider) } : {},
})

widget.on('error', ({ code, message }) => {
  showStatus(message)
  console.error('[Gale Widget]', code, message)
})
widget.on('transactionSent', ({ txHash }) => {
  showStatus('Source transaction submitted; waiting for completion: ' + txHash)
})
widget.on('completed', ({ txHash, order }) => {
  showStatus('Bridge order completed: ' + txHash)
  console.log('Order details', order)
})
void widget.ready.catch((error: Error) => showStatus(error.message))

// Call when the SPA page or component unmounts:
// widget.destroy()
```

This example uses the browser's injected EVM wallet. If your app already uses a wallet connector, pass its selected EIP-1193 Provider to `createEvmAdapter(provider)` so the host and Widget use the same wallet. Compile this TypeScript with your build tool; do not load it directly as a plain browser script.

### React and Next.js example

The host's wallet connection module supplies `provider`. Keep its reference stable. Without it, users can open the standalone page. Keep `'use client'` at the top when using the Next.js App Router.

```tsx
'use client'

import { useEffect, useRef, useState } from 'react'
import { createWidget, createEvmAdapter, type EVMProvider } from '@galebridge/widget'

export function GaleBridge({ provider }: { provider?: EVMProvider }) {
  const container = useRef<HTMLDivElement>(null)
  const [status, setStatus] = useState('')

  useEffect(() => {
    if (!container.current) return
    setStatus('')
    const widget = createWidget({
      container: container.current,
      clientId: 'your-project-web',
      lang: 'en',
      theme: 'light',
      wallets: provider ? { evm: createEvmAdapter(provider) } : {},
    })
    widget.on('error', ({ message }) => setStatus(message))
    widget.on('transactionSent', ({ txHash }) => setStatus('Transaction submitted: ' + txHash))
    widget.on('completed', ({ txHash }) => setStatus('Bridge order completed: ' + txHash))
    void widget.ready.catch((error: Error) => setStatus(error.message))

    return () => widget.destroy()
  }, [provider])

  return (
    <section>
      <div ref={container} style={{ width: '100%', maxWidth: 500 }} />
      <p role="status" aria-live="polite">{status}</p>
    </section>
  )
}
```

Changing `provider` recreates the Widget in this component. Avoid switching Providers or unmounting while a transaction is signing or being tracked. Unmounting does not cancel a submitted on-chain transaction.

## Option 2 CDN integration

No npm installation is required. Save the following as `cdn.html` and serve it over HTTP(S). The SDK exposes its API as `GaleBridge`. Load the SDK before calling `createWidget`.

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Gale Bridge integration example</title>
  </head>
  <body>
    <main style="max-width:500px;margin:24px auto;padding:0 12px">
      <div id="bridge"></div>
      <p id="bridge-status" role="status" aria-live="polite"></p>
    </main>
    <script src="https://galebridge.net/widget/v1/widget.js"></script>
    <script>
      const status = document.getElementById('bridge-status')
      if (!window.GaleBridge) {
        status.textContent = 'SDK failed to load. Check the network and CSP settings.'
      } else {
        const provider = window.ethereum
        const widget = GaleBridge.createWidget({
          container: '#bridge',
          clientId: 'your-project-web',
          lang: 'en',
          theme: 'light',
          wallets: provider ? { evm: GaleBridge.createEvmAdapter(provider) } : {},
        })
        widget.on('error', ({ code, message }) => {
          status.textContent = message
          console.error('[Gale Widget]', code, message)
        })
        widget.on('transactionSent', ({ txHash }) => {
          status.textContent = 'Source transaction submitted; waiting for completion: ' + txHash
        })
        widget.on('completed', ({ txHash, order }) => {
          status.textContent = 'Bridge order completed: ' + txHash
          console.log('Order details', order)
        })
        widget.ready.catch((error) => { status.textContent = error.message })
        // Call widget.destroy() when leaving this SPA page.
      }
    </script>
  </body>
</html>
```

`/widget/v1/widget.js` is a maintained production URL, not an immutable npm version URL. To pin the SDK, install `0.1.0` through npm. Pinning the SDK does not freeze the hosted page, available assets or backend routes.

### Set the Widget language

The hosted Widget defaults to English. For Traditional Chinese, use the existing `zh` value when creating the Widget or update it while idle:

```ts
widget.update({ lang: 'zh' }) // Traditional Chinese
// widget.update({ lang: 'en' }) // English
```

Users can also switch language in the Widget header, including while following a pending transfer. This changes presentation only and does not send or cancel a transaction.

## Wallet integration

### EVM wallets

```ts
import { createEvmAdapter } from '@galebridge/widget'

// provider is the EIP-1193 Provider selected by the host app.
widget.setWallet('evm', createEvmAdapter(provider))
```

After `setWallet` replaces an adapter, users reconnect using the Widget's connect button. The EVM adapter can switch to networks already configured in the wallet; it does not add unknown networks automatically. It checks the active account and chain again before submitting. Approvals use the exact transfer amount. Tokens that require resetting an existing allowance to zero need an appropriate custom adapter.

### TRON wallets

After TronLink or a compatible TRON Provider is injected into the host, keep the getter so the adapter can read `tronWeb` after connection:

```js
// npm: import createTronAdapter from @galebridge/widget.
// CDN: use const { createTronAdapter } = GaleBridge.
const provider = window.tronLink ?? window.tron ?? window.okxwallet?.tronLink
if (provider?.request) {
  widget.setWallet('tron', createTronAdapter({
    request: provider.request.bind(provider),
    get tronWeb() { return provider.tronWeb ?? window.tronWeb },
  }))
}
```

The built-in TRON adapter requires mainnet. For legacy Providers without network queries, it accepts only a node origin of `https://api.trongrid.io`. Other legacy custom RPCs require a custom adapter that verifies the network. Approval and bridge calls use fee limits of 100 TRX and 150 TRX respectively. These are caps; check actual costs in the wallet confirmation.

CDN wallet functions match the npm API, including `GaleBridge.createEvmAdapter` and `GaleBridge.createTronAdapter`. Version 0.1.0 does not include WalletConnect or mobile wallet deep links. Validate the wallet versions and mobile wallet browsers you intend to support in your integration environment.

## Configuration

All options except `container` are optional. Supply chain IDs, amounts and addresses as strings. Use the token `address` returned by the Networks API, not an internal database ID.

| Option | Description |
| --- | --- |
| `container` | Required CSS selector or HTMLElement; the container must exist before mounting |
| `widgetUrl` | Defaults to `https://galebridge.net/widget`; use a trusted hosted URL |
| `clientId` | Stable project / Widget identifier; omitted values share the default recovery group |
| `wallets` | `{ evm, tron }` adapters; can be supplied later with `setWallet` |
| `theme` | `light` or `dark`; defaults to light |
| `lang` | `en` (English) or `zh` (Traditional Chinese); defaults to English. The Widget also has a language selector; documentation language is selected separately |
| `accentColor` | Six-digit hex color, such as `#245fef` |
| `fromChainId`, `toChainId` | Decimal source and destination chain IDs, such as `"1"`, `"137"` |
| `fromTokenAddress`, `toTokenAddress` | Token addresses on the corresponding networks; use the API's placeholder for native assets |
| `amount` | Prefilled source amount in human-readable token units, such as `"10.5"`; do not pass an empty string or an approximate floating-point result |
| `receiver` | Prefilled destination address; users may enter it themselves. Connecting a wallet in the same ecosystem can fill the active wallet address when empty |
| `lockDestination` | Defaults to false. If true, both destination chain and token are required. A supplied `receiver` is also locked |
| `allowedChains` | Allowed chain IDs for both sides, such as `["1", "137"]` |
| `allowedTokens` | Allowed `{ chainId, address }` pairs for both sides |
| `slippage` | Fraction string: `"0.05"` means 5%, the default. Must be greater than 0 and at most 0.5. When configured, the UI cannot edit it. It is not a guaranteed destination minimum |
| `protocol` | `all`, `v1` or `v2`; defaults to `all`. Actual quotes determine available routes |

Preserve the case of TRON Base58 addresses. Networks, assets and routes can change; use the live API and quotes. Prefilling is not locking: unavailable prefilled assets may fall back to another available asset. An unavailable locked destination blocks submission. `allowedChains` and `allowedTokens` restrict selection; they do not enable unsupported backend assets.

### Example lock the destination asset

This configuration locks the destination to Ethereum DAI while users select a source asset and enter a receiver. Supplying a real `receiver` also locks that address. Check the Networks API for availability before going live.

```ts
const destinationConfig = {
  toChainId: '1',
  toTokenAddress: '0x6b175474e89094c44da98b954eedeac495271d0f',
  lockDestination: true,
}

// On creation: createWidget({ container: '#bridge', ...destinationConfig, wallets })
// Or update while no transaction is in progress:
widget.update(destinationConfig)
```

## Events and lifecycle

Register listeners immediately, then wait for `widget.ready`. Readiness means the host / iframe handshake is complete; it does not mean assets have loaded, a wallet is connected or an executable quote is available.

| Event | Payload and meaning |
| --- | --- |
| `ready` | `{ version: 1 }`; `1` is the messaging protocol version |
| `needConnect` | `{ ecosystem }`; a wallet connection is being requested |
| `connected` | `{ address, chainId, ecosystem }`; wallet connected |
| `transactionSent` | `{ txHash, chainId }`; source transaction submitted, not destination settlement |
| `statusChanged` | `{ txHash, status, order? }`; order state changed; can also report `source_failed` |
| `completed` | `{ txHash, order }`; the completed order matches the transfer. `txHash` is still the source transaction hash |
| `error` | `{ code, message }`; initialization, configuration or transaction error |

```ts
const off = widget.on('statusChanged', ({ txHash, status }) => {
  console.log(txHash, status)
})
off() // Remove this listener

widget.update({ theme: 'dark', lang: 'en', accentColor: '#245fef' })
// Recreate the widget to change container or widgetUrl.
// Invalid configuration can throw; a locked update emits an error event.

widget.destroy() // Remove the iframe, listeners and SDK timers; safe to call again
```

The Widget language selector changes presentation without clearing the draft, wallet connection or pending transfer. While idle, SDK updates to only `lang`, `theme` or `accentColor` also preserve the draft. Changing transfer configuration resets form selections, the wallet connection and quote; fields without explicit configuration, including the amount, can reset to defaults. During signing and order tracking, the Widget rejects `update` with `CONFIG_LOCKED`. `destroy()` does not cancel on-chain transactions. Recreating the Widget can resume tracking from its saved pending record. Recovery uses localStorage; blocked third-party storage can prevent signing and prompt users to open the standalone page.

`completed` is a UI notification and may be emitted again after a refresh or remount. For crediting, delivery or similar business actions, verify the order and actual settlement independently on your server and handle duplicates by transaction or order. Do not credit funds solely from a browser event.

## CSP and deployment

If the host uses CSP, extend its existing policy to allow:

- `https://galebridge.net` in `frame-src`.
- For CDN loading, `https://galebridge.net` in `script-src`. Use your approved nonce / hash for the inline example, or move it into an external script.

The Widget calls its hosted origin's API. The host does not need additional API URLs or credentials for those iframe requests. Embed with the SDK rather than a manually constructed iframe. Keep its sandbox settings to preserve the handshake and standalone-page link.

## Troubleshooting

| Symptom | Action |
| --- | --- |
| `Widget container not found` | Check the selector and mount after the container exists |
| `INIT_TIMEOUT` or a blank Widget | Serve over HTTP(S), check SDK / Widget connectivity and the host's script / frame CSP |
| Cannot connect inside the iframe | Supply the appropriate ecosystem adapter and the Provider actually selected by the user |
| EVM chain switching fails | Add the network in the wallet, then reconnect and get a fresh quote |
| `INVALID_CONFIG` / `CONFIG_LOCKED` | Check formats and required locked-destination fields; wait for the current transaction before updating |
| Order temporarily not found | Source indexing may be pending. Continue tracking; do not send again because of indexing delays |
| Wallet timeout or uncertain transaction result | Check wallet history and chain records, then recover the existing hash; do not automatically resend |
| Third-party storage unavailable | Use the Widget's standalone-page link to continue or recover |

## Scope and acceptance

Version 0.1.0 includes npm / CDN loading, EVM / TRON adapters, themes and languages, asset filters, destination locking, order tracking and recovery. Destination amounts are estimates. `slippage` does not provide an enforceable destination minimum. Displayed fees exclude wallet / network gas. Partner fee sharing, authenticated attribution and an enforced domain allowlist are not included.

Before launch, validate narrow screens, wallet connection and chain switching, quotes and approvals, source submission, final settlement, refresh recovery and teardown. Agree on a test plan first; real approvals and transactions must be confirmed by the authorized wallet holder.

## Example files

- `examples/widget/npm.ts`: npm client entry for your container and build tool.
- `examples/widget/cdn.html`: complete CDN page to serve over HTTP(S).
- `examples/widget/react.tsx`: React / Next.js client component.

These examples connect to the production Widget. Wallet actions require user confirmation.
