@hyve-sdk/js 1.5.0 → 2.1.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 +169 -658
- package/dist/index.d.mts +164 -137
- package/dist/index.d.ts +164 -137
- package/dist/index.js +483 -538
- package/dist/index.mjs +478 -530
- package/dist/react.d.mts +453 -0
- package/dist/react.d.ts +453 -0
- package/dist/react.js +2173 -0
- package/dist/react.mjs +2151 -0
- package/package.json +30 -15
package/README.md
CHANGED
|
@@ -1,329 +1,139 @@
|
|
|
1
1
|
# @hyve-sdk/js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The core SDK for integrating with the Hyve platform, providing authentication, telemetry, persistent storage, billing, ads, and native bridge capabilities for web games.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Setup
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
bun add @hyve-sdk/js
|
|
9
|
-
# or
|
|
10
|
-
npm install @hyve-sdk/js
|
|
11
|
-
# or
|
|
12
|
-
pnpm add @hyve-sdk/js
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Features
|
|
16
|
-
|
|
17
|
-
- **Web3 Authentication**: EVM signature validation and verification
|
|
18
|
-
- **Modern & Legacy Token Support**: Dual authentication token formats
|
|
19
|
-
- **JWT Authentication**: Support for JWT tokens passed via URL parameters
|
|
20
|
-
- **API Integration**: Authenticated API calls with JWT token support
|
|
21
|
-
- **Inventory Management**: Built-in methods for user inventory operations
|
|
22
|
-
- **Telemetry Tracking**: Session-based analytics and event tracking
|
|
23
|
-
- **Billing Integration**: Unified billing for web (Stripe) and native (In-App Purchases) platforms
|
|
24
|
-
- **Ads Integration**: Google H5 Games Ads support (disabled by default)
|
|
25
|
-
- **Native Bridge**: Type-safe communication with React Native WebView apps
|
|
26
|
-
- **Logger**: Environment-aware logging system with configurable log levels
|
|
27
|
-
- **Security Utilities**: Domain validation and referrer checking
|
|
28
|
-
- **URL Parameter Parsing**: Easy extraction of authentication parameters
|
|
29
|
-
- **UUID Generation**: Built-in UUID v4 generation
|
|
30
|
-
|
|
31
|
-
## Core Components
|
|
32
|
-
|
|
33
|
-
### HyveClient
|
|
34
|
-
Main client class for SDK operations including authentication, telemetry, and API calls.
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
import { HyveClient } from "@hyve-sdk/js";
|
|
7
|
+
Load the SDK via script tag — no npm install required.
|
|
38
8
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const client = new HyveClient();
|
|
42
|
-
|
|
43
|
-
// Authenticate from URL parameters
|
|
44
|
-
// Extracts: hyve-token/signature, hyve-access (JWT), game-id
|
|
45
|
-
const authenticated = await client.authenticateFromUrl();
|
|
46
|
-
if (authenticated) {
|
|
47
|
-
console.log('User ID:', client.getUserId());
|
|
48
|
-
console.log('Session ID:', client.getSessionId());
|
|
49
|
-
console.log('Has JWT:', client.hasJwtToken());
|
|
50
|
-
console.log('Game ID:', client.getGameId());
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
**Environment Detection:**
|
|
55
|
-
The SDK automatically detects whether to use dev or prod environment by checking the parent page URL:
|
|
56
|
-
- **Dev**: `marvin.dev.hyve.gg` or `dev.hyve.gg`
|
|
57
|
-
- **Prod**: `marvin.hyve.gg` or `hyve.gg`
|
|
58
|
-
|
|
59
|
-
You can optionally override this for testing:
|
|
60
|
-
```typescript
|
|
61
|
-
// Only use for local testing - NOT for production code
|
|
62
|
-
const client = new HyveClient({
|
|
63
|
-
isDev: true, // Force dev mode for testing
|
|
64
|
-
apiBaseUrl: 'https://...' // Optional custom API URL
|
|
65
|
-
});
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### Authentication Utilities
|
|
69
|
-
|
|
70
|
-
#### Parse URL Parameters
|
|
71
|
-
Extract authentication and game parameters from URL:
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
import { parseUrlParams } from "@hyve-sdk/js";
|
|
75
|
-
|
|
76
|
-
const params = parseUrlParams(window.location.search);
|
|
77
|
-
// Returns:
|
|
78
|
-
// {
|
|
79
|
-
// signature: string,
|
|
80
|
-
// message: string,
|
|
81
|
-
// gameStartTab: string,
|
|
82
|
-
// hyveToken: string,
|
|
83
|
-
// platform: string,
|
|
84
|
-
// hyveAccess: string, // JWT token
|
|
85
|
-
// gameId: string // Game identifier
|
|
86
|
-
// }
|
|
9
|
+
```html
|
|
10
|
+
<script src="https://package.hyve.gg/hyve-sdk.global.js"></script>
|
|
87
11
|
```
|
|
88
12
|
|
|
89
|
-
|
|
90
|
-
Unified verification supporting both modern and legacy tokens:
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
import { verifyAuthentication } from "@hyve-sdk/js";
|
|
13
|
+
All exports are available on `window.HyveSDK`:
|
|
94
14
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
signature: params.signature, // Legacy format
|
|
98
|
-
message: params.message // Legacy format
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
if (result.isValid) {
|
|
102
|
-
console.log('Authenticated address:', result.address);
|
|
103
|
-
console.log('Auth method used:', result.method); // 'modern' or 'legacy'
|
|
104
|
-
}
|
|
15
|
+
```js
|
|
16
|
+
const { HyveClient, NativeBridge, NativeMessageType } = window.HyveSDK;
|
|
105
17
|
```
|
|
106
18
|
|
|
107
|
-
|
|
108
|
-
Verify modern authentication tokens with expiration:
|
|
19
|
+
## Authentication
|
|
109
20
|
|
|
110
|
-
|
|
111
|
-
import { verifyHyveToken } from "@hyve-sdk/js";
|
|
21
|
+
Authenticate using the `hyve-access` JWT passed in the URL:
|
|
112
22
|
|
|
113
|
-
// Token format: signature.address.randomBase64.timestamp
|
|
114
|
-
const address = verifyHyveToken(hyveToken, 600); // 600 seconds max age
|
|
115
|
-
if (address) {
|
|
116
|
-
console.log('Valid token for address:', address);
|
|
117
|
-
}
|
|
118
23
|
```
|
|
119
|
-
|
|
120
|
-
#### Legacy Token Verification
|
|
121
|
-
Verify legacy signed messages with metadata:
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
import { handleVerifyMessage, validateSignature } from "@hyve-sdk/js";
|
|
125
|
-
|
|
126
|
-
// Method 1: Verify message with embedded metadata
|
|
127
|
-
const address = handleVerifyMessage(signature, message);
|
|
128
|
-
|
|
129
|
-
// Method 2: Simple signature validation
|
|
130
|
-
const isValid = validateSignature(signature, message, userId);
|
|
24
|
+
https://your-game.com?hyve-access=eyJhbGci...&game-id=my-game
|
|
131
25
|
```
|
|
132
26
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
#### Domain Validation
|
|
136
|
-
Check if current domain is allowed:
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
import { isDomainAllowed } from "@hyve-sdk/js";
|
|
27
|
+
```js
|
|
28
|
+
const { HyveClient } = window.HyveSDK;
|
|
140
29
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// Multiple domains with wildcard support
|
|
145
|
-
const allowed = isDomainAllowed(
|
|
146
|
-
['example.com', '*.subdomain.com'],
|
|
147
|
-
window.location.hostname
|
|
148
|
-
);
|
|
30
|
+
const client = new HyveClient();
|
|
31
|
+
await client.authenticateFromUrl(window.location.search);
|
|
149
32
|
|
|
150
|
-
|
|
33
|
+
console.log('User ID:', client.getUserId());
|
|
34
|
+
console.log('Game ID:', client.getGameId());
|
|
151
35
|
```
|
|
152
36
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
37
|
+
**Environment Detection:**
|
|
38
|
+
The SDK automatically detects dev vs prod by checking the parent page URL:
|
|
39
|
+
- **Dev**: `marvin.dev.hyve.gg` or `dev.hyve.gg`
|
|
40
|
+
- **Prod**: `marvin.hyve.gg` or `hyve.gg`
|
|
157
41
|
|
|
158
|
-
|
|
159
|
-
import { generateUUID } from "@hyve-sdk/js";
|
|
42
|
+
## Telemetry & Analytics
|
|
160
43
|
|
|
161
|
-
|
|
162
|
-
```
|
|
44
|
+
Track user actions and game events:
|
|
163
45
|
|
|
164
|
-
|
|
46
|
+
```js
|
|
47
|
+
const { HyveClient } = window.HyveSDK;
|
|
165
48
|
|
|
166
|
-
|
|
167
|
-
|
|
49
|
+
const client = new HyveClient();
|
|
50
|
+
await client.authenticateFromUrl(window.location.search);
|
|
168
51
|
|
|
169
|
-
```typescript
|
|
170
|
-
// Send a user-level telemetry event
|
|
171
|
-
// Game ID is automatically extracted from 'game-id' URL parameter
|
|
172
52
|
await client.sendTelemetry(
|
|
173
|
-
'game',
|
|
174
|
-
'player',
|
|
175
|
-
'action',
|
|
176
|
-
'combat',
|
|
177
|
-
'attack',
|
|
178
|
-
{
|
|
53
|
+
'game', // location
|
|
54
|
+
'player', // category
|
|
55
|
+
'action', // action
|
|
56
|
+
'combat', // sub-category (optional)
|
|
57
|
+
'attack', // sub-action (optional)
|
|
58
|
+
{ // event details (optional)
|
|
179
59
|
button: 'attack-btn',
|
|
180
|
-
screenPosition: { x: 100, y: 200 },
|
|
181
60
|
damage: 100,
|
|
182
61
|
targetId: 'enemy-123',
|
|
183
|
-
|
|
184
|
-
level: 3
|
|
185
|
-
},
|
|
186
|
-
'web-chrome' // Platform ID (optional)
|
|
62
|
+
}
|
|
187
63
|
);
|
|
188
64
|
```
|
|
189
65
|
|
|
190
66
|
**Requirements:**
|
|
191
|
-
-
|
|
192
|
-
-
|
|
193
|
-
-
|
|
194
|
-
- User ID is automatically extracted from JWT (cannot be spoofed)
|
|
195
|
-
|
|
196
|
-
**URL Parameters Expected:**
|
|
197
|
-
- `hyve-access` - JWT authentication token
|
|
198
|
-
- `game-id` - Game identifier (associates all telemetry with this game)
|
|
199
|
-
- `hyve-token` or `signature`+`message` - For user authentication
|
|
200
|
-
|
|
201
|
-
**Features:**
|
|
202
|
-
- Automatically includes `session_id` and `game_id` from client
|
|
203
|
-
- Objects in `event_details` are auto-stringified to JSON
|
|
204
|
-
- Validates `event_details` is valid JSON before sending (returns false if invalid)
|
|
205
|
-
- All events tied to authenticated user identity
|
|
206
|
-
|
|
207
|
-
## API Integration
|
|
208
|
-
|
|
209
|
-
### JWT Authentication
|
|
210
|
-
The SDK supports JWT tokens and game IDs passed via URL parameters:
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
// URL: https://game.com?hyve-access=your-jwt-token&game-id=my-game
|
|
67
|
+
- `hyve-access` JWT in URL
|
|
68
|
+
- `game-id` in URL or JWT
|
|
69
|
+
- User authenticated via `authenticateFromUrl`
|
|
214
70
|
|
|
215
|
-
|
|
216
|
-
await client.authenticateFromUrl();
|
|
71
|
+
See [docs/TELEMETRY.md](./docs/TELEMETRY.md) for validation rules and examples.
|
|
217
72
|
|
|
218
|
-
|
|
219
|
-
if (client.hasJwtToken()) {
|
|
220
|
-
console.log('JWT token:', client.getJwtToken());
|
|
221
|
-
console.log('Game ID:', client.getGameId());
|
|
222
|
-
}
|
|
223
|
-
```
|
|
73
|
+
## API Integration
|
|
224
74
|
|
|
225
75
|
### Generic API Calls
|
|
226
|
-
Make authenticated API requests:
|
|
227
76
|
|
|
228
|
-
```
|
|
77
|
+
```js
|
|
229
78
|
// GET request
|
|
230
79
|
const userData = await client.callApi('/api/v1/user');
|
|
231
80
|
|
|
232
|
-
// POST request
|
|
81
|
+
// POST request
|
|
233
82
|
const result = await client.callApi('/api/v1/game/score', {
|
|
234
83
|
method: 'POST',
|
|
235
84
|
body: JSON.stringify({ score: 1000 })
|
|
236
85
|
});
|
|
237
|
-
|
|
238
|
-
// With TypeScript typing
|
|
239
|
-
interface UserData {
|
|
240
|
-
id: string;
|
|
241
|
-
username: string;
|
|
242
|
-
}
|
|
243
|
-
const user = await client.callApi<UserData>('/api/v1/user');
|
|
244
86
|
```
|
|
245
87
|
|
|
246
|
-
### Inventory
|
|
247
|
-
|
|
248
|
-
#### Get User Inventory
|
|
249
|
-
Retrieve all inventory items:
|
|
88
|
+
### Inventory
|
|
250
89
|
|
|
251
|
-
```
|
|
90
|
+
```js
|
|
252
91
|
const inventory = await client.getInventory();
|
|
253
|
-
|
|
254
92
|
console.log(`Total items: ${inventory.total_count}`);
|
|
255
93
|
inventory.items.forEach(item => {
|
|
256
94
|
console.log(`${item.item_type}: ${item.quantity}x`);
|
|
257
95
|
});
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
**Response Type:**
|
|
261
|
-
```typescript
|
|
262
|
-
interface Inventory {
|
|
263
|
-
items: InventoryItem[];
|
|
264
|
-
total_count: number;
|
|
265
|
-
}
|
|
266
96
|
|
|
267
|
-
|
|
268
|
-
id: string;
|
|
269
|
-
user_id: string;
|
|
270
|
-
item_type: string;
|
|
271
|
-
item_id: string;
|
|
272
|
-
quantity: number;
|
|
273
|
-
metadata?: Record<string, any>;
|
|
274
|
-
created_at: string;
|
|
275
|
-
updated_at: string;
|
|
276
|
-
}
|
|
97
|
+
const item = await client.getInventoryItem('item-id-123');
|
|
277
98
|
```
|
|
278
99
|
|
|
279
|
-
|
|
280
|
-
Fetch details for a single item:
|
|
281
|
-
|
|
282
|
-
```typescript
|
|
283
|
-
const item = await client.getInventoryItem('item-id-123');
|
|
100
|
+
## Ads Integration
|
|
284
101
|
|
|
285
|
-
|
|
286
|
-
console.log(`Quantity: ${item.quantity}`);
|
|
287
|
-
if (item.metadata) {
|
|
288
|
-
console.log('Metadata:', item.metadata);
|
|
289
|
-
}
|
|
290
|
-
```
|
|
102
|
+
Ads are **disabled by default** — must be explicitly enabled.
|
|
291
103
|
|
|
292
|
-
|
|
293
|
-
- `GET /api/v1/inventory` - Get all inventory items
|
|
294
|
-
- `GET /api/v1/inventory/:id` - Get specific item
|
|
104
|
+
### Prerequisites
|
|
295
105
|
|
|
296
|
-
|
|
106
|
+
Add the Google H5 Games Ads SDK to your HTML before the Hyve SDK:
|
|
297
107
|
|
|
298
|
-
|
|
108
|
+
```html
|
|
109
|
+
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
|
|
110
|
+
<script>
|
|
111
|
+
window.adBreak = window.adBreak || function(o) {
|
|
112
|
+
(window.adsbygoogle = window.adsbygoogle || []).push(o);
|
|
113
|
+
};
|
|
114
|
+
window.adConfig = window.adConfig || function(o) {
|
|
115
|
+
(window.adsbygoogle = window.adsbygoogle || []).push(o);
|
|
116
|
+
};
|
|
117
|
+
</script>
|
|
118
|
+
<script src="https://package.hyve.gg/hyve-sdk.global.js"></script>
|
|
119
|
+
```
|
|
299
120
|
|
|
300
121
|
### Quick Start
|
|
301
122
|
|
|
302
|
-
```
|
|
303
|
-
|
|
123
|
+
```js
|
|
124
|
+
const { HyveClient } = window.HyveSDK;
|
|
304
125
|
|
|
305
|
-
// Enable ads in initial config
|
|
306
126
|
const client = new HyveClient({
|
|
307
127
|
ads: {
|
|
308
|
-
enabled: true,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
console.log('Pausing game for ad:', type);
|
|
313
|
-
game.pause();
|
|
314
|
-
},
|
|
315
|
-
onAfterAd: (type) => {
|
|
316
|
-
console.log('Resuming game after ad');
|
|
317
|
-
game.resume();
|
|
318
|
-
},
|
|
319
|
-
onRewardEarned: () => {
|
|
320
|
-
console.log('User earned reward!');
|
|
321
|
-
player.coins += 100;
|
|
322
|
-
}
|
|
128
|
+
enabled: true,
|
|
129
|
+
onBeforeAd: (type) => game.pause(),
|
|
130
|
+
onAfterAd: (type) => game.resume(),
|
|
131
|
+
onRewardEarned: () => { player.coins += 100; }
|
|
323
132
|
}
|
|
324
133
|
});
|
|
325
134
|
|
|
326
|
-
|
|
135
|
+
await client.authenticateFromUrl(window.location.search);
|
|
136
|
+
|
|
327
137
|
const result = await client.showAd('rewarded');
|
|
328
138
|
if (result.success) {
|
|
329
139
|
console.log('User watched the ad!');
|
|
@@ -333,91 +143,38 @@ await client.showAd('interstitial'); // Between levels
|
|
|
333
143
|
await client.showAd('preroll'); // Game start
|
|
334
144
|
```
|
|
335
145
|
|
|
336
|
-
### Prerequisites
|
|
337
|
-
|
|
338
|
-
Add the Google H5 Games Ads SDK to your HTML:
|
|
339
|
-
|
|
340
|
-
```html
|
|
341
|
-
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
|
|
342
|
-
<script>
|
|
343
|
-
window.adBreak = window.adBreak || function(o) {
|
|
344
|
-
(window.adsbygoogle = window.adsbygoogle || []).push(o);
|
|
345
|
-
};
|
|
346
|
-
window.adConfig = window.adConfig || function(o) {
|
|
347
|
-
(window.adsbygoogle = window.adsbygoogle || []).push(o);
|
|
348
|
-
};
|
|
349
|
-
</script>
|
|
350
|
-
```
|
|
351
|
-
|
|
352
146
|
### Ad Types
|
|
353
147
|
|
|
354
148
|
| Type | Use Case |
|
|
355
149
|
|------|----------|
|
|
356
|
-
| `rewarded` | User watches full ad for reward
|
|
150
|
+
| `rewarded` | User watches full ad for reward |
|
|
357
151
|
| `interstitial` | Between levels or game screens |
|
|
358
152
|
| `preroll` | Before game starts |
|
|
359
153
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
```typescript
|
|
363
|
-
interface AdConfig {
|
|
364
|
-
enabled?: boolean; // Enable/disable ads (default: false)
|
|
365
|
-
sound?: 'on' | 'off'; // Sound setting (default: 'on')
|
|
366
|
-
debug?: boolean; // Enable debug logging (default: false)
|
|
367
|
-
onBeforeAd?: (type) => void; // Called before ad shows
|
|
368
|
-
onAfterAd?: (type) => void; // Called after ad finishes
|
|
369
|
-
onRewardEarned?: () => void; // Called when user earns reward
|
|
370
|
-
}
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
### Methods
|
|
374
|
-
|
|
375
|
-
```typescript
|
|
376
|
-
// Configure ads after initialization
|
|
377
|
-
client.configureAds({ enabled: true, sound: 'off' });
|
|
378
|
-
|
|
379
|
-
// Show an ad
|
|
380
|
-
const result = await client.showAd('rewarded');
|
|
381
|
-
|
|
382
|
-
// Check status
|
|
383
|
-
client.areAdsEnabled(); // Boolean
|
|
384
|
-
client.areAdsReady(); // Boolean
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
See [docs/ads.md](./docs/ads.md) for detailed documentation and examples.
|
|
388
|
-
|
|
389
|
-
**Requirements:**
|
|
390
|
-
- JWT token must be available (via `hyve-access` URL parameter)
|
|
391
|
-
- User must be authenticated
|
|
154
|
+
See [docs/ads.md](./docs/ads.md) for detailed documentation.
|
|
392
155
|
|
|
393
156
|
## Billing Integration
|
|
394
157
|
|
|
395
|
-
|
|
158
|
+
Unified billing for web (Stripe) and native (In-App Purchases). Platform is detected automatically.
|
|
396
159
|
|
|
397
160
|
### Quick Start
|
|
398
161
|
|
|
399
|
-
```
|
|
400
|
-
|
|
162
|
+
```js
|
|
163
|
+
const { HyveClient } = window.HyveSDK;
|
|
401
164
|
|
|
402
|
-
// Enable billing in initial config
|
|
403
165
|
const client = new HyveClient({
|
|
404
166
|
billing: {
|
|
405
|
-
stripePublishableKey: '
|
|
406
|
-
checkoutUrl: 'https://
|
|
167
|
+
stripePublishableKey: 'pk_live_...',
|
|
168
|
+
checkoutUrl: 'https://checkout-api.hyve.gg',
|
|
407
169
|
}
|
|
408
170
|
});
|
|
409
171
|
|
|
410
|
-
// Authenticate user
|
|
411
172
|
await client.authenticateFromUrl(window.location.search);
|
|
412
|
-
|
|
413
|
-
// Initialize billing (uses client's userId and gameId automatically)
|
|
414
173
|
const initialized = await client.initializeBilling();
|
|
415
174
|
|
|
416
175
|
if (initialized && client.isBillingAvailable()) {
|
|
417
|
-
// Set up purchase callbacks
|
|
418
176
|
client.onPurchaseComplete((result) => {
|
|
419
177
|
console.log('Purchase successful!', result.transactionId);
|
|
420
|
-
// Refresh inventory
|
|
421
178
|
client.getInventory().then(inv => console.log('Updated inventory:', inv));
|
|
422
179
|
});
|
|
423
180
|
|
|
@@ -425,409 +182,166 @@ if (initialized && client.isBillingAvailable()) {
|
|
|
425
182
|
console.error('Purchase failed:', result.error?.message);
|
|
426
183
|
});
|
|
427
184
|
|
|
428
|
-
// Get available products
|
|
429
185
|
const products = await client.getBillingProducts();
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
// Purchase a product
|
|
433
|
-
await client.purchaseProduct('price_1234', {
|
|
434
|
-
elementId: 'stripe-checkout-element' // For web platform
|
|
435
|
-
});
|
|
186
|
+
await client.purchaseProduct('price_1234');
|
|
436
187
|
}
|
|
437
188
|
```
|
|
438
189
|
|
|
439
190
|
### Platform Support
|
|
440
191
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
|
444
|
-
|
|
445
|
-
| Web | Stripe Embedded Checkout | Stripe publishable key |
|
|
446
|
-
| Native (iOS/Android) | In-App Purchases | Native app integration |
|
|
192
|
+
| Platform | Payment Method | Detection |
|
|
193
|
+
|----------|---------------|-----------|
|
|
194
|
+
| Web | Stripe Embedded Checkout | Default (no ReactNativeWebView) |
|
|
195
|
+
| Native (iOS/Android) | In-App Purchases | `ReactNativeWebView` in window |
|
|
447
196
|
|
|
448
197
|
### Prerequisites
|
|
449
198
|
|
|
450
|
-
For web
|
|
199
|
+
For web, add a container element for the Stripe checkout UI:
|
|
451
200
|
|
|
452
201
|
```html
|
|
453
202
|
<div id="stripe-checkout-element"></div>
|
|
454
203
|
```
|
|
455
204
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
For native platform, ensure the mobile app implements the Native Bridge billing messages.
|
|
459
|
-
|
|
460
|
-
### Configuration
|
|
461
|
-
|
|
462
|
-
```typescript
|
|
463
|
-
interface BillingConfig {
|
|
464
|
-
stripePublishableKey?: string; // Stripe key for web (required for web)
|
|
465
|
-
checkoutUrl?: string; // API endpoint for checkout
|
|
466
|
-
gameId?: number; // Game identifier
|
|
467
|
-
userId?: string; // User identifier
|
|
468
|
-
}
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
Note: When using billing through HyveClient, `userId` and `gameId` are automatically populated from the authenticated user.
|
|
472
|
-
|
|
473
|
-
### Methods
|
|
474
|
-
|
|
475
|
-
```typescript
|
|
476
|
-
// Configure billing after initialization
|
|
477
|
-
client.configureBilling({
|
|
478
|
-
stripePublishableKey: 'pk_live_...',
|
|
479
|
-
checkoutUrl: 'https://api.example.com'
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
// Initialize billing
|
|
483
|
-
await client.initializeBilling();
|
|
484
|
-
|
|
485
|
-
// Check platform and availability
|
|
486
|
-
const platform = client.getBillingPlatform(); // 'web' | 'native' | 'unknown'
|
|
487
|
-
const available = client.isBillingAvailable(); // Boolean
|
|
488
|
-
|
|
489
|
-
// Get products
|
|
490
|
-
const products = await client.getBillingProducts();
|
|
491
|
-
|
|
492
|
-
// Purchase a product
|
|
493
|
-
const result = await client.purchaseProduct(productId, {
|
|
494
|
-
elementId: 'stripe-checkout-element' // Optional, defaults to 'stripe-checkout-element'
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
// Set up callbacks
|
|
498
|
-
client.onPurchaseComplete((result) => {
|
|
499
|
-
console.log('Success!', result);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
client.onPurchaseError((result) => {
|
|
503
|
-
console.error('Failed:', result.error);
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
// Optional: Listen to billing logs
|
|
507
|
-
client.onBillingLog((level, message, data) => {
|
|
508
|
-
console.log(`[${level}] ${message}`, data);
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
// Clean up checkout UI
|
|
512
|
-
client.unmountBillingCheckout();
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
### Product Interface
|
|
516
|
-
|
|
517
|
-
```typescript
|
|
518
|
-
interface BillingProduct {
|
|
519
|
-
productId: string; // Product/price ID
|
|
520
|
-
title: string; // Product name
|
|
521
|
-
description: string; // Product description
|
|
522
|
-
price: number; // Price in dollars (e.g., 9.99)
|
|
523
|
-
localizedPrice: string; // Formatted price (e.g., "$9.99")
|
|
524
|
-
currency: string; // Currency code (e.g., "USD")
|
|
525
|
-
}
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
### Purchase Result
|
|
529
|
-
|
|
530
|
-
```typescript
|
|
531
|
-
interface PurchaseResult {
|
|
532
|
-
success: boolean;
|
|
533
|
-
productId: string;
|
|
534
|
-
transactionId?: string;
|
|
535
|
-
transactionDate?: string;
|
|
536
|
-
error?: {
|
|
537
|
-
code: string;
|
|
538
|
-
message: string;
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### Complete Example with Telemetry
|
|
544
|
-
|
|
545
|
-
```typescript
|
|
546
|
-
const client = new HyveClient({
|
|
547
|
-
billing: {
|
|
548
|
-
stripePublishableKey: process.env.VITE_STRIPE_KEY,
|
|
549
|
-
checkoutUrl: process.env.VITE_CHECKOUT_URL,
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
// Authenticate
|
|
554
|
-
await client.authenticateFromUrl();
|
|
555
|
-
|
|
556
|
-
// Initialize billing
|
|
557
|
-
await client.initializeBilling();
|
|
558
|
-
|
|
559
|
-
// Set up callbacks with telemetry
|
|
560
|
-
client.onPurchaseComplete((result) => {
|
|
561
|
-
// Send success telemetry
|
|
562
|
-
client.sendTelemetry(
|
|
563
|
-
'shop',
|
|
564
|
-
'purchase',
|
|
565
|
-
'complete',
|
|
566
|
-
'success',
|
|
567
|
-
null,
|
|
568
|
-
{ productId: result.productId, transactionId: result.transactionId }
|
|
569
|
-
);
|
|
570
|
-
|
|
571
|
-
// Refresh inventory
|
|
572
|
-
client.getInventory();
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
client.onPurchaseError((result) => {
|
|
576
|
-
// Send error telemetry
|
|
577
|
-
client.sendTelemetry(
|
|
578
|
-
'shop',
|
|
579
|
-
'purchase',
|
|
580
|
-
'error',
|
|
581
|
-
'failed',
|
|
582
|
-
null,
|
|
583
|
-
{
|
|
584
|
-
productId: result.productId,
|
|
585
|
-
errorCode: result.error?.code,
|
|
586
|
-
errorMessage: result.error?.message
|
|
587
|
-
}
|
|
588
|
-
);
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
// Get and display products
|
|
592
|
-
const products = await client.getBillingProducts();
|
|
593
|
-
```
|
|
594
|
-
|
|
595
|
-
See [docs/examples/billing-with-client-example.ts](./docs/examples/billing-with-client-example.ts) for detailed examples including React components and Phaser integration.
|
|
596
|
-
|
|
597
|
-
**Requirements:**
|
|
598
|
-
- JWT token must be available (via `hyve-access` URL parameter)
|
|
599
|
-
- User must be authenticated
|
|
600
|
-
- Game ID must be available (via `game-id` URL parameter or JWT)
|
|
601
|
-
- For web: Stripe publishable key required
|
|
602
|
-
- For native: Mobile app must implement Native Bridge billing integration
|
|
205
|
+
See [docs/BILLING_INTEGRATION.md](./docs/BILLING_INTEGRATION.md) for full documentation and examples.
|
|
603
206
|
|
|
604
207
|
## Native Bridge (React Native WebView)
|
|
605
208
|
|
|
606
|
-
|
|
209
|
+
Type-safe bidirectional communication between your web game and a React Native mobile app.
|
|
607
210
|
|
|
608
211
|
### Quick Start
|
|
609
212
|
|
|
610
|
-
```
|
|
611
|
-
|
|
213
|
+
```js
|
|
214
|
+
const { NativeBridge } = window.HyveSDK;
|
|
612
215
|
|
|
613
|
-
// Initialize on app start
|
|
614
216
|
if (NativeBridge.isNativeContext()) {
|
|
615
217
|
NativeBridge.initialize();
|
|
616
218
|
}
|
|
617
219
|
```
|
|
618
220
|
|
|
619
|
-
###
|
|
221
|
+
### IAP Availability
|
|
620
222
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
```typescript
|
|
624
|
-
// Register response handler
|
|
625
|
-
NativeBridge.on("IAP_AVAILABILITY_RESULT", (payload) => {
|
|
223
|
+
```js
|
|
224
|
+
NativeBridge.on('IAP_AVAILABILITY_RESULT', (payload) => {
|
|
626
225
|
if (payload.available) {
|
|
627
|
-
console.log(
|
|
628
|
-
// Show purchase UI
|
|
629
|
-
} else {
|
|
630
|
-
console.log("IAP is not available");
|
|
631
|
-
// Hide purchase features
|
|
226
|
+
console.log('IAP available on platform:', payload.platform);
|
|
632
227
|
}
|
|
633
228
|
});
|
|
634
229
|
|
|
635
|
-
// Send request
|
|
636
230
|
NativeBridge.checkIAPAvailability();
|
|
637
231
|
```
|
|
638
232
|
|
|
639
|
-
###
|
|
640
|
-
|
|
641
|
-
Request notification permissions from the native app:
|
|
233
|
+
### Push Notifications
|
|
642
234
|
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
console.log("Push notifications enabled");
|
|
647
|
-
console.log("Token:", payload?.token);
|
|
235
|
+
```js
|
|
236
|
+
NativeBridge.on('PUSH_PERMISSION_GRANTED', (payload) => {
|
|
237
|
+
console.log('Token:', payload?.token);
|
|
648
238
|
});
|
|
649
239
|
|
|
650
|
-
NativeBridge.on(
|
|
651
|
-
console.log(
|
|
240
|
+
NativeBridge.on('PUSH_PERMISSION_DENIED', () => {
|
|
241
|
+
console.log('Permission denied');
|
|
652
242
|
});
|
|
653
243
|
|
|
654
|
-
// Send request
|
|
655
244
|
NativeBridge.requestNotificationPermission();
|
|
656
245
|
```
|
|
657
246
|
|
|
658
|
-
###
|
|
247
|
+
### Custom Messages
|
|
659
248
|
|
|
660
|
-
|
|
249
|
+
```js
|
|
250
|
+
NativeBridge.send('GAME_EVENT', { action: 'level_complete', level: 3 });
|
|
661
251
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
NativeBridge.send("CUSTOM_EVENT", {
|
|
665
|
-
action: "open_settings",
|
|
666
|
-
data: { setting: "notifications" }
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
// Listen for custom responses
|
|
670
|
-
NativeBridge.on("CUSTOM_RESPONSE", (payload) => {
|
|
671
|
-
console.log("Received:", payload);
|
|
252
|
+
NativeBridge.on('CUSTOM_RESPONSE', (payload) => {
|
|
253
|
+
console.log('Received:', payload);
|
|
672
254
|
});
|
|
673
255
|
```
|
|
674
256
|
|
|
675
|
-
### Built-in Message Types
|
|
676
|
-
|
|
677
|
-
**Web → Native:**
|
|
678
|
-
- `CHECK_IAP_AVAILABILITY` - Check if IAP is available
|
|
679
|
-
- `REQUEST_NOTIFICATION_PERMISSION` - Request push notification permission
|
|
680
|
-
|
|
681
|
-
**Native → Web:**
|
|
682
|
-
- `IAP_AVAILABILITY_RESULT` - Response with IAP availability
|
|
683
|
-
- `PUSH_PERMISSION_GRANTED` - Push permission granted
|
|
684
|
-
- `PUSH_PERMISSION_DENIED` - Push permission denied
|
|
685
|
-
|
|
686
257
|
### API Reference
|
|
687
258
|
|
|
688
|
-
- `NativeBridge.isNativeContext()`
|
|
689
|
-
- `NativeBridge.initialize()`
|
|
690
|
-
- `NativeBridge.send(type, payload?)`
|
|
691
|
-
- `NativeBridge.on(type, handler)`
|
|
692
|
-
- `NativeBridge.off(type)`
|
|
693
|
-
- `NativeBridge.clearHandlers()`
|
|
694
|
-
- `NativeBridge.checkIAPAvailability()`
|
|
695
|
-
- `NativeBridge.requestNotificationPermission()`
|
|
259
|
+
- `NativeBridge.isNativeContext()` — Check if running in React Native WebView
|
|
260
|
+
- `NativeBridge.initialize()` — Start message listener
|
|
261
|
+
- `NativeBridge.send(type, payload?)` — Send message to native
|
|
262
|
+
- `NativeBridge.on(type, handler)` — Register message handler
|
|
263
|
+
- `NativeBridge.off(type)` — Remove message handler
|
|
264
|
+
- `NativeBridge.clearHandlers()` — Remove all handlers
|
|
265
|
+
- `NativeBridge.checkIAPAvailability()` — Request IAP availability
|
|
266
|
+
- `NativeBridge.requestNotificationPermission()` — Request notification permission
|
|
696
267
|
|
|
697
|
-
|
|
268
|
+
See [docs/NATIVE_BRIDGE.md](./docs/NATIVE_BRIDGE.md) for full documentation.
|
|
698
269
|
|
|
699
270
|
## Logger
|
|
700
271
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
### Quick Start
|
|
704
|
-
|
|
705
|
-
```typescript
|
|
706
|
-
import { logger } from "@hyve-sdk/js";
|
|
707
|
-
|
|
708
|
-
logger.debug("Debug information", { data: "value" });
|
|
709
|
-
logger.info("Informational message");
|
|
710
|
-
logger.warn("Warning message");
|
|
711
|
-
logger.error("Error message", error);
|
|
712
|
-
```
|
|
713
|
-
|
|
714
|
-
### Configuration
|
|
272
|
+
```js
|
|
273
|
+
const { logger } = window.HyveSDK;
|
|
715
274
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
**Browser Override:**
|
|
721
|
-
```javascript
|
|
722
|
-
// Enable specific log levels
|
|
723
|
-
localStorage.setItem('HYVE_SDK_LOG_LEVEL', 'error,warn');
|
|
724
|
-
```
|
|
725
|
-
|
|
726
|
-
**Node.js Override:**
|
|
727
|
-
```bash
|
|
728
|
-
HYVE_SDK_LOG_LEVEL=error,warn node app.js
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
**Programmatic Control:**
|
|
732
|
-
```typescript
|
|
733
|
-
import { Logger } from "@hyve-sdk/js";
|
|
734
|
-
|
|
735
|
-
const logger = new Logger();
|
|
736
|
-
logger.setLevels(['error', 'warn']);
|
|
737
|
-
```
|
|
738
|
-
|
|
739
|
-
### Child Loggers
|
|
740
|
-
|
|
741
|
-
Create namespaced loggers for different parts of your application:
|
|
742
|
-
|
|
743
|
-
```typescript
|
|
744
|
-
import { logger } from "@hyve-sdk/js";
|
|
275
|
+
logger.debug('Debug info', { data: 'value' });
|
|
276
|
+
logger.info('Informational message');
|
|
277
|
+
logger.warn('Warning message');
|
|
278
|
+
logger.error('Error message', error);
|
|
745
279
|
|
|
280
|
+
// Namespaced child loggers
|
|
746
281
|
const gameLogger = logger.child('Game');
|
|
747
282
|
gameLogger.info('Game started');
|
|
748
283
|
// Output: [Hyve SDK] [Game] [INFO] [timestamp] Game started
|
|
749
|
-
|
|
750
|
-
const uiLogger = logger.child('UI');
|
|
751
|
-
uiLogger.debug('Button clicked');
|
|
752
|
-
// Output: [Hyve SDK] [UI] [DEBUG] [timestamp] Button clicked
|
|
753
284
|
```
|
|
754
285
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
- **Timestamps**: All logs include ISO 8601 timestamps
|
|
760
|
-
- **Prefixed output**: All logs prefixed with `[Hyve SDK]`
|
|
761
|
-
- **Child loggers**: Create namespaced loggers for different modules
|
|
762
|
-
- **Browser storage**: Log level persists in localStorage
|
|
286
|
+
**Log level override (browser):**
|
|
287
|
+
```js
|
|
288
|
+
localStorage.setItem('HYVE_SDK_LOG_LEVEL', 'error,warn');
|
|
289
|
+
```
|
|
763
290
|
|
|
764
291
|
## Client Methods Reference
|
|
765
292
|
|
|
766
293
|
### Authentication
|
|
767
|
-
- `authenticateFromUrl(urlParams
|
|
768
|
-
- `getUserId()`
|
|
769
|
-
- `getSessionId()`
|
|
770
|
-
- `getGameId()`
|
|
771
|
-
- `isUserAuthenticated()`
|
|
772
|
-
- `hasJwtToken()`
|
|
773
|
-
- `getJwtToken()`
|
|
774
|
-
- `logout()`
|
|
775
|
-
- `reset()`
|
|
776
|
-
|
|
777
|
-
### API
|
|
778
|
-
- `callApi<T>(endpoint, options?)`
|
|
779
|
-
- `getInventory()`
|
|
780
|
-
- `getInventoryItem(itemId)`
|
|
294
|
+
- `authenticateFromUrl(urlParams)` — Extract JWT and game ID from URL, authenticate user
|
|
295
|
+
- `getUserId()` — Authenticated user ID
|
|
296
|
+
- `getSessionId()` — Unique session ID
|
|
297
|
+
- `getGameId()` — Game ID from URL or JWT
|
|
298
|
+
- `isUserAuthenticated()` — Check if authenticated
|
|
299
|
+
- `hasJwtToken()` — Check if JWT available
|
|
300
|
+
- `getJwtToken()` — Get JWT string
|
|
301
|
+
- `logout()` — Clear authentication
|
|
302
|
+
- `reset()` — Reset client state
|
|
303
|
+
|
|
304
|
+
### API
|
|
305
|
+
- `callApi<T>(endpoint, options?)` — Authenticated API call
|
|
306
|
+
- `getInventory()` — Get user inventory
|
|
307
|
+
- `getInventoryItem(itemId)` — Get specific inventory item
|
|
781
308
|
|
|
782
309
|
### Telemetry
|
|
783
|
-
- `sendTelemetry(location, category, action, subCategory?, subAction?, details
|
|
784
|
-
- `updateTelemetryConfig(config)`
|
|
310
|
+
- `sendTelemetry(location, category, action, subCategory?, subAction?, details?)` — Send analytics event
|
|
311
|
+
- `updateTelemetryConfig(config)` — Update telemetry settings
|
|
785
312
|
|
|
786
313
|
### Ads
|
|
787
|
-
- `configureAds(config)`
|
|
788
|
-
- `showAd(type)`
|
|
789
|
-
- `areAdsEnabled()`
|
|
790
|
-
- `areAdsReady()`
|
|
791
|
-
|
|
792
|
-
###
|
|
793
|
-
- `
|
|
794
|
-
- `
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
4. SDK verifies token with `verifyHyveToken()` or `verifyAuthentication()`
|
|
803
|
-
|
|
804
|
-
### Legacy Token Flow
|
|
805
|
-
1. User signs message containing metadata (expiration, address)
|
|
806
|
-
2. Signature and message passed via URL parameters
|
|
807
|
-
3. SDK verifies with `handleVerifyMessage()` or `verifyAuthentication()`
|
|
314
|
+
- `configureAds(config)` — Configure ads service
|
|
315
|
+
- `showAd(type)` — Show an ad (`'rewarded'` | `'interstitial'` | `'preroll'`)
|
|
316
|
+
- `areAdsEnabled()` — Check if ads are enabled
|
|
317
|
+
- `areAdsReady()` — Check if ads are ready
|
|
318
|
+
|
|
319
|
+
### Billing
|
|
320
|
+
- `configureBilling(config)` — Configure billing service
|
|
321
|
+
- `initializeBilling()` — Initialize billing (resolves platform automatically)
|
|
322
|
+
- `getBillingPlatform()` — `'web'` | `'native'` | `'unknown'`
|
|
323
|
+
- `isBillingAvailable()` — Check if billing is ready
|
|
324
|
+
- `getBillingProducts()` — Fetch available products
|
|
325
|
+
- `purchaseProduct(productId, options?)` — Initiate purchase
|
|
326
|
+
- `onPurchaseComplete(callback)` — Register purchase success handler
|
|
327
|
+
- `onPurchaseError(callback)` — Register purchase error handler
|
|
328
|
+
- `unmountBillingCheckout()` — Clean up Stripe checkout UI
|
|
808
329
|
|
|
809
330
|
## Security Considerations
|
|
810
331
|
|
|
811
|
-
-
|
|
812
|
-
-
|
|
813
|
-
-
|
|
814
|
-
-
|
|
332
|
+
- Never expose secret keys in game code
|
|
333
|
+
- Always validate receipts server-side
|
|
334
|
+
- Use HTTPS for all requests
|
|
335
|
+
- Don't trust client-reported prices
|
|
336
|
+
- Verify user authentication before crediting purchases
|
|
815
337
|
|
|
816
|
-
##
|
|
338
|
+
## Build Output
|
|
817
339
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
- **uuid**: (peer dependency) - UUID generation
|
|
824
|
-
|
|
825
|
-
## Build Configuration
|
|
826
|
-
|
|
827
|
-
The SDK builds to multiple formats:
|
|
828
|
-
- CommonJS (`dist/index.js`)
|
|
829
|
-
- ES Modules (`dist/index.mjs`)
|
|
830
|
-
- TypeScript declarations (`dist/index.d.ts`)
|
|
340
|
+
| Format | File | Use |
|
|
341
|
+
|--------|------|-----|
|
|
342
|
+
| CJS | `dist/index.js` | Node.js / bundler |
|
|
343
|
+
| ESM | `dist/index.mjs` | Bundler (tree-shakeable) |
|
|
344
|
+
| IIFE | `dist/hyve-sdk.global.js` | CDN / script tag |
|
|
831
345
|
|
|
832
346
|
## Development
|
|
833
347
|
|
|
@@ -847,16 +361,13 @@ bun run build
|
|
|
847
361
|
|
|
848
362
|
## Documentation
|
|
849
363
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
- [
|
|
855
|
-
- [Billing
|
|
856
|
-
- [Billing Migration Guide](./docs/BILLING_MIGRATION_GUIDE.md) - Upgrading from older billing implementations
|
|
857
|
-
- [Ads Integration](./docs/ads.md) - Google H5 Games Ads integration
|
|
858
|
-
- [Native Bridge](./docs/NATIVE_BRIDGE.md) - React Native WebView communication
|
|
364
|
+
- [Telemetry Guide](./docs/TELEMETRY.md)
|
|
365
|
+
- [Billing Integration](./docs/BILLING_INTEGRATION.md)
|
|
366
|
+
- [Billing Migration Guide](./docs/BILLING_MIGRATION_GUIDE.md)
|
|
367
|
+
- [Ads Integration](./docs/ads.md)
|
|
368
|
+
- [Native Bridge](./docs/NATIVE_BRIDGE.md)
|
|
369
|
+
- [Platform-Aware Billing](./docs/PLATFORM_AWARE_BILLING.md)
|
|
859
370
|
|
|
860
371
|
## License
|
|
861
372
|
|
|
862
|
-
MIT
|
|
373
|
+
MIT
|