@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
package/README.md ADDED
@@ -0,0 +1,402 @@
1
+ # @clocktone/game-sdk
2
+
3
+ Unified TypeScript SDK for PlayLoop games. Provides a single API surface for both **embedded** (WebView inside the PlayLoop app) and **standalone** (Capacitor-wrapped APK) game modes.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @clocktone/game-sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ The npm package ships only the **CDN loader** — a ~1KB bootstrap that loads the full SDK from CDN at runtime. This keeps the SDK out of your bundle and enables over-the-air updates. See the [Game Integration Guide](docs/game-integration-guide.md) for a complete walkthrough.
14
+
15
+ ```typescript
16
+ import { loadPlayLoopSDK } from '@clocktone/game-sdk/loader';
17
+ import type { PlayLoopSDKConfig } from '@clocktone/game-sdk/loader';
18
+
19
+ // Load SDK from CDN
20
+ const SDK = await loadPlayLoopSDK({ apiBaseUrl: 'https://api.vbackendapi.com' });
21
+
22
+ const sdk = new SDK({
23
+ gameId: 'smash-monsters',
24
+ debug: true,
25
+ });
26
+
27
+ await sdk.init();
28
+
29
+ // Show a rewarded video ad
30
+ const success = await sdk.ads.showRewardedVideo();
31
+ if (success) {
32
+ await sdk.rewards.awardCoins();
33
+ }
34
+
35
+ // Start play-time tracking
36
+ sdk.analytics.startAutoTracking();
37
+
38
+ // Clean up when done
39
+ sdk.dispose();
40
+ ```
41
+
42
+ ### Babylon.js Integration
43
+
44
+ The Babylon.js plugin is auto-detected at runtime and binds your scene for automatic freeze/resume:
45
+
46
+ ```typescript
47
+ // After SDK is loaded and initialized via the loader:
48
+ sdk.babylon.bindScene(scene, engine);
49
+ // The SDK will automatically stop/start the render loop
50
+ // when the host app freezes/resumes the game
51
+ ```
52
+
53
+ ## Modes
54
+
55
+ The SDK operates in two modes, auto-detected at runtime:
56
+
57
+ | | Embedded | Standalone |
58
+ |---|---|---|
59
+ | **Context** | WebView inside PlayLoop app | Capacitor-wrapped APK |
60
+ | **Detection** | `window.ReactNativeWebView` exists | `window.Capacitor` exists |
61
+ | **Transport** | postMessage bridge | HTTP REST calls |
62
+ | **Auth** | Host manages identity | App Set ID via Capacitor plugin |
63
+ | **Ads** | Host shows ads | Native AppLovin MAX plugin |
64
+
65
+ You can override detection by passing `mode` explicitly:
66
+
67
+ ```typescript
68
+ const sdk = new PlayLoopSDK({
69
+ gameId: 'my-game',
70
+ mode: 'standalone',
71
+ apiBaseUrl: 'https://api.vbackendapi.com',
72
+ appLovin: {
73
+ sdkKey: 'YOUR_SDK_KEY',
74
+ rewardedAdUnitId: 'YOUR_REWARDED_AD_UNIT_ID',
75
+ interstitialAdUnitId: 'YOUR_INTERSTITIAL_AD_UNIT_ID',
76
+ bannerAdUnitId: 'YOUR_BANNER_AD_UNIT_ID',
77
+ },
78
+ ui: {
79
+ onClose: () => Capacitor.Plugins.App.exitApp(),
80
+ },
81
+ });
82
+ ```
83
+
84
+ ## Configuration
85
+
86
+ ```typescript
87
+ interface PlayLoopSDKConfig {
88
+ /** Unique game identifier */
89
+ gameId: string;
90
+ /** Force a specific mode instead of auto-detecting */
91
+ mode?: 'embedded' | 'standalone';
92
+ /** Backend API base URL (standalone mode only) */
93
+ apiBaseUrl?: string;
94
+ /** Request timeout in milliseconds (default: 10000) */
95
+ requestTimeoutMs?: number;
96
+ /** Enable debug logging (default: false) */
97
+ debug?: boolean;
98
+ /** AppLovin MAX config (required for standalone mode ads) */
99
+ appLovin?: {
100
+ /** SDK key from AppLovin dashboard (Account > General > Keys) */
101
+ sdkKey: string;
102
+ rewardedAdUnitId: string;
103
+ interstitialAdUnitId: string;
104
+ /** Optional - omit to disable banner ads */
105
+ bannerAdUnitId?: string;
106
+ };
107
+ /** UI config for standalone mode */
108
+ ui?: {
109
+ onClose?: () => void; // Close button callback
110
+ onMenuOpen?: () => void; // Menu button callback (optional)
111
+ showPlayLoopButton?: boolean; // Show floating panel button (default: false)
112
+ playLoopButton?: {
113
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
114
+ };
115
+ };
116
+ }
117
+ ```
118
+
119
+ > **Note**: The `appLovin` and `ui` configs are only used in standalone mode. In embedded mode, the host PlayLoop app handles ads and UI.
120
+
121
+ ## Modules
122
+
123
+ The SDK exposes its functionality through focused modules:
124
+
125
+ | Module | Accessor | Description |
126
+ |---|---|---|
127
+ | [Session](docs/session.md) | `sdk.session` | User identity, balance |
128
+ | [Rewards](docs/rewards.md) | `sdk.rewards` | Coin awarding |
129
+ | [Ads](docs/ads.md) | `sdk.ads` | Rewarded video, interstitial & banner ads |
130
+ | [Analytics](docs/analytics.md) | `sdk.analytics` | Play-time tracking, theme color |
131
+ | [Babylon](docs/babylon.md) | `sdk.babylon` | Babylon.js render loop freeze/resume |
132
+ | [Feature Flags](docs/feature-flags.md) | `sdk.featureFlags` | Remote config, feature gates, event logging (Statsig) |
133
+ | [UI](docs/ui.md) | `sdk.ui` | Floating panel, sponsored modal, offers, tier celebrations (standalone only) |
134
+
135
+ ## Authentication
136
+
137
+ **Embedded mode**: The host PlayLoop app handles all authentication. The SDK is always considered authenticated (`sdk.isAuthenticated === true`).
138
+
139
+ **Standalone mode**: The SDK reads the device's App Set ID via a Capacitor plugin and sends it as `X-Install-Token` header. The backend maps this to a known user if the user has previously opened the game in the PlayLoop app. If no mapping exists, `sdk.isAuthenticated` is `false` and coin-related features will fail gracefully.
140
+
141
+ ```typescript
142
+ if (sdk.isAuthenticated) {
143
+ const { coins } = await sdk.session.getBalance();
144
+ console.log(`User has ${coins} coins`);
145
+ } else {
146
+ console.log('User not linked - coin features unavailable');
147
+ }
148
+ ```
149
+
150
+ ## Lifecycle Events
151
+
152
+ ```typescript
153
+ // Called when the host app puts the game in the background
154
+ sdk.onFreeze(() => {
155
+ // Pause game logic, audio, etc.
156
+ });
157
+
158
+ // Called when the host app brings the game back
159
+ sdk.onResume(() => {
160
+ // Resume game logic, audio, etc.
161
+ });
162
+ ```
163
+
164
+ If you've bound a Babylon.js scene, the render loop is automatically stopped/started for you.
165
+
166
+ ## Error Handling
167
+
168
+ SDK errors include a `code` property for programmatic handling:
169
+
170
+ | Code | Meaning |
171
+ |---|---|
172
+ | `TIMEOUT` | Request timed out |
173
+ | `TRANSPORT_ERROR` | Communication layer failure |
174
+ | `AD_NOT_AVAILABLE` | No ad fill available |
175
+ | `NETWORK_ERROR` | HTTP request failed |
176
+ | `USER_NOT_LINKED` | Standalone user has no PlayLoop account mapping |
177
+
178
+ ```typescript
179
+ try {
180
+ await sdk.rewards.awardCoins();
181
+ } catch (err) {
182
+ if (err.code === 'USER_NOT_LINKED') {
183
+ // User hasn't opened this game in the PlayLoop app yet
184
+ }
185
+ }
186
+ ```
187
+
188
+ ## Cleanup
189
+
190
+ Always call `dispose()` when the SDK is no longer needed:
191
+
192
+ ```typescript
193
+ sdk.dispose();
194
+ ```
195
+
196
+ This stops analytics tracking, removes event listeners, aborts pending requests, and cleans up the Babylon.js plugin.
197
+
198
+ ## Top-Level Convenience APIs
199
+
200
+ ### `PlayLoopSDK.getWidgetHeight(): number` (static)
201
+
202
+ Computes the widget top-bar height from `window.innerWidth` alone — no DOM or `init()` required. Call it at script load time to offset your game canvas immediately:
203
+
204
+ ```typescript
205
+ const height = PlayLoopSDK.getWidgetHeight();
206
+ game.setTopOffset(height);
207
+ ```
208
+
209
+ Also available as a standalone function from both entry points:
210
+
211
+ ```typescript
212
+ // CDN / main entry
213
+ import { getWidgetHeight } from '@clocktone/game-sdk';
214
+
215
+ // Loader entry (works before CDN script loads)
216
+ import { getWidgetHeight } from '@clocktone/game-sdk/loader';
217
+ ```
218
+
219
+ ### `sdk.onWidgetReady(handler): () => void`
220
+
221
+ Fires after `init()` mounts the UI with the exact DOM-measured height. Use it to refine the initial estimate:
222
+
223
+ ```typescript
224
+ sdk.onWidgetReady(({ height }) => {
225
+ game.setTopOffset(height); // update with real value
226
+ });
227
+ ```
228
+
229
+ ### `sdk.getTopBarHeight(): number`
230
+
231
+ Returns the pixel height of the SDK's top bar. Before `init()` completes, falls back to `PlayLoopSDK.getWidgetHeight()` (computed estimate). After `init()`, returns the real DOM-measured value.
232
+
233
+ ```typescript
234
+ const offset = sdk.getTopBarHeight();
235
+ gameCanvas.style.marginTop = `${offset}px`;
236
+ ```
237
+
238
+ ### `sdk.adConfig: Readonly<AdConfig>`
239
+
240
+ Returns the current ad configuration with Statsig remote overrides merged on top of defaults. Shorthand for `sdk.featureFlags?.adConfig ?? DEFAULT_AD_CONFIG`.
241
+
242
+ ```typescript
243
+ const cooldown = sdk.adConfig.interstitialCooldownMs;
244
+ ```
245
+
246
+ See [`AdConfig`](docs/feature-flags.md#adconfig-readonlyadconfig) for all available fields.
247
+
248
+ ## Standalone UI
249
+
250
+ In standalone mode, when the user is authenticated, the SDK renders a **floating PlayLoopButton** that opens a panel with rewards info, cashout flow, and games. It also provides **sponsored video modals** for interstitials and **bonus/booster offer modals**.
251
+
252
+ See the full [UI Module documentation](docs/ui.md) for all methods, components, and behavior details.
253
+
254
+ ### PlayLoopButton Panel
255
+
256
+ Shown automatically after `init()` when `showPlayLoopButton: true`. The panel has three content areas:
257
+
258
+ - **Rewards tab (main screen)** — coins pill, tier card with progress bar, "Convert coins to real money" CTA, partners section
259
+ - **Cashout screen (sub-screen)** — countdown timer, earnings display, cashout button, partners. Accessed via the CTA button; back button returns to main screen
260
+ - **Games tab** — lists available games
261
+
262
+ The panel trigger row displays the current coin balance with earn animations, inline countdown timer, and offer countdown badges.
263
+
264
+ ### Sponsored Interstitial
265
+
266
+ ```typescript
267
+ // Show a 3-second countdown modal, then the interstitial ad
268
+ const success = await sdk.ui?.showSponsoredInterstitial();
269
+ ```
270
+
271
+ Shows an animated countdown, then the ad, then a success/failure state. Coins are automatically awarded on success with the current boost multiplier applied.
272
+
273
+ ### Bonus & Booster Offers
274
+
275
+ The SDK periodically shows bonus coin offers and 2x boost offers via the `OfferScheduler`. When an offer triggers, a 20-second countdown badge appears on the panel trigger. If the user taps within the countdown, the corresponding modal opens (rewarded ad required to claim).
276
+
277
+ ### Controlling the UI
278
+
279
+ ```typescript
280
+ sdk.ui?.updateCoins(newBalance); // Manually update displayed coins
281
+ sdk.ui?.updateThemeColor('#1a1a2e'); // Change panel background
282
+ await sdk.ui?.refreshWallet(); // Re-fetch wallet data from backend
283
+ ```
284
+
285
+ > `sdk.ui` is `null` in embedded mode or when the user is not authenticated.
286
+
287
+ ## Testing on Device
288
+
289
+ The SDK ships with two Android test targets for on-device testing. Both reference the SDK as a local `file:` dependency, so they always pick up the latest build.
290
+
291
+ | Target | Directory | App ID | Description |
292
+ |--------|-----------|--------|-------------|
293
+ | **test-app** | `debug/` | `com.playloop.monster` | Minimal SDK debug harness |
294
+ | **test-game** | `debug/test-game/block-blast/Block-Blast/` | `com.playloop.monster` | Block Blast game with full SDK integration |
295
+
296
+ ### Prerequisites
297
+
298
+ - Android device connected via USB with ADB debugging enabled
299
+ - Java 21 available (`JAVA_HOME=$(/usr/libexec/java_home -v 21)`)
300
+
301
+ ### Quick Start
302
+
303
+ ```bash
304
+ # 1. Build the SDK
305
+ cd cashable-game-sdk && npm run build
306
+
307
+ # 2a. Deploy test-app (convenience script)
308
+ cd debug
309
+ JAVA_HOME=$(/usr/libexec/java_home -v 21) npm run run:android
310
+
311
+ # 2b. Deploy test-game (Block Blast)
312
+ cd debug/test-game/block-blast/Block-Blast
313
+ npm run build
314
+ npx cap sync android
315
+ cd android && JAVA_HOME=$(/usr/libexec/java_home -v 21) ./gradlew assembleDebug && cd ..
316
+ adb install -r android/app/build/outputs/apk/debug/app-debug.apk
317
+ ```
318
+
319
+ ### APK Location
320
+
321
+ After building, the debug APK is at:
322
+ - **test-app**: `debug/android/app/build/outputs/apk/debug/app-debug.apk`
323
+ - **test-game**: `debug/test-game/block-blast/Block-Blast/android/app/build/outputs/apk/debug/app-debug.apk`
324
+
325
+ ### Claude Code Skill
326
+
327
+ The `sdk-android-test` skill automates the full build-and-deploy flow. Invoke it with `/sdk-android-test` in Claude Code to build the SDK and deploy to a connected device.
328
+
329
+ ## Versioning
330
+
331
+ This package has two independent version tracks:
332
+
333
+ | Version | File | What it tracks | When to bump |
334
+ |---------|------|---------------|--------------|
335
+ | **npm** (loader + types) | `package.json` `"version"` | Loader code, type declarations, native plugins | Loader logic or exported types change |
336
+ | **CDN** (SDK runtime) | `cdn-version.json` `"version"` | Full SDK IIFE bundle served from CDN | SDK runtime code changes |
337
+
338
+ The npm package ships only the **loader** (~1KB bootstrap) and **type declarations**. The full SDK runtime is built separately and deployed to CDN (`vbackendapi.com/sdk/v<CDN_VERSION>/playloop-sdk.js`). At runtime, the loader fetches the SDK version from the backend and loads the IIFE bundle from CDN.
339
+
340
+ A CDN deploy does not require an npm publish, and vice versa.
341
+
342
+ ## Publishing
343
+
344
+ ### Automated (CI)
345
+
346
+ Push a tag matching `sdk-v*` to trigger the GitHub Actions workflow (`.github/workflows/publish-sdk.yml`):
347
+
348
+ ```bash
349
+ # Bump version in package.json and/or cdn-version.json first
350
+ cd cashable-game-sdk
351
+ npm version patch # or minor/major — updates package.json version
352
+
353
+ # Tag and push
354
+ git add -A && git commit -m "chore(sdk): bump to v0.8.6"
355
+ git tag sdk-v0.8.6
356
+ git push origin main --tags
357
+ ```
358
+
359
+ The workflow will:
360
+ 1. Build the loader and CDN bundle
361
+ 2. Publish the npm package (`@clocktone/game-sdk`) to the public registry
362
+ 3. Upload the CDN bundle to R2 (`playloop-media/sdk/v<CDN_VERSION>/playloop-sdk.js`)
363
+
364
+ **Required GitHub secrets:** `NPM_TOKEN`, `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`
365
+
366
+ ### Manual
367
+
368
+ ```bash
369
+ cd cashable-game-sdk
370
+ npm run build
371
+ npm publish --access public # publish loader to npm
372
+ CLOUDFLARE_ACCOUNT_ID=<id> wrangler r2 object put \
373
+ playloop-media/sdk/v0.7.0/playloop-sdk.js \
374
+ --file dist/cdn/playloop-sdk.js \
375
+ --content-type "application/javascript" \
376
+ --remote # upload CDN bundle to R2
377
+ ```
378
+
379
+ ### npm package
380
+
381
+ - **Registry:** https://www.npmjs.com/package/@clocktone/game-sdk
382
+ - **Contents:** Loader (~5KB), TypeScript types, Android Capacitor plugins, docs
383
+ - **No secrets or business logic** — all config is passed at runtime
384
+
385
+ ### R2 CDN bundle
386
+
387
+ - **Bucket:** `playloop-media` (on the Mirko@playloop Cloudflare account)
388
+ - **Path:** `sdk/v<VERSION>/playloop-sdk.js`
389
+ - **Fallback URL:** `https://getplayloop.com/sdk/v0.8.5/playloop-sdk.js`
390
+
391
+ ## API Reference
392
+
393
+ - [CDN Loader](docs/loader.md)
394
+ - [Game Integration Guide](docs/game-integration-guide.md)
395
+ - [Session Module](docs/session.md)
396
+ - [Rewards Module](docs/rewards.md)
397
+ - [Ads Module](docs/ads.md)
398
+ - [Analytics Module](docs/analytics.md)
399
+ - [Babylon.js Plugin](docs/babylon.md)
400
+ - [Feature Flags Module](docs/feature-flags.md)
401
+ - [UI Module](docs/ui.md)
402
+ - [Wire Protocol](docs/wire-protocol.md) (for host app implementors)
@@ -0,0 +1,69 @@
1
+ ext {
2
+ junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
3
+ androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
4
+ androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
5
+ androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
6
+ }
7
+
8
+ buildscript {
9
+ repositories {
10
+ google()
11
+ mavenCentral()
12
+ }
13
+ dependencies {
14
+ classpath 'com.android.tools.build:gradle:8.7.0'
15
+ classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0'
16
+ }
17
+ }
18
+
19
+ apply plugin: 'com.android.library'
20
+ apply plugin: 'kotlin-android'
21
+
22
+ android {
23
+ namespace "com.playloop.plugins"
24
+ compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
25
+
26
+ defaultConfig {
27
+ minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 22
28
+ targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
29
+ versionCode 1
30
+ versionName "0.1.0"
31
+ }
32
+
33
+ buildTypes {
34
+ release {
35
+ minifyEnabled false
36
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
37
+ }
38
+ }
39
+
40
+ compileOptions {
41
+ sourceCompatibility JavaVersion.VERSION_17
42
+ targetCompatibility JavaVersion.VERSION_17
43
+ }
44
+
45
+ kotlinOptions {
46
+ jvmTarget = '17'
47
+ }
48
+ }
49
+
50
+ repositories {
51
+ google()
52
+ mavenCentral()
53
+ }
54
+
55
+ dependencies {
56
+ implementation project(':capacitor-android')
57
+ implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
58
+
59
+ // AppLovin MAX SDK
60
+ implementation 'com.applovin:applovin-sdk:13.5.1'
61
+
62
+ // Google Play Services App Set ID
63
+ implementation 'com.google.android.gms:play-services-appset:16.1.0'
64
+
65
+ // Testing
66
+ testImplementation "junit:junit:$junitVersion"
67
+ androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
68
+ androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
69
+ }