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

# How Do I Set Up In-App Messaging in the Web SDK (v4)?

> Display Gameball in-app campaigns on your website or in a React, Angular, or Ionic app

> [!NOTE]
>
> **Not yet released.** The Web SDK is not published yet, so the package version and script URL on this page are placeholders. In-app messaging is available now in the [iOS](https://docs.gameball.co/installation-guides/v4/ios/in-app-messaging.md), [Android](https://docs.gameball.co/installation-guides/v4/android/in-app-messaging.md), [Flutter](https://docs.gameball.co/installation-guides/v4/flutter/in-app-messaging.md), and [React Native](https://docs.gameball.co/installation-guides/v4/react-native/in-app-messaging.md) SDKs, version 4.0.0 and later.

# Display In-App Messages

One package for websites, React, Angular, and Ionic apps. Identify the customer, start messaging, and your dashboard campaigns appear: slide-ups, modals, and full screens, triggered by session start or by the events you already send.

For campaign options, triggers, filters, priority, cooldown, quiet hours, and how the analytics are defined, see [In-App Messaging Campaigns](https://docs.gameball.co/product-documentation/communication-campaigns/create-and-configure-in-app-messaging-campaigns.md).

> [!WARNING]
>
> **Campaign targeting is not yet available for browsers.** Campaigns target iOS and Android, and the campaign composer cannot target a browser yet, so a website integration syncs successfully but receives no campaigns. Contact [support@gameball.co](mailto:support@gameball.co) before you start, so the team can tell you where browser targeting stands for your account.
>
> This does not apply to **Ionic + Capacitor**, which runs as a native app and reports the real `ios` or `android` platform. Those builds are targetable today.

***

## Requirements

> [tab] Script tag
>
> | **Item** | Requirement |
> | - | - |
> | Build | The script build, around 21 KB gzipped. No bundler, no build step |
> | Import | The script defines the global `window.Gameball` |
> | Verified against | Any page that can load a script tag |
> | Browsers | Anything with Shadow DOM and `Promise`: Chrome, Edge, Firefox, Safari 14+. The build targets ES2019 |
> | Dependencies | None |
> | Storage | `localStorage` when available. Private mode or blocked storage falls back to memory, so frequency then lasts a page load rather than a device |
> | Content Security Policy | `connect-src` for your Gameball API host and `img-src` for the artwork CDN. The SDK injects no third-party script and sets no cookie |

> [tab] React
>
> | **Item** | Requirement |
> | - | - |
> | Package | `@gameball/web-sdk`, not yet published |
> | Import | `import { Gameball } from '@gameball/web-sdk';` — one shared singleton, so every file that imports it gets the same instance |
> | Verified against | React 18 and 19, with Vite. The API is imperative, so it works the same under Next.js, Remix, or CRA |
> | Browsers | Chrome, Edge, Firefox, Safari 14+. The build targets ES2019 |
> | Dependencies | None |
> | Storage | `localStorage` when available, falling back to memory |
> | Content Security Policy | `connect-src` for your Gameball API host and `img-src` for the artwork CDN |

> [tab] Angular
>
> | **Item** | Requirement |
> | - | - |
> | Package | `@gameball/web-sdk`, not yet published |
> | Import | `import { Gameball } from '@gameball/web-sdk';` — one shared singleton |
> | Verified against | Angular 18 and 22, standalone bootstrap. Works with NgModule bootstrapping too; only the file the `init` call sits in changes |
> | Browsers | Chrome, Edge, Firefox, Safari 14+. The build targets ES2019 |
> | Dependencies | None |
> | Storage | `localStorage` when available, falling back to memory |
> | Content Security Policy | `connect-src` for your Gameball API host and `img-src` for the artwork CDN |

> [tab] Ionic + Capacitor
>
> | **Item** | Requirement |
> | - | - |
> | Package | `@gameball/web-sdk`, not yet published |
> | Import | Two entry points: `@gameball/web-sdk` for the SDK and `@gameball/web-sdk/capacitor` for the native adapter |
> | Verified against | Ionic 9 (Angular and React) on Capacitor 8, iOS and Android |
> | Dependencies | None of its own. The adapter uses three optional Capacitor plugins |
> | Storage | `localStorage` when available, falling back to memory |
> | Content Security Policy | `connect-src` for your Gameball API host and `img-src` for the artwork CDN |

***

## Install

> [!NOTE]
>
> **Placeholders until release.** `GAMEBALL_WEB_SDK_VERSION` and `GAMEBALL_WEB_SDK_URL` stand in for the version and script URL the Web SDK will be published under.

> [tab] Script tag
>
> Load the script build before your own scripts. It defines `window.Gameball` and needs nothing else.
>
> ```html index.html theme={null}
> <script src="GAMEBALL_WEB_SDK_URL"></script>
> ```

> [tab] React
>
> ```bash theme={null}
> npm install @gameball/web-sdk@GAMEBALL_WEB_SDK_VERSION
> ```
>
> The package ships ESM, CommonJS, and TypeScript declarations, and is marked `sideEffects: false`, so a bundler tree-shakes what you do not use.

> [tab] Angular
>
> ```bash theme={null}
> npm install @gameball/web-sdk@GAMEBALL_WEB_SDK_VERSION
> ```
>
> The package ships ESM, CommonJS, and TypeScript declarations, and is marked `sideEffects: false`.

> [tab] Ionic + Capacitor
>
> Install the SDK, then the three Capacitor plugins the adapter uses. Each one is optional, and a plugin you skip falls back to the web behavior with a log line.
>
> ```bash theme={null}
> npm install @gameball/web-sdk@GAMEBALL_WEB_SDK_VERSION
> npm install @capacitor/app @capacitor/browser @capacitor/push-notifications
> npx cap sync
> ```

***

## Quick Start

Four calls carry the integration: `init` once at startup, `identify` when you know who the visitor is, `startInAppMessaging` to begin evaluating campaigns, and `stopInAppMessaging` on logout.

> [tab] Script tag
>
> **1. Configure at page load.** `init` configures the API client and nothing else. No network request is made until you identify a customer.
>
> ```html index.html theme={null}
> <script src="GAMEBALL_WEB_SDK_URL"></script>
> <script>
>   Gameball.init({
>     apiKey: 'YOUR_API_KEY',
>     lang: 'en',
>   });
> </script>
> ```
>
> **2. Identify, then start.** Run this wherever your page learns who the visitor is.
>
> ```js gameball-setup.js theme={null}
> async function gameballSignedIn(customerId) {
>   await Gameball.identify(customerId, { preferredLanguage: 'en' });
>
>   Gameball.startInAppMessaging({
>     // A campaign's `navigate` action hands you a route. On a classic site, follow it.
>     onNavigate: (route) => { window.location.href = route; },
>   });
> }
>
> function gameballSignedOut() {
>   Gameball.stopInAppMessaging();
> }
> ```
>
> > [!NOTE]
> >
> > **Multi-page sites re-run all of it.** Every navigation is a fresh page load, so `init`, `identify`, and `startInAppMessaging` all run again. That is expected and cheap: the SDK reads its cached sync, and the display history in `localStorage` means a once-per-customer message still shows only once.
>
> **3. Send the events your campaigns trigger on.**
>
> ```js theme={null}
> document.querySelector('#add-to-cart').addEventListener('click', function () {
>   Gameball.sendEvent('add_to_cart', { price: 1200, category: 'shoes' });
> });
> ```

> [tab] React
>
> **1. Configure before React renders.** `init` belongs at module load in your entry file, above `createRoot`, not in a component. Any component that calls the SDK is then guaranteed to find it configured.
>
> ```tsx src/main.tsx theme={null}
> import { createRoot } from 'react-dom/client';
> import { Gameball } from '@gameball/web-sdk';
> import { App } from './App';
>
> // Runs once, at module load, before anything can render.
> Gameball.init({
>   apiKey: import.meta.env.VITE_GAMEBALL_API_KEY,
>   lang: 'en',
>   debug: import.meta.env.DEV,
> });
>
> createRoot(document.getElementById('root')!).render(<App />);
> ```
>
> **2. Own the lifecycle in one provider.** Identifying and starting are tied to "a customer is signed in", which is a lifecycle, so put them in one component and let the rest of your app stay unaware.
>
> ```tsx src/gameball.tsx theme={null}
> import { createContext, useContext, useEffect, useRef, type ReactNode } from 'react';
> import { Gameball } from '@gameball/web-sdk';
> import { useNavigate } from 'react-router-dom';
>
> const GameballContext = createContext(Gameball);
> export const useGameball = () => useContext(GameballContext);
>
> export function GameballProvider({ customerId, children }: { customerId: string | null; children: ReactNode }) {
>   // Held in a ref so a new navigate function never restarts messaging.
>   const navigate = useNavigate();
>   const navigateRef = useRef(navigate);
>   navigateRef.current = navigate;
>
>   useEffect(() => {
>     if (!customerId) return;
>     let cancelled = false;
>
>     void (async () => {
>       await Gameball.identify(customerId, { preferredLanguage: 'en' });
>       if (cancelled) return;  // Signed out while the request was in flight
>       Gameball.startInAppMessaging({
>         onNavigate: (route, args) => navigateRef.current(route, { state: args }),
>       });
>     })();
>
>     return () => { cancelled = true; Gameball.stopInAppMessaging(); };
>   }, [customerId]);
>
>   return <GameballContext.Provider value={Gameball}>{children}</GameballContext.Provider>;
> }
> ```
>
> > [!WARNING]
> >
> > **That cleanup is not optional in development.** React StrictMode mounts every effect twice. Without `stopInAppMessaging()` in the cleanup, the second `startInAppMessaging` finds messaging already running and logs `hooks ignored — messaging is already running`, silently discarding your `onNavigate` and `beforeDisplay`. Stopping and restarting is cheap and keeps the display history.
>
> **3. Mount it around your app.** Pass the customer ID from wherever your auth lives. `null` while signed out is what drives the stop.
>
> ```tsx src/App.tsx theme={null}
> import { GameballProvider } from './gameball';
> import { useAuth } from './auth';
>
> export function App() {
>   const { user } = useAuth();
>   return (
>     <GameballProvider customerId={user?.id ?? null}>
>       <Routes>{/* your app */}</Routes>
>     </GameballProvider>
>   );
> }
> ```
>
> **4. Send the events your campaigns trigger on.**
>
> ```tsx src/ProductPage.tsx theme={null}
> import { useGameball } from './gameball';
>
> export function ProductPage() {
>   const gameball = useGameball();
>   return (
>     <button onClick={() => void gameball.sendEvent('add_to_cart', { price: 1200, category: 'shoes' })}>
>       Add to cart
>     </button>
>   );
> }
> ```

> [tab] Angular
>
> **1. Configure before Angular bootstraps.** `init` belongs in `main.ts`, above `bootstrapApplication`, so no component or service can reach the SDK unconfigured.
>
> ```ts src/main.ts theme={null}
> import { bootstrapApplication } from '@angular/platform-browser';
> import { Gameball } from '@gameball/web-sdk';
> import { AppComponent } from './app/app.component';
> import { appConfig } from './app/app.config';
> import { environment } from './environments/environment';
>
> // Runs once, before bootstrap.
> Gameball.init({
>   apiKey: environment.gameballApiKey,
>   lang: 'en',
>   debug: !environment.production,
> });
>
> bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));
> ```
>
> On an NgModule app, put the same call above `platformBrowserDynamic().bootstrapModule(AppModule)`. Nothing else differs.
>
> **2. Wrap the SDK in one root-provided service.** A single injectable keeps the SDK out of your components and gives the zone problem exactly one place to be solved.
>
> ```ts src/app/gameball.service.ts theme={null}
> import { Injectable, NgZone, inject } from '@angular/core';
> import { Router } from '@angular/router';
> import { Gameball } from '@gameball/web-sdk';
>
> @Injectable({ providedIn: 'root' })
> export class GameballService {
>   private readonly zone = inject(NgZone);
>   private readonly router = inject(Router);
>
>   async signedIn(customerId: string, lang = 'en'): Promise<void> {
>     await Gameball.identify(customerId, { preferredLanguage: lang });
>
>     Gameball.startInAppMessaging({
>       // Messages are drawn into a Shadow DOM overlay attached to document.body,
>       // outside Angular's component tree, so every hook fires OUTSIDE the zone.
>       onNavigate: (route, args) => this.zone.run(() => {
>         void this.router.navigate([route], { queryParams: args });
>       }),
>     });
>   }
>
>   signedOut(): void {
>     Gameball.stopInAppMessaging();
>   }
>
>   sendEvent(name: string, properties: Record<string, unknown> = {}): void {
>     void Gameball.sendEvent(name, properties);
>   }
> }
> ```
>
> > [!WARNING]
> >
> > **Every SDK callback lands outside the Angular zone.** That applies to `onNavigate`, `onAction`, `beforeDisplay`, and `onInAppMessage`. Anything in them that should update the UI needs `NgZone.run()`. Miss it and the symptom is confusing: the log shows the action fired, but the screen does not move.
>
> **3. Call it from your auth flow.**
>
> ```ts src/app/auth.component.ts theme={null}
> import { Component, inject } from '@angular/core';
> import { GameballService } from './gameball.service';
>
> @Component({ selector: 'app-auth', standalone: true, template: '' })
> export class AuthComponent {
>   private readonly gameball = inject(GameballService);
>
>   async onLoginSuccess(customerId: string): Promise<void> {
>     await this.gameball.signedIn(customerId);
>   }
>
>   onLogout(): void {
>     this.gameball.signedOut();
>   }
> }
> ```

> [tab] Ionic + Capacitor
>
> An Ionic app is a native app, and your campaigns target it as one. The difference from a website is a single `adapter` argument to `init`: it reports `ios` or `android` instead of a browser, follows the native app lifecycle, opens links in the in-app browser sheet, and raises the native push prompt.
>
> > [!WARNING]
> >
> > **Do not pass `platform` here.** The adapter reports the real `ios` or `android` at runtime, and an explicit `platform` only overrides the truth. Because one JavaScript bundle ships to both operating systems, any hard-coded value is wrong on one of them: those devices would get the other platform's campaigns, be registered under the wrong `osType`, and have every impression and click attributed to the wrong platform in your reporting. The messages still appear, so nothing looks broken while the numbers quietly go wrong.
>
> **1. Configure with the adapter, before the app boots.**
>
> ```ts Angular (src/main.ts) theme={null}
> import { bootstrapApplication } from '@angular/platform-browser';
> import { RouteReuseStrategy, provideRouter } from '@angular/router';
> import { IonicRouteStrategy, provideIonicAngular } from '@ionic/angular';
> import { Gameball } from '@gameball/web-sdk';
> import { capacitorAdapter } from '@gameball/web-sdk/capacitor';
> import { App } from '@capacitor/app';
> import { Browser } from '@capacitor/browser';
> import { PushNotifications } from '@capacitor/push-notifications';
> import { routes } from './app/app.routes';
> import { AppComponent } from './app/app.component';
> import { environment } from './environments/environment';
>
> Gameball.init({
>   apiKey: environment.gameballApiKey,
>   lang: 'en',
>   debug: !environment.production,
>   adapter: capacitorAdapter({ plugins: { App, Browser, PushNotifications } }),
> });
>
> bootstrapApplication(AppComponent, {
>   providers: [
>     { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
>     provideIonicAngular(),
>     provideRouter(routes),
>   ],
> });
> ```
>
> ```tsx React (src/main.tsx) theme={null}
> import { createRoot } from 'react-dom/client';
> import { Gameball } from '@gameball/web-sdk';
> import { capacitorAdapter } from '@gameball/web-sdk/capacitor';
> // Capacitor's plugin is also called App — alias it, or it collides with your root component.
> import { App as CapacitorApp } from '@capacitor/app';
> import { Browser } from '@capacitor/browser';
> import { PushNotifications } from '@capacitor/push-notifications';
> import App from './App';
>
> Gameball.init({
>   apiKey: import.meta.env.VITE_GAMEBALL_API_KEY,
>   lang: 'en',
>   debug: import.meta.env.DEV,
>   adapter: capacitorAdapter({ plugins: { App: CapacitorApp, Browser, PushNotifications } }),
> });
>
> createRoot(document.getElementById('root')!).render(<App />);
> ```
>
> > [!NOTE]
> >
> > **Under `ionic serve` you will see no messages, and that is correct.** A browser is not Capacitor, so the adapter logs `capacitor: running in a browser; web defaults apply` and reports a browser platform, which campaigns cannot target. Test on a simulator or device with `npx cap run ios` or `npx cap run android`.
>
> **2. Own the lifecycle.** From here the wiring is your framework's, not Capacitor's — identical to the React and Angular tabs above. Wrap the provider around your `IonApp`, or inject the service from your auth flow, exactly as you would any other app-wide concern.

***

## Start and Stop

`startInAppMessaging` syncs your campaigns, evaluates the session-start ones, and begins listening for your events. It needs an identified customer, so call it after `identify`, or pass `customerId` directly.

`stopInAppMessaging()` belongs on logout. It dismisses anything on screen, clears the in-memory session, and flushes analytics. It deliberately keeps the stored display history and the unsent outbox, because dropping them would lose impressions the backend has already counted and would let a once-ever message show again. Identifying a *different* customer is what discards them.

> [!NOTE]
>
> **Single-page apps need no route hooks.** Messages are drawn into a Shadow DOM overlay attached to `document.body`, outside your framework's tree, so a re-render or a route change never removes one. You do not restart messaging per route.

> [!WARNING]
>
> **Hooks are read once, when messaging starts.** Calling `startInAppMessaging` again while it is running logs `hooks ignored — messaging is already running` and changes nothing. To swap hooks, call `stopInAppMessaging()` first.

***

## Control Display and Actions

All hooks are optional and all are passed to `startInAppMessaging`. `beforeDisplay` decides whether a message may appear right now, and `onAction` lets you handle a button yourself.

```js theme={null}
Gameball.startInAppMessaging({
  // Decide per message: show it now, hold it, or drop it.
  beforeDisplay: (message) => (checkoutIsOpen ? 'later' : 'show'),

  // Handle an action yourself. Return true and the SDK does nothing further;
  // the click is still reported and the message still closes.
  onAction: (message, button, action) => {
    if (action.type === 'navigate') { myRouter.go(action.route); return true; }
    return false;
  },

  onNavigate: (route) => { myRouter.go(route); },
});
```

> [!TIP]
>
> In React, read live state through a ref. `beforeDisplay` is captured once, but a ref's current value is always the latest render's, so messaging never restarts. In Angular, wrap anything that touches the UI in `NgZone.run()`.

### Holding Messages Behind Your Own Overlays

While your own modal, drawer, or checkout step is open, tell the SDK so a message does not land on top of it. The held message displays as soon as you clear the flag.

```js theme={null}
Gameball.setOverlayOpen(true);
// … show your own modal, then when it closes:
Gameball.setOverlayOpen(false);  // A held message appears now
```

> [!TIP]
>
> In React, put this in one effect per overlay and clear the flag in the cleanup, so it is cleared even if the component unmounts without an explicit close. In Angular, use `ngOnInit` and `ngOnDestroy`.

***

## Actions

A message's surface and each of its buttons carry one action. `action.type` is a string, so a `switch` covers them.

| **Type** | Fields | Built-in behavior |
| - | - | - |
| `dismiss` | None | Closes the message. |
| `open_url` | `url`, `external` | A new tab when `external` is true, the same tab otherwise. In an Ionic app, the in-app browser sheet through `@capacitor/browser`, or the system browser if that plugin is missing. Only `http`, `https`, `mailto`, and `tel` are opened; anything else is refused and logged. |
| `navigate` | `route`, `args` | Your `onNavigate`. Without one the SDK logs that the route was ignored, because it cannot know your router. |
| `request_push_permission` | None | Asks for notification permission, then closes. |

***

## Observe Messages

Every selected message is published to any listener you register, whatever the SDK then does with it. `onInAppMessage` returns an unsubscribe function — call it, or you leak the listener.

```js theme={null}
const off = Gameball.onInAppMessage((message) => {
  myAnalytics.track('gameball_message_shown', {
    type: message.type,
    campaignId: message.campaignId,
    isTest: message.isTest,
  });
});

// off() when you no longer want them.
```

***

## Events and Purchases

Send the events your campaigns are triggered by. Properties feed the campaign filters, and they are evaluated locally before the request goes out, so a matching message can display immediately. `logPurchase` is the reserved `purchase` event, so both any-purchase and price-filtered campaigns match it.

```js theme={null}
await Gameball.sendEvent('add_to_cart', { price: 1200, category: 'shoes' });

await Gameball.logPurchase({ productId: 'sku-1', price: 120, currency: 'EGP' });
```

> [!NOTE]
>
> **Both calls need an identified customer.** Before `identify`, `sendEvent` logs `sendEvent ignored: identify a customer first` and does nothing.

***

## Language

The message locale comes from the customer's `preferredLanguage` when they have one, otherwise from `init({ lang })`, otherwise English. Changing it takes effect at the *next* session, because the locale is chosen by the sync.

Arabic messages render right-to-left. The overlay reads the resolved locale first and the document's `dir` second, so a page that is already RTL needs no extra configuration.

```js theme={null}
await Gameball.identify('customer_123', { preferredLanguage: 'ar' });
document.documentElement.lang = 'ar';
document.documentElement.dir = 'rtl';
```

***

## Push Permission

A campaign button can ask for notification permission. On the web the SDK calls `Notification.requestPermission()`. Inside Capacitor it raises the native prompt through `@capacitor/push-notifications`, falling back to `Notification.requestPermission()` when that plugin is not installed.

Supply your own requester if you use a push provider with its own flow. Return `true` only when permission was actually granted.

```js theme={null}
Gameball.startInAppMessaging({
  requestPushPermission: async () => (await myPushProvider.ask()) === 'granted',
});
```

> [!WARNING]
>
> **The prompt needs a user gesture, and the button press is one.** A browser that has already blocked notifications for your origin resolves immediately as denied, and the message still closes.

***

## The Capacitor Adapter

The adapter is what turns the same package into a native integration. Every plugin is detected at runtime: one you have not installed falls back to the web default with a log line, and the `plugins` argument is optional when Capacitor's own registry already holds them. The adapter has no build-time dependency on Capacitor, so a website's bundle is unaffected.

| **Plugin** | Present | Absent |
| - | - | - |
| `@capacitor/app` | The native app lifecycle drives sessions. | Browser visibility events are used instead. |
| `@capacitor/browser` | `open_url` opens the in-app browser sheet. | The system browser opens instead. |
| `@capacitor/push-notifications` | `request_push_permission` raises the native prompt. | Falls back to `Notification.requestPermission()`. |

***

## Behavior Reference

These rules are shared with every Gameball SDK and are described in full in [In-App Messaging Campaigns](https://docs.gameball.co/product-documentation/communication-campaigns/create-and-configure-in-app-messaging-campaigns.md). This is what they mean on the web.

| **Topic** | In this SDK |
| - | - |
| Sessions | A session starts when messaging starts, and again when the page returns to visibility after more than 30 seconds hidden. Each session re-syncs. |
| Sync and cache | Campaigns and account settings are fetched at session start and cached. The cache is read only when the sync fails. |
| Where it draws | A Shadow DOM overlay attached to `document.body`, so your own CSS cannot leak into a message and a message cannot leak into your page. |
| One at a time | A message that cannot show now waits in a pending slot and is retried at the next opportunity. |
| Cooldown and quiet hours | Both come from the account settings. Quiet hours are judged on the visitor's local clock. |
| Artwork | Prefetched at sync. `http://` URLs are refused before any request. A campaign whose artwork is not ready is passed over. |
| Impressions | Recorded when the message has actually painted, which is also when auto-dismiss starts. |
| Analytics | Impression, click with button ID, and dismiss, batched and persisted in `localStorage`. |
| Storage | `localStorage` when available. Private mode falls back to memory, so frequency lasts a page load rather than a device. |

***

## Logging and Troubleshooting

Pass `debug: true` to `init` for a running commentary prefixed `[GameballIAM]`.

| **You see** | It means |
| - | - |
| `synced 0 campaign(s)` | No campaign matched this customer and platform. For a browser integration, this is expected until campaign targeting supports browsers. |
| `hooks ignored — messaging is already running` | `startInAppMessaging` was called twice. Call `stopInAppMessaging()` first to swap hooks. |
| `sendEvent ignored: identify a customer first` | `identify` has not resolved yet. |
| `campaign N skipped: artwork not ready` | The image has not loaded, or is `http://`. The next eligible campaign takes the slot. |
| `capacitor: running in a browser; web defaults apply` | An Ionic build running under `ionic serve`. Test on a device or simulator. |

***

## API Reference

| **Member** | Signature |
| - | - |
| Configure | `Gameball.init({ apiKey, lang?, debug?, adapter? })` |
| Identify | `Gameball.identify(customerId, attributes?)` |
| Start | `Gameball.startInAppMessaging(hooks?)` |
| Stop | `Gameball.stopInAppMessaging()` |
| Overlay flag | `Gameball.setOverlayOpen(open: boolean)` |
| Observe | `Gameball.onInAppMessage(listener)` returning an unsubscribe function |
| Events | `Gameball.sendEvent(name, properties?)` |
| Purchase | `Gameball.logPurchase({ productId, price, currency, quantity?, properties? })` |
| Capacitor adapter | `capacitorAdapter({ plugins })` from `@gameball/web-sdk/capacitor` |

***

## Launch Checklist

**1. Configure once, before anything renders**

`init` runs once at module load or in a script tag above anything that could call the SDK, never inside a component.

**2. Identify and start**

`identify` runs on login and on every page load for a signed-in visitor, and `stopInAppMessaging` runs on logout.

**3. Handle your framework's lifecycle**

In React, the effect that starts messaging returns `stopInAppMessaging()` from its cleanup. In Angular, every hook that touches the UI is wrapped in `NgZone.run()`. In Ionic, Capacitor's `App` plugin is aliased if your root component is also called `App`, and no `platform` is passed to `init`.

**4. Wire hooks and overlays**

`onNavigate` is wired to your router, or your campaigns use no `navigate` actions, and `setOverlayOpen` brackets your own modals, drawers, and checkout steps.

**5. Check CSP and release settings**

Your Content Security Policy allows the API host and the artwork CDN, campaign artwork is served over `https`, and `debug` is `false` before release. Test an Ionic build in the native shell rather than `ionic serve`.

***

## Related Articles

> [card] [In-App Messaging Campaigns](https://docs.gameball.co/product-documentation/communication-campaigns/create-and-configure-in-app-messaging-campaigns.md)
>
> Campaign options, triggers, delivery rules, and analytics definitions.

> [card] [Initialize Widget](https://docs.gameball.co/installation-guides/v4/web/initialize-widget.md)
>
> Set up the Gameball widget on your website.

> [card] [Track Events](https://docs.gameball.co/installation-guides/v4/web/track-events.md)
>
> Send the events your campaigns are triggered by.

> [card] [Go-Live Checklist](https://docs.gameball.co/installation-guides/v4/web/go-live-checklist.md)
>
> Verify the full web integration before release.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.