@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.
- package/README.md +402 -0
- package/android/build.gradle +69 -0
- package/android/src/main/kotlin/com/playloop/plugins/applovinmax/PlayLoopAppLovinMaxPlugin.kt +316 -0
- package/android/src/main/kotlin/com/playloop/plugins/deviceid/PlayLoopDeviceIdPlugin.kt +30 -0
- package/dist/loader/cjs/index.js +72 -0
- package/dist/loader/esm/index.js +69 -0
- package/dist/types/CashableSDK.d.ts +46 -0
- package/dist/types/adapters/CapacitorAdsAdapter.d.ts +25 -0
- package/dist/types/adapters/WebViewAdsAdapter.d.ts +13 -0
- package/dist/types/babylon/BabylonPlugin.d.ts +17 -0
- package/dist/types/babylon/index.d.ts +17 -0
- package/dist/types/index.d.ts +481 -0
- package/dist/types/loader/index.d.ts +300 -0
- package/dist/types/modules/AnalyticsModule.d.ts +17 -0
- package/dist/types/modules/EmbeddedAdsModule.d.ts +30 -0
- package/dist/types/modules/FeatureFlagModule.d.ts +36 -0
- package/dist/types/modules/RewardsModule.d.ts +19 -0
- package/dist/types/modules/SessionModule.d.ts +16 -0
- package/dist/types/modules/StandaloneAdsModule.d.ts +51 -0
- package/dist/types/transport/HttpTransport.d.ts +19 -0
- package/dist/types/transport/WebViewTransport.d.ts +17 -0
- package/dist/types/types.d.ts +147 -0
- package/dist/types/ui/UIModule.d.ts +64 -0
- package/docs/ads.md +210 -0
- package/docs/analytics.md +45 -0
- package/docs/babylon.md +88 -0
- package/docs/feature-flags.md +109 -0
- package/docs/game-integration-guide.md +449 -0
- package/docs/loader.md +113 -0
- package/docs/rewards.md +57 -0
- package/docs/session.md +43 -0
- package/docs/ui.md +248 -0
- package/docs/wire-protocol.md +194 -0
- 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
|
package/docs/babylon.md
ADDED
|
@@ -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
|
+
```
|