@clocktone/game-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +402 -0
  2. package/android/build.gradle +69 -0
  3. package/android/src/main/kotlin/com/playloop/plugins/applovinmax/PlayLoopAppLovinMaxPlugin.kt +316 -0
  4. package/android/src/main/kotlin/com/playloop/plugins/deviceid/PlayLoopDeviceIdPlugin.kt +30 -0
  5. package/dist/loader/cjs/index.js +72 -0
  6. package/dist/loader/esm/index.js +69 -0
  7. package/dist/types/CashableSDK.d.ts +46 -0
  8. package/dist/types/adapters/CapacitorAdsAdapter.d.ts +25 -0
  9. package/dist/types/adapters/WebViewAdsAdapter.d.ts +13 -0
  10. package/dist/types/babylon/BabylonPlugin.d.ts +17 -0
  11. package/dist/types/babylon/index.d.ts +17 -0
  12. package/dist/types/index.d.ts +481 -0
  13. package/dist/types/loader/index.d.ts +300 -0
  14. package/dist/types/modules/AnalyticsModule.d.ts +17 -0
  15. package/dist/types/modules/EmbeddedAdsModule.d.ts +30 -0
  16. package/dist/types/modules/FeatureFlagModule.d.ts +36 -0
  17. package/dist/types/modules/RewardsModule.d.ts +19 -0
  18. package/dist/types/modules/SessionModule.d.ts +16 -0
  19. package/dist/types/modules/StandaloneAdsModule.d.ts +51 -0
  20. package/dist/types/transport/HttpTransport.d.ts +19 -0
  21. package/dist/types/transport/WebViewTransport.d.ts +17 -0
  22. package/dist/types/types.d.ts +147 -0
  23. package/dist/types/ui/UIModule.d.ts +64 -0
  24. package/docs/ads.md +210 -0
  25. package/docs/analytics.md +45 -0
  26. package/docs/babylon.md +88 -0
  27. package/docs/feature-flags.md +109 -0
  28. package/docs/game-integration-guide.md +449 -0
  29. package/docs/loader.md +113 -0
  30. package/docs/rewards.md +57 -0
  31. package/docs/session.md +43 -0
  32. package/docs/ui.md +248 -0
  33. package/docs/wire-protocol.md +194 -0
  34. package/package.json +81 -0
