@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/docs/ui.md ADDED
@@ -0,0 +1,248 @@
1
+ # UI Module
2
+
3
+ Standalone-mode UI overlay: top bar, sponsored interstitials, bonus/booster offers, tier celebrations, and coin toasts.
4
+
5
+ **Accessor**: `cashable.ui` (nullable — `null` in embedded mode or when the user is not authenticated)
6
+
7
+ ## Overview
8
+
9
+ The UI module renders a DOM overlay on top of the game canvas in standalone mode. It provides:
10
+
11
+ - **PlayLoopButton** — floating button that opens a panel with three content areas:
12
+ - **Rewards tab (main screen)** — coins pill, tier card with progress bar, "Convert coins to real money" CTA button, partners section
13
+ - **Cashout screen (sub-screen)** — countdown timer, earnings display, cashout button (with wait modal), partners. Accessed via the CTA button; back button returns to main screen
14
+ - **Games tab** — lists available games
15
+ - **SponsoredModal** — animated countdown before interstitial ads
16
+ - **BonusOfferModal** — periodic bonus coin offers (watch ad to claim)
17
+ - **BoostOfferModal** — 2x coin multiplier activation (watch ad to activate)
18
+ - **TierCelebrationModal** — tier-up celebrations with optional ad-doubled rewards
19
+ - **CoinToast** — floating "+N coins" notifications
20
+
21
+ The module is initialized automatically during `cashable.init()` when running in standalone mode with an authenticated user. The `showPlayLoopButton` config option (default: `false`) controls whether the floating button is rendered.
22
+
23
+ ### PlayLoopButton Config
24
+
25
+ ```typescript
26
+ ui: {
27
+ showPlayLoopButton: true,
28
+ cashableButton: {
29
+ position: 'top-left', // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
30
+ autoPeek: false, // Auto-open panel briefly on first wallet data (default: true)
31
+ },
32
+ onClose: () => { /* handle close */ },
33
+ onMenuOpen: () => { /* handle menu */ },
34
+ }
35
+ ```
36
+
37
+ ## Methods
38
+
39
+ ### `showSponsoredInterstitial(): Promise<boolean>`
40
+
41
+ Shows a sponsored interstitial ad flow:
42
+
43
+ 1. Reserves a reward amount from the backend (`rewards.reserveSponsoredVideo`)
44
+ 2. Shows a 3-second animated countdown modal
45
+ 3. Plays the interstitial ad
46
+ 4. On success: claims the reward (`rewards.claimSponsoredVideo`), updates coin balance, shows toast
47
+ 5. On failure: shows "Video unavailable" state
48
+
49
+ Returns `true` if the ad was shown and the reward was claimed. Returns `false` if the interstitial is on cooldown, the reservation fails, or the ad fails.
50
+
51
+ ```typescript
52
+ const success = await cashable.ui?.showSponsoredInterstitial();
53
+ if (success) {
54
+ console.log('User earned coins from sponsored video');
55
+ }
56
+ ```
57
+
58
+ ### `showCoinAward(newTotalCoins): void`
59
+
60
+ Calculates the delta from the current balance, updates the displayed coin count, and shows a coin toast for the difference.
61
+
62
+ - `newTotalCoins: number` — the user's new total coin balance
63
+
64
+ ```typescript
65
+ cashable.ui?.showCoinAward(1500); // If user had 1400, shows "+100 coins" toast
66
+ ```
67
+
68
+ ### `showCoinToast(amount, note?): void`
69
+
70
+ Shows a floating coin toast notification. Automatically appends "Includes 2x boost" when a boost is active and no custom note is provided.
71
+
72
+ - `amount: number` — coins earned
73
+ - `note?: string` — optional note text
74
+
75
+ ```typescript
76
+ cashable.ui?.showCoinToast(50, 'Bonus reward!');
77
+ ```
78
+
79
+ ### `updateCoins(coins): void`
80
+
81
+ Updates the displayed coin count in the panel and triggers a tier advancement check.
82
+
83
+ - `coins: number` — new total coin balance
84
+
85
+ ```typescript
86
+ cashable.ui?.updateCoins(2000);
87
+ ```
88
+
89
+ ### `updateThemeColor(color): void`
90
+
91
+ Changes the panel background color. The panel automatically adjusts text/icon colors for contrast.
92
+
93
+ - `color: string` — CSS color value (e.g. `'#1a1a2e'`)
94
+
95
+ ```typescript
96
+ cashable.ui?.updateThemeColor('#1a1a2e');
97
+ ```
98
+
99
+ ### `refreshWallet(): Promise<void>`
100
+
101
+ Re-fetches wallet data from the backend (`session.getWallet`) and updates all UI elements (coins, tier progress).
102
+
103
+ ```typescript
104
+ await cashable.ui?.refreshWallet();
105
+ ```
106
+
107
+ ### `getTopBarHeight(): number`
108
+
109
+ Returns the current pixel height of the panel trigger row. Useful for positioning game content below the button.
110
+
111
+ ```typescript
112
+ const offset = cashable.ui?.getTopBarHeight() ?? 0;
113
+ gameCanvas.style.marginTop = `${offset}px`;
114
+ ```
115
+
116
+ Also available as a top-level convenience method: `cashable.getTopBarHeight()`.
117
+
118
+ ### `setVisible(visible): void`
119
+
120
+ Shows or hides the UI overlay. Called automatically on `lifecycle.freeze` (hide) and `lifecycle.resume` (show). Also pauses/resumes the offer scheduler.
121
+
122
+ - `visible: boolean`
123
+
124
+ ```typescript
125
+ cashable.ui?.setVisible(false); // Hide during cutscene
126
+ cashable.ui?.setVisible(true); // Show again
127
+ ```
128
+
129
+ ### `triggerBonusOffer(): Promise<void>`
130
+
131
+ **Debug method.** Immediately triggers a bonus offer modal by fetching one from the backend, bypassing the normal offer scheduler cadence.
132
+
133
+ ```typescript
134
+ await cashable.ui?.triggerBonusOffer();
135
+ ```
136
+
137
+ ### `triggerBoosterOffer(): Promise<void>`
138
+
139
+ **Debug method.** Immediately triggers a booster offer modal, bypassing the normal offer scheduler cadence.
140
+
141
+ ```typescript
142
+ await cashable.ui?.triggerBoosterOffer();
143
+ ```
144
+
145
+ ### `dispose(): void`
146
+
147
+ Removes all DOM elements, clears timers, and disposes child components. Called automatically by `cashable.dispose()`.
148
+
149
+ ## Components
150
+
151
+ ### PlayLoopButton (Panel)
152
+
153
+ Floating button that opens an expandable panel with tabbed navigation:
154
+
155
+ - **Trigger row** — coin balance display with earn animation, inline countdown timer, offer countdown badge
156
+ - **Rewards tab (main screen):**
157
+ - Coins pill with "Your Coins" header
158
+ - Tier card with progress bar, tier name, and "Fill the bar to maximize your earnings" hint
159
+ - "Convert coins to real money" CTA button (navigates to cashout screen)
160
+ - Partners section ("Get paid through" with payment logos)
161
+ - **Cashout screen (sub-screen):**
162
+ - Back button to return to main screen
163
+ - Title: "Coins → Real Money" (no earnings) or "Money Ready!" (has earnings)
164
+ - Subtitle with context-sensitive messaging
165
+ - Countdown timer (hidden when earnings ready)
166
+ - Earnings display (shown when earnings ready)
167
+ - Cashout CTA button with wait modal logic
168
+ - Partners section
169
+ - **Games tab** — lists available games with play buttons
170
+ - **Close button** — calls `config.onClose` callback
171
+
172
+ The panel auto-resets to the rewards main screen when closed.
173
+
174
+ ### SponsoredModal
175
+
176
+ Fullscreen modal with phases:
177
+
178
+ 1. **Countdown** (3 seconds) — animated SVG ring, reward badge shows "+{amount} coins"
179
+ 2. **Playing** — "Loading video..." while the ad shows
180
+ 3. **Success** — green checkmark + "Coins Earned!" (auto-dismiss after 2s)
181
+ 4. **Failed** — "Video unavailable" (auto-dismiss after 2s)
182
+
183
+ ### BonusOfferModal
184
+
185
+ Modal showing a periodic bonus coin offer. Displays the reward amount (doubled if boost is active). User taps "Claim" to watch a rewarded ad, then coins are awarded via `rewards.awardBonus`.
186
+
187
+ ### BoostOfferModal
188
+
189
+ Modal offering a 2x coin multiplier for 3 minutes. User taps "Activate" to watch a rewarded ad, then the boost is activated.
190
+
191
+ ### TierCelebrationModal
192
+
193
+ Shown when the user advances to a new tier. Offers two claim options:
194
+
195
+ - **Claim** — claim the tier reward directly (`tier.claimCelebration` with `doubled: false`)
196
+ - **Watch Ad & Double** — watch a rewarded ad, then claim doubled reward (`tier.claimCelebration` with `doubled: true`)
197
+
198
+ ### CoinToast
199
+
200
+ Floating toast notification showing "+N coins" with an optional note line.
201
+
202
+ ## Types
203
+
204
+ ### `ActiveOffer`
205
+
206
+ Represents a bonus or booster offer in progress:
207
+
208
+ ```typescript
209
+ interface ActiveOffer {
210
+ offerId: string;
211
+ amount?: number; // Coin reward amount (bonus offers only)
212
+ type: 'bonus' | 'booster';
213
+ }
214
+ ```
215
+
216
+ ## Tier System
217
+
218
+ Tier progress is tracked via `WalletData.tier`:
219
+
220
+ - `currentTier` — current tier number (0-based)
221
+ - `currentTierName` — display name (e.g. "Silver")
222
+ - `nextTier` — next tier info with `name` and `target` (coins needed)
223
+
224
+ When `updateCoins()` is called, the module fetches fresh wallet data and compares the new tier against the last known tier. If the tier increased, a `TierCelebrationModal` is shown.
225
+
226
+ ## Offer System
227
+
228
+ The `OfferScheduler` alternates between bonus and booster offers on configurable cadences:
229
+
230
+ - **Bonus offer cadence**: `adConfig.gamesBonusOfferCadenceMs` (default: 60s)
231
+ - **Booster offer cadence**: `adConfig.gamesCoinBoosterCadenceMs` (default: 60s)
232
+
233
+ When an offer triggers:
234
+ 1. The scheduler fetches a bonus offer from the backend (for bonus type)
235
+ 2. A countdown badge appears on the panel trigger (20 seconds)
236
+ 3. If the user taps within the countdown, the corresponding modal opens
237
+ 4. If the countdown expires, the offer is dismissed
238
+
239
+ The scheduler pauses when the UI is hidden and resumes when visible.
240
+
241
+ ## Boost Mechanics
242
+
243
+ - **Duration**: 3 minutes (180,000ms)
244
+ - **Multiplier**: 2x on all coin awards
245
+ - **Persistence**: `localStorage` key `cashable:boost:endsAt` stores the expiry timestamp
246
+ - **Survival**: Boost survives page reloads within the 3-minute window
247
+ - **Countdown**: Panel trigger shows remaining time (updates every second)
248
+ - **Integration**: The `RewardsModule` reads boost state from localStorage to apply the multiplier
@@ -0,0 +1,194 @@
1
+ # Wire Protocol
2
+
3
+ Reference for host app implementors (React Native WebView bridge). Game developers using the SDK don't need to know about this.
4
+
5
+ ## Overview
6
+
7
+ Communication between the SDK (running inside a WebView) and the host PlayLoop app uses `postMessage` with JSON payloads. There are four message types:
8
+
9
+ | Type | Direction | Purpose |
10
+ |---|---|---|
11
+ | `cashable:request` | SDK -> Host | SDK requests an action |
12
+ | `cashable:response` | Host -> SDK | Host responds to a request |
13
+ | `cashable:event` | Host -> SDK | Host pushes an event |
14
+ | `cashable:ready` | SDK -> Host | SDK signals initialization complete |
15
+
16
+ ## Message Formats
17
+
18
+ ### Request (SDK -> Host)
19
+
20
+ ```json
21
+ {
22
+ "type": "cashable:request",
23
+ "action": "ads.showRewarded",
24
+ "requestId": "req_1707000000000_1",
25
+ "payload": {}
26
+ }
27
+ ```
28
+
29
+ ### Response (Host -> SDK)
30
+
31
+ ```json
32
+ {
33
+ "type": "cashable:response",
34
+ "requestId": "req_1707000000000_1",
35
+ "data": { "success": true }
36
+ }
37
+ ```
38
+
39
+ Error response:
40
+
41
+ ```json
42
+ {
43
+ "type": "cashable:response",
44
+ "requestId": "req_1707000000000_1",
45
+ "error": {
46
+ "code": "AD_NOT_AVAILABLE",
47
+ "message": "No ad fill available"
48
+ }
49
+ }
50
+ ```
51
+
52
+ ### Event (Host -> SDK)
53
+
54
+ ```json
55
+ {
56
+ "type": "cashable:event",
57
+ "event": "lifecycle.freeze",
58
+ "data": null
59
+ }
60
+ ```
61
+
62
+ ### Ready (SDK -> Host)
63
+
64
+ ```json
65
+ {
66
+ "type": "cashable:ready",
67
+ "version": "0.6.0"
68
+ }
69
+ ```
70
+
71
+ ## Actions
72
+
73
+ Actions are sent from the SDK to the host via `cashable:request` messages.
74
+
75
+ | Action | Payload | Expected Response |
76
+ |---|---|---|
77
+ | `ads.showRewarded` | `{}` | `{ success: boolean }` |
78
+ | `ads.showInterstitial` | `{}` | `{ success: boolean }` |
79
+ | `ads.showBanner` | `{ position?: 'top' \| 'bottom' }` | `{ success: boolean }` |
80
+ | `ads.hideBanner` | `{}` | `{}` (ack) |
81
+ | `ads.destroyBanner` | `{}` | `{}` (ack) |
82
+ | `rewards.awardCoins` | `{ multiplier?: number }` | `{ coins: number }` |
83
+ | `rewards.bonusOffer` | `{}` | `{ offered: boolean }` |
84
+ | `rewards.awardBonus` | `{}` | `{ coins: number }` |
85
+ | `rewards.reserveSponsoredVideo` | `{}` | `{ offerId: string, amount: number }` |
86
+ | `rewards.claimSponsoredVideo` | `{ offerId: string, amount: number }` | `{ coinsAwarded: number, totalCoins: number }` |
87
+ | `session.getBalance` | - | `{ coins: number }` |
88
+ | `session.getWallet` | - | `WalletData` (see [Types](#types)) |
89
+ | `games.init` | `{ gameId: string, mode: string }` | `InitResponse` (see [Types](#types)) |
90
+ | `analytics.playTick` | `{ seconds: number }` | `{}` (ack) |
91
+ | `analytics.themeColor` | `{ color: string }` | `{}` (ack) |
92
+ | `lifecycle.ready` | `{ version: string }` | `{}` (ack) |
93
+ | `lifecycle.progress` | `{ percent: number }` | `{}` (ack) |
94
+ | `tier.claimCelebration` | `{ tierNumber: number, doubled: boolean }` | `{ success: boolean, coinsAwarded: number, totalCoins: number }` |
95
+
96
+ ## Events
97
+
98
+ Events are pushed from the host to the SDK via `cashable:event` messages.
99
+
100
+ | Event | Data | Description |
101
+ |---|---|---|
102
+ | `lifecycle.freeze` | - | Host is putting the game in the background |
103
+ | `lifecycle.resume` | - | Host is bringing the game back to the foreground |
104
+ | `rewards.coinsAwarded` | `{ coins: number }` | Host awarded coins (e.g., bonus) |
105
+ | `ui.topBarHeight` | `{ height: number }` | Host reports top bar pixel height for game layout offset |
106
+
107
+ ## Error Codes
108
+
109
+ | Code | Meaning |
110
+ |---|---|
111
+ | `TIMEOUT` | Request timed out (default: 10s) |
112
+ | `TRANSPORT_ERROR` | Communication failure |
113
+ | `AD_NOT_AVAILABLE` | No ad fill |
114
+ | `NETWORK_ERROR` | HTTP request failed |
115
+ | `USER_NOT_LINKED` | User not found in backend |
116
+
117
+ ## Host Implementation
118
+
119
+ The host (React Native) should:
120
+
121
+ 1. Listen for `onMessage` events from the WebView
122
+ 2. Parse JSON, check for `type: 'cashable:request'`
123
+ 3. Dispatch by `action`
124
+ 4. Send responses back via `webViewRef.current.injectJavaScript()`:
125
+
126
+ ```javascript
127
+ // In React Native
128
+ const sendResponse = (requestId, data, error) => {
129
+ const message = JSON.stringify({
130
+ type: 'cashable:response',
131
+ requestId,
132
+ data: error ? undefined : data,
133
+ error: error ? { code: error.code, message: error.message } : undefined,
134
+ });
135
+ webViewRef.current?.injectJavaScript(
136
+ `window.dispatchEvent(new MessageEvent('message', { data: '${message}' }));true;`
137
+ );
138
+ };
139
+
140
+ const sendEvent = (event, data) => {
141
+ const message = JSON.stringify({
142
+ type: 'cashable:event',
143
+ event,
144
+ data,
145
+ });
146
+ webViewRef.current?.injectJavaScript(
147
+ `window.dispatchEvent(new MessageEvent('message', { data: '${message}' }));true;`
148
+ );
149
+ };
150
+ ```
151
+
152
+ See `frontend/src/screens/PlayGamesScreen/hooks/usePlayLoopGameBridge.ts` for the full reference implementation.
153
+
154
+ ## Types
155
+
156
+ ### `WalletData`
157
+
158
+ Returned by `session.getWallet`:
159
+
160
+ ```typescript
161
+ interface WalletData {
162
+ coins: number;
163
+ tier: {
164
+ currentTier: number;
165
+ currentTierName: string;
166
+ nextTier?: { name: string; target: number };
167
+ };
168
+ tierDefinitions: TierInfo[];
169
+ coinGoal: number;
170
+ coinsEarnedThisPeriod: number;
171
+ earnings: number;
172
+ cashoutPeriodEndsAt: number;
173
+ }
174
+
175
+ interface TierInfo {
176
+ tier: number;
177
+ name: string;
178
+ target: number;
179
+ }
180
+ ```
181
+
182
+ ### `InitResponse`
183
+
184
+ Returned by `games.init`:
185
+
186
+ ```typescript
187
+ interface InitResponse {
188
+ userId: string;
189
+ featureFlags?: {
190
+ clientKey: string;
191
+ bootstrapValues?: string; // JSON-serialized Statsig bootstrap
192
+ };
193
+ }
194
+ ```
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@clocktone/game-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Cashable Unified Game SDK for embedded (WebView) and standalone (Capacitor) games",
5
+ "main": "dist/loader/cjs/index.js",
6
+ "module": "dist/loader/esm/index.js",
7
+ "types": "dist/types/loader/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/types/index.d.ts",
11
+ "import": "./dist/loader/esm/index.js",
12
+ "require": "./dist/loader/cjs/index.js"
13
+ },
14
+ "./loader": {
15
+ "types": "./dist/types/loader/index.d.ts",
16
+ "import": "./dist/loader/esm/index.js",
17
+ "require": "./dist/loader/cjs/index.js"
18
+ }
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "files": [
24
+ "dist/loader",
25
+ "dist/types",
26
+ "android/src",
27
+ "android/build.gradle",
28
+ "docs"
29
+ ],
30
+ "capacitor": {
31
+ "android": {
32
+ "src": "android"
33
+ }
34
+ },
35
+ "scripts": {
36
+ "build": "npm run build:loader && npm run build:cdn",
37
+ "build:loader": "rollup -c rollup.loader.config.mjs",
38
+ "build:cdn": "rollup -c rollup.cdn.config.mjs",
39
+ "dev": "rollup -c rollup.cdn.config.mjs -w",
40
+ "preview": "vite --config preview/vite.config.ts",
41
+ "lint": "tsc --noEmit",
42
+ "test": "vitest run",
43
+ "test:watch": "vitest"
44
+ },
45
+ "peerDependencies": {
46
+ "@babylonjs/core": ">=6.0.0",
47
+ "@capacitor/core": ">=5.0.0",
48
+ "@statsig/js-client": ">=3.0.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "@babylonjs/core": {
52
+ "optional": true
53
+ },
54
+ "@capacitor/core": {
55
+ "optional": true
56
+ },
57
+ "@statsig/js-client": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "devDependencies": {
62
+ "@capacitor/core": "^8.1.0",
63
+ "@rollup/plugin-commonjs": "^29.0.0",
64
+ "@rollup/plugin-image": "^3.0.3",
65
+ "@rollup/plugin-node-resolve": "^16.0.0",
66
+ "@rollup/plugin-replace": "^6.0.0",
67
+ "@rollup/plugin-terser": "^0.4.4",
68
+ "@rollup/plugin-typescript": "^12.1.0",
69
+ "@statsig/js-client": "^3.31.2",
70
+ "@vitest/runner": "^4.1.5",
71
+ "happy-dom": "^20.9.0",
72
+ "jsdom": "^27.0.1",
73
+ "rollup": "^4.9.0",
74
+ "rollup-plugin-dts": "^6.1.0",
75
+ "tslib": "^2.8.0",
76
+ "typescript": "~5.8.0",
77
+ "vite": "^7.3.1",
78
+ "vitest": "^3.2.0"
79
+ },
80
+ "license": "UNLICENSED"
81
+ }