@@ -0,0 +1,64 @@
1
+ import { ITransport, IAds, UIConfig, AdConfig } from '../types.js';
2
+
3
+ declare class UIModule {
4
+ private transport;
5
+ private ads;
6
+ private config;
7
+ private adConfig;
8
+ private debug;
9
+ private rootEl;
10
+ private cashableButton;
11
+ private sponsoredModal;
12
+ private bonusModal;
13
+ private boostOfferModal;
14
+ private coinToast;
15
+ private tierModal;
16
+ private offerScheduler;
17
+ private boostInterval;
18
+ private walletData;
19
+ private games;
20
+ private lastKnownTier;
21
+ private tierInitialized;
22
+ private lastWalletFetchAt;
23
+ constructor(transport: ITransport, ads: IAds, config: UIConfig, debug: boolean, adConfig: AdConfig);
24
+ initialize(): Promise<void>;
25
+ getTopBarHeight(): number;
26
+ /**
27
+ * Show the sponsored video countdown, then show an interstitial ad.
28
+ * Reserves the reward amount from the backend first, then claims after the ad.
29
+ * Returns true if the ad was shown and reward claimed successfully.
30
+ */
31
+ showSponsoredInterstitial(): Promise<boolean>;
32
+ /** Award coins: calculate delta from current balance, update display, show toast. */
33
+ showCoinAward(newTotalCoins: number): void;
34
+ /** Show a coin earned toast notification. Auto-appends boost note when active. */
35
+ showCoinToast(amount: number, note?: string): void;
36
+ /** Update displayed coin count and check for tier advancement. */
37
+ updateCoins(coins: number): void;
38
+ /** Re-fetch wallet data from backend and update UI. Debounced to 1s. */
39
+ refreshWallet(): Promise<void>;
40
+ /** Debug: trigger a bonus offer on the widget (fetches from backend). */
41
+ triggerBonusOffer(): Promise<void>;
42
+ /** Debug: trigger a booster offer on the widget. */
43
+ triggerBoosterOffer(): Promise<void>;
44
+ /** Hide/show the UI (for freeze/resume). */
45
+ setVisible(visible: boolean): void;
46
+ dispose(): void;
47
+ private initCashableButton;
48
+ private applyWalletData;
49
+ private handleBoostTap;
50
+ private startBoostCountdown;
51
+ private initOfferScheduler;
52
+ private handleOfferTap;
53
+ private handleBonusOffer;
54
+ private handleBoosterOffer;
55
+ private activateBoost;
56
+ private checkTierAdvancement;
57
+ private handleTierClaim;
58
+ private handleTierClaimWithAd;
59
+ /** Update coins display without triggering tier check (to avoid loops). */
60
+ private updateCoinsDisplay;
61
+ private log;
62
+ }
63
+
64
+ export { UIModule };
package/docs/ads.md ADDED
@@ -0,0 +1,210 @@
1
+ # Ads Module
2
+
3
+ Handles showing rewarded video, interstitial, and banner ads.
4
+
5
+ **Accessor**: `cashable.ads`
6
+
7
+ ## Methods
8
+
9
+ ### `showRewardedVideo(): Promise<boolean>`
10
+
11
+ Shows a rewarded video ad. Returns `true` if the user watched the full ad, `false` if they dismissed it or no ad was available.
12
+
13
+ ```typescript
14
+ const success = await cashable.ads.showRewardedVideo();
15
+ if (success) {
16
+ await cashable.rewards.awardCoins();
17
+ }
18
+ ```
19
+
20
+ ### `showInterstitial(): Promise<boolean>`
21
+
22
+ Shows an interstitial ad. Returns `true` if the ad was shown, `false` otherwise.
23
+
24
+ ```typescript
25
+ await cashable.ads.showInterstitial();
26
+ ```
27
+
28
+ ### `showBanner(position?): Promise<boolean>`
29
+
30
+ Shows a banner ad at the given screen position. Returns `true` if the banner was shown, `false` if no banner ad unit is configured or no fill is available.
31
+
32
+ - `position` — `'top'` or `'bottom'` (default: `'bottom'`)
33
+
34
+ Banner size is automatic: 320×50 on phones, 728×90 on tablets.
35
+
36
+ ```typescript
37
+ // Show banner at the bottom of the screen
38
+ const shown = await cashable.ads.showBanner();
39
+
40
+ // Show banner at the top
41
+ await cashable.ads.showBanner('top');
42
+ ```
43
+
44
+ ### `hideBanner(): Promise<void>`
45
+
46
+ Hides the banner ad without destroying it. The banner stays loaded in memory for fast re-show. Auto-refresh is paused while hidden.
47
+
48
+ ```typescript
49
+ await cashable.ads.hideBanner();
50
+ ```
51
+
52
+ ### `destroyBanner(): Promise<void>`
53
+
54
+ Destroys the banner ad and frees all resources. After calling this, you cannot show the banner again until the SDK is re-initialized.
55
+
56
+ ```typescript
57
+ await cashable.ads.destroyBanner();
58
+ ```
59
+
60
+ ### `onBeforeAd(handler): () => void`
61
+
62
+ Fires immediately before a fullscreen ad (rewarded/interstitial) is shown. Use this to pause audio, animations, or game logic.
63
+
64
+ ```typescript
65
+ const unsub = cashable.ads.onBeforeAd(() => {
66
+ pauseGameAudio();
67
+ });
68
+ ```
69
+
70
+ ### `onAfterAd(handler): () => void`
71
+
72
+ Fires after a fullscreen ad completes or is dismissed.
73
+
74
+ ```typescript
75
+ const unsub = cashable.ads.onAfterAd(({ success }) => {
76
+ resumeGameAudio();
77
+ if (success) {
78
+ showRewardAnimation();
79
+ }
80
+ });
81
+ ```
82
+
83
+ ## How Ads Work Per Mode
84
+
85
+ ### Embedded mode
86
+
87
+ The SDK sends a request to the host PlayLoop app via the WebView bridge. The host displays the ad using its own ad infrastructure and responds with the result. No `appLovin` config is needed.
88
+
89
+ ### Standalone mode
90
+
91
+ The SDK uses the bundled AppLovin MAX native plugin (Android Kotlin) to show ads directly. You must provide the `appLovin` config when initializing the SDK:
92
+
93
+ ```typescript
94
+ const cashable = new PlayLoopSDK({
95
+ gameId: 'my-game',
96
+ mode: 'standalone',
97
+ apiBaseUrl: 'https://api.cashable.app',
98
+ appLovin: {
99
+ sdkKey: 'YOUR_SDK_KEY',
100
+ rewardedAdUnitId: 'YOUR_REWARDED_AD_UNIT_ID',
101
+ interstitialAdUnitId: 'YOUR_INTERSTITIAL_AD_UNIT_ID',
102
+ bannerAdUnitId: 'YOUR_BANNER_AD_UNIT_ID', // optional
103
+ },
104
+ });
105
+ ```
106
+
107
+ **Where to find these values**:
108
+ - **SDK Key**: AppLovin dashboard → Account → General → Keys
109
+ - **Ad Unit IDs**: AppLovin dashboard → MAX → Ad Units → create or select units for your app
110
+
111
+ The native plugin (AppLovin MAX SDK 13.5.1) is bundled in the npm package under `android/`. When you install the SDK in a Capacitor project, Capacitor auto-links the native code.
112
+
113
+ If `appLovin` config is omitted in standalone mode, ads will silently return `false` (a debug warning is logged if `debug: true`).
114
+
115
+ If `bannerAdUnitId` is omitted, banner methods (`showBanner`, `hideBanner`, `destroyBanner`) will silently no-op.
116
+
117
+ This difference is transparent to the game developer - the API is identical in both modes.
118
+
119
+ ## Standalone-Only Methods
120
+
121
+ In standalone mode, the ads module wraps the core ad methods with cooldown enforcement (via `StandaloneAdsModule`). These additional methods are available for querying and managing cooldown state.
122
+
123
+ ### `isInterstitialAvailable(): boolean`
124
+
125
+ Returns `true` if the interstitial cooldown has elapsed and an interstitial can be shown. In embedded mode, always returns `true` (the host app manages pacing).
126
+
127
+ ```typescript
128
+ if (cashable.ads.isInterstitialAvailable()) {
129
+ await cashable.ui?.showSponsoredInterstitial();
130
+ }
131
+ ```
132
+
133
+ ### `isRewardedAvailable(): boolean`
134
+
135
+ Returns `true` if the rewarded ad cooldown has elapsed. In embedded mode, always returns `true`.
136
+
137
+ ```typescript
138
+ if (cashable.ads.isRewardedAvailable()) {
139
+ const success = await cashable.ads.showRewardedVideo();
140
+ }
141
+ ```
142
+
143
+ ### `hasShownFullScreenAd: boolean`
144
+
145
+ Read-only property. Returns `true` if any full-screen ad (rewarded or interstitial) has been shown during this session. Useful for tracking whether the user has engaged with ads.
146
+
147
+ ```typescript
148
+ if (!cashable.ads.hasShownFullScreenAd) {
149
+ // User hasn't seen any ads yet this session
150
+ }
151
+ ```
152
+
153
+ ### `updateAdConfig(overrides: Partial<AdConfig>): void`
154
+
155
+ Updates ad cooldown configuration at runtime by merging partial overrides into the current config. Standalone mode only — no-op in embedded mode.
156
+
157
+ ```typescript
158
+ cashable.ads.updateAdConfig?.({
159
+ interstitialCooldownMs: 120_000, // 2 minutes between interstitials
160
+ });
161
+ ```
162
+
163
+ ### `resetCooldowns(): void`
164
+
165
+ Resets all cooldown timers so ads can be shown immediately. Standalone mode only — no-op in embedded mode.
166
+
167
+ ```typescript
168
+ cashable.ads.resetCooldowns?.();
169
+ ```
170
+
171
+ ### `getAdConfig(): Readonly<AdConfig>`
172
+
173
+ Returns a read-only snapshot of the current ad configuration. Standalone mode only.
174
+
175
+ ```typescript
176
+ const config = cashable.ads.getAdConfig?.();
177
+ console.log(config?.interstitialCooldownMs);
178
+ ```
179
+
180
+ ### Cooldown Rules
181
+
182
+ The standalone ads module enforces cooldowns that match the PlayLoop host app behavior:
183
+
184
+ | Trigger | Next Interstitial Delay | Next Rewarded Delay |
185
+ |---------|-------------------------|---------------------|
186
+ | First interstitial access | `firstInterstitialCooldownMs` (default: 45s) | — |
187
+ | After interstitial shown | `interstitialCooldownMs` (default: 75s) | `interstitialToRewardedCooldownMs` (default: 30s) |
188
+ | After rewarded shown | `rewardedToInterstitialCooldownMs` (default: 90s) | — |
189
+
190
+ Key behaviors:
191
+ - **Rewarded ads are never blocked** — they are user-initiated, but showing one records cross-cooldowns for interstitials.
192
+ - **First interstitial is lazy-initialized** — the cooldown timer starts on the first availability check, not on SDK init.
193
+ - Cooldown values are read from the `ad_config` Statsig dynamic config (see [Feature Flags](feature-flags.md)) and fall back to `DEFAULT_AD_CONFIG`.
194
+
195
+ ## Sponsored Interstitial
196
+
197
+ In standalone mode, the SDK provides a `showSponsoredInterstitial()` method on the UI module. This reserves a reward amount from the backend, shows a 3-second animated countdown modal ("Sponsored Video"), then automatically displays the interstitial ad and claims the reward on success.
198
+
199
+ ```typescript
200
+ // Available via cashable.ui (standalone + authenticated only)
201
+ const success = await cashable.ui?.showSponsoredInterstitial();
202
+ ```
203
+
204
+ **Phases:**
205
+ 1. **Countdown** (3 seconds) — Animated SVG ring, reward badge shows "+{amount} coins"
206
+ 2. **Playing** — "Loading video..." while the ad shows
207
+ 3. **Success** — Green checkmark + "Coins Earned!" (auto-dismiss after 2s)
208
+ 4. **Failed** — "Video unavailable" (auto-dismiss after 2s)
209
+
210
+ See the [UI Module docs](ui.md#showsponsoredinterstitial-promiseboolean) for the full flow details.
@@ -0,0 +1,45 @@
1
+ # Analytics Module
2
+
3
+ Tracks play time and reports game metadata.
4
+
5
+ **Accessor**: `cashable.analytics`
6
+
7
+ ## Methods
8
+
9
+ ### `startAutoTracking(): void`
10
+
11
+ Starts automatic play-time tracking. Accumulates seconds and syncs to the backend every 10 seconds.
12
+
13
+ ```typescript
14
+ cashable.analytics.startAutoTracking();
15
+ ```
16
+
17
+ Call this once when the game starts. If called multiple times, subsequent calls are no-ops.
18
+
19
+ ### `stopAutoTracking(): void`
20
+
21
+ Stops play-time tracking and flushes any remaining accumulated seconds.
22
+
23
+ ```typescript
24
+ cashable.analytics.stopAutoTracking();
25
+ ```
26
+
27
+ ### `reportThemeColor(hex: string): void`
28
+
29
+ Reports the game's dominant theme color to the host. Used for UI customization in the PlayLoop app. Fire-and-forget - errors are silently ignored.
30
+
31
+ ```typescript
32
+ cashable.analytics.reportThemeColor('#FF6B35');
33
+ ```
34
+
35
+ ## Automatic Cleanup
36
+
37
+ When `cashable.dispose()` is called, analytics tracking is automatically stopped and flushed. You don't need to call `stopAutoTracking()` manually if you're disposing the entire SDK.
38
+
39
+ ## Sync Details
40
+
41
+ - Ticks accumulate locally at 1-second intervals
42
+ - Every 10 seconds, accumulated seconds are flushed to the backend via `analytics.playTick`
43
+ - Syncs are fire-and-forget: network failures don't block the game
44
+ - In embedded mode, the sync goes through the WebView bridge
45
+ - In standalone mode, it's a `POST /api/games/daily/{gameId}/tick?seconds=N` HTTP call
@@ -0,0 +1,88 @@
1
+ # Babylon.js Plugin
2
+
3
+ Integrates with Babylon.js for automatic render loop freeze/resume when the host app sends lifecycle events.
4
+
5
+ **Accessor**: `cashable.babylon`
6
+
7
+ ## Methods
8
+
9
+ ### `bindScene(scene, engine?): void`
10
+
11
+ Binds a Babylon.js scene (and optionally its engine) to the SDK. Once bound, the SDK automatically stops and restarts the render loop on freeze/resume events.
12
+
13
+ ```typescript
14
+ import { Engine, Scene } from '@babylonjs/core';
15
+
16
+ const engine = new Engine(canvas);
17
+ const scene = new Scene(engine);
18
+
19
+ // Option 1: Pass both scene and engine
20
+ cashable.babylon.bindScene(scene, engine);
21
+
22
+ // Option 2: Pass scene only (engine auto-detected via scene.getEngine())
23
+ cashable.babylon.bindScene(scene);
24
+ ```
25
+
26
+ **Parameters**:
27
+
28
+ | Name | Type | Description |
29
+ |---|---|---|
30
+ | `scene` | `{ render(): void }` | Babylon.js Scene instance |
31
+ | `engine` | `{ stopRenderLoop, runRenderLoop }` (optional) | Babylon.js Engine instance. If omitted, auto-detected via `scene.getEngine()` |
32
+
33
+ ### `freeze(): void`
34
+
35
+ Manually stops the render loop. Called automatically on `lifecycle.freeze` events.
36
+
37
+ ```typescript
38
+ cashable.babylon.freeze();
39
+ ```
40
+
41
+ ### `resume(): void`
42
+
43
+ Manually restarts the render loop. Called automatically on `lifecycle.resume` events.
44
+
45
+ ```typescript
46
+ cashable.babylon.resume();
47
+ ```
48
+
49
+ ### `dispose(): void`
50
+
51
+ Clears scene and engine references. Called automatically by `cashable.dispose()`.
52
+
53
+ ## Fallback Behavior
54
+
55
+ If no Babylon.js scene is bound, the plugin falls back to calling `window.__eaFreeze()` / `window.__eaResume()` if they exist. This provides backward compatibility with games that set up their own freeze/resume hooks.
56
+
57
+ ## Why This Exists
58
+
59
+ Previously, the PlayLoop app used ~200 lines of injected JavaScript to monkey-patch `setTimeout`, `setInterval`, `requestAnimationFrame`, `Date.now`, and `performance.now` for freeze/resume. This was fragile and Unity-specific.
60
+
61
+ The Babylon.js plugin replaces all of that with two clean API calls: `engine.stopRenderLoop()` and `engine.runRenderLoop(() => scene.render())`.
62
+
63
+ ## Full Example
64
+
65
+ ```typescript
66
+ import { Engine, Scene, ArcRotateCamera, HemisphericLight, MeshBuilder, Vector3 } from '@babylonjs/core';
67
+ import { PlayLoopSDK } from '@clocktone/game-sdk';
68
+
69
+ // Set up Babylon.js
70
+ const canvas = document.getElementById('renderCanvas') as HTMLCanvasElement;
71
+ const engine = new Engine(canvas, true);
72
+ const scene = new Scene(engine);
73
+
74
+ new ArcRotateCamera('cam', 0, 1, 10, Vector3.Zero(), scene);
75
+ new HemisphericLight('light', new Vector3(0, 1, 0), scene);
76
+ MeshBuilder.CreateSphere('sphere', {}, scene);
77
+
78
+ engine.runRenderLoop(() => scene.render());
79
+
80
+ // Set up SDK
81
+ const cashable = new PlayLoopSDK({ gameId: 'my-babylon-game' });
82
+ await cashable.init();
83
+ cashable.babylon.bindScene(scene, engine);
84
+ cashable.analytics.startAutoTracking();
85
+
86
+ // The render loop is now automatically managed by the SDK
87
+ // on freeze/resume events from the host app
88
+ ```
@@ -0,0 +1,109 @@
1
+ # Feature Flags Module
2
+
3
+ Remote configuration and feature gating powered by Statsig.
4
+
5
+ **Accessor**: `cashable.featureFlags` (nullable — `null` if not initialized)
6
+
7
+ ## Overview
8
+
9
+ The Feature Flags module provides access to Statsig dynamic configs, feature gates, and event logging. It is initialized automatically during `cashable.init()` using bootstrap values from the backend `/api/games/init` endpoint.
10
+
11
+ Bootstrap values are provided server-side so the Statsig client initializes synchronously without a blocking network request. Updated values are fetched in the background automatically.
12
+
13
+ ## Methods
14
+
15
+ ### `isInitialized(): boolean`
16
+
17
+ Returns `true` if the Statsig client was successfully bootstrapped.
18
+
19
+ ```typescript
20
+ if (cashable.featureFlags?.isInitialized()) {
21
+ // Safe to read configs and gates
22
+ }
23
+ ```
24
+
25
+ ### `getConfig(configName): { get, value }`
26
+
27
+ Read a Statsig dynamic config by name. Returns an object with:
28
+
29
+ - `get<V>(key: string, defaultValue: V): V` — type-safe value accessor
30
+ - `value: Record<string, unknown>` — raw config object
31
+
32
+ If Statsig is not initialized, `get()` always returns the `defaultValue` and `value` is `{}`.
33
+
34
+ ```typescript
35
+ const config = cashable.featureFlags?.getConfig('game_settings');
36
+ const maxLives = config?.get('max_lives', 3) ?? 3;
37
+ const dailyLimit = config?.get('daily_coin_limit', 1000) ?? 1000;
38
+ ```
39
+
40
+ ### `checkGate(name): boolean`
41
+
42
+ Check whether a feature gate is passing. Returns `false` if Statsig is not initialized.
43
+
44
+ ```typescript
45
+ if (cashable.featureFlags?.checkGate('new_bonus_flow')) {
46
+ showNewBonusUI();
47
+ } else {
48
+ showLegacyBonusUI();
49
+ }
50
+ ```
51
+
52
+ ### `logEvent(eventName, value?, metadata?): void`
53
+
54
+ Log a custom event to Statsig for analytics and experiment exposure tracking.
55
+
56
+ - `eventName: string` — event name
57
+ - `value?: number` — optional numeric value
58
+ - `metadata?: Record<string, string>` — optional key-value metadata
59
+
60
+ No-op if Statsig is not initialized.
61
+
62
+ ```typescript
63
+ cashable.featureFlags?.logEvent('level_completed', 5, {
64
+ difficulty: 'hard',
65
+ timeMs: '12345',
66
+ });
67
+ ```
68
+
69
+ ### `adConfig: Readonly<AdConfig>`
70
+
71
+ Getter that returns the current ad configuration values, with Statsig overrides merged on top of defaults. Used internally by the SDK to configure ad cooldowns and offer cadences.
72
+
73
+ ```typescript
74
+ const config = cashable.featureFlags?.adConfig;
75
+ console.log(config?.interstitialCooldownMs); // e.g. 75000
76
+ ```
77
+
78
+ The ad config is read from the `ad_config` Statsig dynamic config. The following keys are mapped:
79
+
80
+ | SDK Key | Statsig Key |
81
+ |---------|-------------|
82
+ | `interstitialCooldownMs` | `interstitial_cooldown_ms` |
83
+ | `firstInterstitialCooldownMs` | `first_interstitial_cooldown_ms` |
84
+ | `rewardedToInterstitialCooldownMs` | `rewarded_to_interstitial_cooldown_ms` |
85
+ | `interstitialToRewardedCooldownMs` | `interstitial_to_rewarded_cooldown_ms` |
86
+ | `playClickInterstitialCooldownMs` | `play_click_interstitial_cooldown_ms` |
87
+ | `earnBeforePlayingAdCooldownMs` | `earn_before_playing_ad_cooldown_ms` |
88
+ | `gamesBonusOfferCadenceMs` | `games_bonus_offer_cadence_ms` |
89
+ | `gamesCoinBoosterCadenceMs` | `games_coin_booster_cadence_ms` |
90
+
91
+ ### `dispose(): void`
92
+
93
+ Shuts down the Statsig client and releases resources. Called automatically by `cashable.dispose()`.
94
+
95
+ ## How It Works
96
+
97
+ 1. During `cashable.init()`, the SDK calls `games.init` (via the transport layer) to fetch init data from the backend.
98
+ 2. The backend responds with a `userId` and optional `featureFlags` containing a Statsig `clientKey` and pre-serialized `bootstrapValues`.
99
+ 3. The module creates a `StatsigClient` with the bootstrap values and calls `initializeSync()` for instant, synchronous initialization.
100
+ 4. If no bootstrap values are provided, `initializeAsync()` is used instead (blocking network call to Statsig).
101
+ 5. After initialization, the `ad_config` dynamic config is read and merged with `DEFAULT_AD_CONFIG`.
102
+
103
+ ## Peer Dependency
104
+
105
+ The `@statsig/js-client` package is a peer dependency (optional). If not installed, the module will fail to initialize and fall back to default values.
106
+
107
+ ```bash
108
+ npm install @statsig/js-client
109
+ ```