capybara-game-sdk 0.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/dist/index.mjs ADDED
@@ -0,0 +1,690 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+
3
+ // src/AuthModule.ts
4
+ var AuthModule = class {
5
+ constructor(bridge) {
6
+ this.bridge = bridge;
7
+ this.currentUser = null;
8
+ this.authChangeListeners = /* @__PURE__ */ new Set();
9
+ this.initialize();
10
+ }
11
+ async initialize() {
12
+ this.currentUser = await this.bridge.getCurrentUser();
13
+ this.bridge.on("auth_changed", (user) => {
14
+ this.currentUser = user;
15
+ this.notifyAuthChange();
16
+ });
17
+ }
18
+ notifyAuthChange() {
19
+ this.authChangeListeners.forEach((listener) => {
20
+ try {
21
+ listener(this.currentUser);
22
+ } catch (error) {
23
+ console.error("Error in auth change listener:", error);
24
+ }
25
+ });
26
+ }
27
+ /**
28
+ * Get the currently authenticated user
29
+ */
30
+ async getCurrentUser() {
31
+ if (!this.currentUser) {
32
+ this.currentUser = await this.bridge.getCurrentUser();
33
+ }
34
+ return this.currentUser;
35
+ }
36
+ /**
37
+ * Get bearer token for the authenticated user
38
+ */
39
+ async getBearerToken() {
40
+ return await this.bridge.getBearerToken();
41
+ }
42
+ /**
43
+ * Request user authentication (triggers parent app UI)
44
+ */
45
+ async requestLogin() {
46
+ await this.bridge.requestLogin();
47
+ await new Promise((resolve) => setTimeout(resolve, 500));
48
+ this.currentUser = await this.bridge.getCurrentUser();
49
+ this.notifyAuthChange();
50
+ }
51
+ /**
52
+ * Logout the current user
53
+ */
54
+ async logout() {
55
+ await this.bridge.logout();
56
+ this.currentUser = null;
57
+ this.notifyAuthChange();
58
+ }
59
+ /**
60
+ * Listen for authentication state changes
61
+ */
62
+ onAuthChange(callback) {
63
+ this.authChangeListeners.add(callback);
64
+ callback(this.currentUser);
65
+ return () => {
66
+ this.authChangeListeners.delete(callback);
67
+ };
68
+ }
69
+ /**
70
+ * Check if user is authenticated
71
+ */
72
+ isAuthenticated() {
73
+ return this.currentUser !== null;
74
+ }
75
+ };
76
+
77
+ // src/ParentBridge.ts
78
+ var ParentBridge = class {
79
+ constructor(allowedOrigins = ["*"]) {
80
+ this.messageHandlers = /* @__PURE__ */ new Map();
81
+ this.pendingRequests = /* @__PURE__ */ new Map();
82
+ this.requestIdCounter = 0;
83
+ this.allowedOrigins = new Set(allowedOrigins);
84
+ this.setupMessageListener();
85
+ }
86
+ setupMessageListener() {
87
+ window.addEventListener("message", (event) => {
88
+ if (this.allowedOrigins.size > 0 && !this.allowedOrigins.has("*") && !this.allowedOrigins.has(event.origin)) {
89
+ console.warn(
90
+ "Rejected message from unauthorized origin:",
91
+ event.origin
92
+ );
93
+ return;
94
+ }
95
+ const message = event.data;
96
+ if (!message || typeof message !== "object" || !message.type) {
97
+ return;
98
+ }
99
+ const requestId = message.requestId;
100
+ if (requestId && this.pendingRequests.has(requestId)) {
101
+ const { resolve, reject } = this.pendingRequests.get(requestId);
102
+ this.pendingRequests.delete(requestId);
103
+ if (message.error) {
104
+ reject(new Error(message.error));
105
+ } else {
106
+ resolve(message.data);
107
+ }
108
+ return;
109
+ }
110
+ const handler = this.messageHandlers.get(message.type);
111
+ if (handler) {
112
+ handler(message.data);
113
+ }
114
+ });
115
+ }
116
+ /**
117
+ * Send a message to parent and wait for response
118
+ */
119
+ sendRequest(type, data) {
120
+ return new Promise((resolve, reject) => {
121
+ const requestId = `req_${this.requestIdCounter++}_${Date.now()}`;
122
+ this.pendingRequests.set(requestId, { resolve, reject });
123
+ const message = {
124
+ type,
125
+ data,
126
+ requestId
127
+ };
128
+ window.parent.postMessage(message, "*");
129
+ setTimeout(() => {
130
+ if (this.pendingRequests.has(requestId)) {
131
+ this.pendingRequests.delete(requestId);
132
+ reject(new Error("Request timeout"));
133
+ }
134
+ }, 3e4);
135
+ });
136
+ }
137
+ /**
138
+ * Send a message to parent without expecting a response
139
+ */
140
+ sendMessage(type, data) {
141
+ const message = {
142
+ type,
143
+ data
144
+ };
145
+ window.parent.postMessage(message, "*");
146
+ }
147
+ /**
148
+ * Listen for messages from parent
149
+ */
150
+ on(type, handler) {
151
+ this.messageHandlers.set(type, handler);
152
+ }
153
+ /**
154
+ * Remove message listener
155
+ */
156
+ off(type) {
157
+ this.messageHandlers.delete(type);
158
+ }
159
+ /**
160
+ * Get current authenticated user
161
+ */
162
+ async getCurrentUser() {
163
+ try {
164
+ return await this.sendRequest("get_user");
165
+ } catch (error) {
166
+ console.error("Failed to get current user:", error);
167
+ return null;
168
+ }
169
+ }
170
+ /**
171
+ * Request user to login (triggers parent app UI)
172
+ */
173
+ async requestLogin() {
174
+ return this.sendRequest("request_login");
175
+ }
176
+ /**
177
+ * Logout current user
178
+ */
179
+ async logout() {
180
+ return this.sendRequest("logout");
181
+ }
182
+ /**
183
+ * Get bearer token for authenticated user
184
+ */
185
+ async getBearerToken() {
186
+ try {
187
+ return await this.sendRequest("get_bearer_token");
188
+ } catch (error) {
189
+ console.error("Failed to get bearer token:", error);
190
+ return null;
191
+ }
192
+ }
193
+ /**
194
+ * Get user's credit balance
195
+ */
196
+ async getBalance() {
197
+ return this.sendRequest("get_balance");
198
+ }
199
+ /**
200
+ * Charge credits from user's balance
201
+ */
202
+ async chargeCredits(amount, description) {
203
+ return this.sendRequest("charge_credits", {
204
+ amount,
205
+ description
206
+ });
207
+ }
208
+ /**
209
+ * Notify parent that SDK is ready
210
+ */
211
+ notifyReady() {
212
+ this.sendMessage("ready");
213
+ }
214
+ };
215
+
216
+ // src/PaymentsModule.ts
217
+ var PaymentsModule = class {
218
+ constructor(bridge, auth, devMode) {
219
+ this.bridge = bridge;
220
+ this.auth = auth;
221
+ this.devMode = devMode;
222
+ this.currentBalance = { credits: 0 };
223
+ this.balanceChangeListeners = /* @__PURE__ */ new Set();
224
+ this.transactions = [];
225
+ this.initialize();
226
+ }
227
+ async initialize() {
228
+ if (this.auth.isAuthenticated()) {
229
+ await this.refreshBalance();
230
+ }
231
+ this.auth.onAuthChange(async (user) => {
232
+ if (user) {
233
+ await this.refreshBalance();
234
+ } else {
235
+ this.currentBalance = { credits: 0 };
236
+ this.notifyBalanceChange();
237
+ }
238
+ });
239
+ this.bridge.on("balance_updated", (balance) => {
240
+ this.currentBalance = balance;
241
+ this.notifyBalanceChange();
242
+ });
243
+ }
244
+ async refreshBalance() {
245
+ try {
246
+ this.currentBalance = await this.bridge.getBalance();
247
+ this.notifyBalanceChange();
248
+ } catch (error) {
249
+ console.error("Failed to refresh balance:", error);
250
+ }
251
+ }
252
+ notifyBalanceChange() {
253
+ this.balanceChangeListeners.forEach((listener) => {
254
+ try {
255
+ listener(this.currentBalance);
256
+ } catch (error) {
257
+ console.error("Error in balance change listener:", error);
258
+ }
259
+ });
260
+ }
261
+ /**
262
+ * Get the current credit balance
263
+ */
264
+ async getBalance() {
265
+ if (!this.auth.isAuthenticated()) {
266
+ throw new Error("User must be authenticated to check balance");
267
+ }
268
+ if (this.devMode) {
269
+ return { credits: 999999 };
270
+ }
271
+ await this.refreshBalance();
272
+ return this.currentBalance;
273
+ }
274
+ /**
275
+ * Charge credits from the user's balance
276
+ * In dev mode, this is a no-op (free)
277
+ */
278
+ async charge(amount, description) {
279
+ if (!this.auth.isAuthenticated()) {
280
+ throw new Error("User must be authenticated to make payments");
281
+ }
282
+ if (amount <= 0) {
283
+ throw new Error("Amount must be greater than 0");
284
+ }
285
+ if (this.devMode) {
286
+ console.warn(
287
+ `Dev mode: Charging ${amount} credits for "${description}" - no actual charge applied.`
288
+ );
289
+ return {
290
+ success: true,
291
+ newBalance: 999999
292
+ };
293
+ }
294
+ try {
295
+ const result = await this.bridge.chargeCredits(amount, description);
296
+ if (result.success) {
297
+ this.currentBalance = { credits: result.newBalance };
298
+ this.notifyBalanceChange();
299
+ this.transactions.push({
300
+ amount: -amount,
301
+ description,
302
+ timestamp: Date.now()
303
+ });
304
+ }
305
+ return result;
306
+ } catch (error) {
307
+ return {
308
+ success: false,
309
+ newBalance: this.currentBalance.credits,
310
+ error: error instanceof Error ? error.message : "Failed to charge credits"
311
+ };
312
+ }
313
+ }
314
+ /**
315
+ * Listen for balance changes
316
+ */
317
+ onBalanceChange(callback) {
318
+ this.balanceChangeListeners.add(callback);
319
+ callback(this.currentBalance);
320
+ return () => {
321
+ this.balanceChangeListeners.delete(callback);
322
+ };
323
+ }
324
+ /**
325
+ * Get transaction history
326
+ * Note: This only includes transactions made during the current session
327
+ * For full history, the parent app should provide an API
328
+ */
329
+ async getHistory() {
330
+ return [...this.transactions];
331
+ }
332
+ /**
333
+ * Gift credits to another user (requires server-side implementation)
334
+ */
335
+ async giftCredits(userId, amount, description) {
336
+ throw new Error("Gift credits not yet implemented");
337
+ }
338
+ /**
339
+ * Set development mode
340
+ */
341
+ setDevMode(enabled) {
342
+ this.devMode = enabled;
343
+ }
344
+ };
345
+
346
+ // src/GameSDK.ts
347
+ var _GameSDK = class _GameSDK {
348
+ constructor(config) {
349
+ this.config = config;
350
+ this.gameId = config.gameId;
351
+ this.bridge = new ParentBridge();
352
+ this.auth = new AuthModule(this.bridge);
353
+ this.payments = new PaymentsModule(
354
+ this.bridge,
355
+ this.auth,
356
+ config.devMode || false
357
+ );
358
+ this.bridge.notifyReady();
359
+ if (config.onReady) {
360
+ setTimeout(config.onReady, 0);
361
+ }
362
+ }
363
+ /**
364
+ * Initialize the Game SDK
365
+ */
366
+ static async init(config) {
367
+ if (_GameSDK.instance) {
368
+ console.warn("GameSDK already initialized. Returning existing instance.");
369
+ return _GameSDK.instance;
370
+ }
371
+ if (!config.gameId) {
372
+ throw new Error("gameId is required");
373
+ }
374
+ _GameSDK.instance = new _GameSDK(config);
375
+ return _GameSDK.instance;
376
+ }
377
+ /**
378
+ * Get the current SDK instance
379
+ */
380
+ static getInstance() {
381
+ if (!_GameSDK.instance) {
382
+ throw new Error("GameSDK not initialized. Call GameSDK.init() first.");
383
+ }
384
+ return _GameSDK.instance;
385
+ }
386
+ /**
387
+ * Set development mode
388
+ */
389
+ setDevMode(enabled) {
390
+ this.config.devMode = enabled;
391
+ this.payments.setDevMode(enabled);
392
+ }
393
+ /**
394
+ * Get SDK configuration
395
+ */
396
+ getConfig() {
397
+ return { ...this.config };
398
+ }
399
+ /**
400
+ * Clean up SDK resources
401
+ */
402
+ async destroy() {
403
+ _GameSDK.instance = null;
404
+ }
405
+ };
406
+ _GameSDK.instance = null;
407
+ var GameSDK = _GameSDK;
408
+
409
+ // src/ApiClient.ts
410
+ var API_BASE_URL = "https://api.gamesdk.com";
411
+ var ApiClient = class {
412
+ constructor(auth) {
413
+ this.auth = auth;
414
+ }
415
+ async getHeaders() {
416
+ const token = await this.auth.getBearerToken();
417
+ return {
418
+ "Content-Type": "application/json",
419
+ ...token ? { Authorization: `Bearer ${token}` } : {}
420
+ };
421
+ }
422
+ async makeRequest(endpoint, options = {}) {
423
+ const url = `${API_BASE_URL}${endpoint}`;
424
+ const headers = await this.getHeaders();
425
+ const response = await fetch(url, {
426
+ ...options,
427
+ headers: {
428
+ ...headers,
429
+ ...options.headers
430
+ }
431
+ });
432
+ if (!response.ok) {
433
+ throw new Error(
434
+ `API request failed: ${response.status} ${response.statusText}`
435
+ );
436
+ }
437
+ return response.json();
438
+ }
439
+ /**
440
+ * Save game data to the server
441
+ */
442
+ async saveGameData(gameId, data) {
443
+ await this.makeRequest("/api/storage/save", {
444
+ method: "POST",
445
+ body: JSON.stringify({ gameId, data })
446
+ });
447
+ }
448
+ /**
449
+ * Load game data from the server
450
+ */
451
+ async loadGameData(gameId) {
452
+ try {
453
+ const result = await this.makeRequest(
454
+ "/api/storage/load",
455
+ {
456
+ method: "GET"
457
+ }
458
+ );
459
+ return result.data;
460
+ } catch (error) {
461
+ if (error instanceof Error && error.message.includes("404")) {
462
+ return null;
463
+ }
464
+ throw error;
465
+ }
466
+ }
467
+ /**
468
+ * Clear game data from the server
469
+ */
470
+ async clearGameData(gameId) {
471
+ await this.makeRequest("/api/storage/clear", {
472
+ method: "DELETE"
473
+ });
474
+ }
475
+ /**
476
+ * Check if game data exists on the server
477
+ */
478
+ async gameDataExists(gameId) {
479
+ try {
480
+ await this.makeRequest("/api/storage/exists", {
481
+ method: "GET"
482
+ });
483
+ return true;
484
+ } catch (error) {
485
+ if (error instanceof Error && error.message.includes("404")) {
486
+ return false;
487
+ }
488
+ throw error;
489
+ }
490
+ }
491
+ /**
492
+ * Get the size of stored game data
493
+ */
494
+ async getGameDataSize(gameId) {
495
+ const result = await this.makeRequest(
496
+ "/api/storage/size",
497
+ {
498
+ method: "GET"
499
+ }
500
+ );
501
+ return result.size;
502
+ }
503
+ };
504
+
505
+ // src/StorageModule.ts
506
+ var StorageModule = class {
507
+ constructor(gameId, auth) {
508
+ this.auth = auth;
509
+ this.gameId = gameId;
510
+ this.apiClient = new ApiClient(auth);
511
+ }
512
+ /**
513
+ * Save data to storage
514
+ * Data is automatically scoped to current user and game
515
+ * Maximum size: 5MB
516
+ */
517
+ async save(data) {
518
+ try {
519
+ const serialized = JSON.stringify(data);
520
+ const sizeInBytes = new Blob([serialized]).size;
521
+ if (sizeInBytes > 5 * 1024 * 1024) {
522
+ throw new Error("Storage limit exceeded: Maximum 5MB allowed");
523
+ }
524
+ await this.apiClient.saveGameData(this.gameId, data);
525
+ } catch (error) {
526
+ if (error instanceof Error) {
527
+ throw error;
528
+ }
529
+ throw new Error("Failed to save data to storage");
530
+ }
531
+ }
532
+ /**
533
+ * Load data from storage
534
+ * Returns null if no data exists
535
+ */
536
+ async load() {
537
+ try {
538
+ return await this.apiClient.loadGameData(this.gameId);
539
+ } catch (error) {
540
+ console.error("Failed to load data from storage:", error);
541
+ return null;
542
+ }
543
+ }
544
+ /**
545
+ * Clear all stored data for current user and game
546
+ */
547
+ async clear() {
548
+ try {
549
+ await this.apiClient.clearGameData(this.gameId);
550
+ } catch (error) {
551
+ console.error("Failed to clear storage:", error);
552
+ }
553
+ }
554
+ /**
555
+ * Check if data exists in storage
556
+ */
557
+ async exists() {
558
+ return this.apiClient.gameDataExists(this.gameId);
559
+ }
560
+ /**
561
+ * Get the size of stored data in bytes
562
+ */
563
+ async getSize() {
564
+ try {
565
+ return await this.apiClient.getGameDataSize(this.gameId);
566
+ } catch (error) {
567
+ return 0;
568
+ }
569
+ }
570
+ };
571
+ function useGameSDK(config) {
572
+ const [sdk, setSDK] = useState(null);
573
+ const [isReady, setIsReady] = useState(false);
574
+ const [error, setError] = useState(null);
575
+ useEffect(() => {
576
+ let mounted = true;
577
+ const initSDK = async () => {
578
+ try {
579
+ const configWithReady = {
580
+ ...config,
581
+ onReady: () => {
582
+ if (mounted) {
583
+ setIsReady(true);
584
+ config.onReady?.();
585
+ }
586
+ }
587
+ };
588
+ const instance = await GameSDK.init(configWithReady);
589
+ if (mounted) {
590
+ setSDK(instance);
591
+ }
592
+ } catch (err) {
593
+ if (mounted) {
594
+ setError(
595
+ err instanceof Error ? err : new Error("Failed to initialize SDK")
596
+ );
597
+ }
598
+ }
599
+ };
600
+ initSDK();
601
+ return () => {
602
+ mounted = false;
603
+ };
604
+ }, [config.gameId]);
605
+ return { sdk, isReady, error };
606
+ }
607
+ function useAuth() {
608
+ const sdk = GameSDK.getInstance();
609
+ const [user, setUser] = useState(null);
610
+ const [isLoading, setIsLoading] = useState(true);
611
+ useEffect(() => {
612
+ let mounted = true;
613
+ const loadUser = async () => {
614
+ const currentUser = await sdk.auth.getCurrentUser();
615
+ if (mounted) {
616
+ setUser(currentUser);
617
+ setIsLoading(false);
618
+ }
619
+ };
620
+ loadUser();
621
+ const unsubscribe = sdk.auth.onAuthChange((newUser) => {
622
+ if (mounted) {
623
+ setUser(newUser);
624
+ }
625
+ });
626
+ return () => {
627
+ mounted = false;
628
+ unsubscribe();
629
+ };
630
+ }, [sdk]);
631
+ const login = useCallback(async () => {
632
+ await sdk.auth.requestLogin();
633
+ }, [sdk]);
634
+ const logout = useCallback(async () => {
635
+ await sdk.auth.logout();
636
+ }, [sdk]);
637
+ return {
638
+ user,
639
+ isLoading,
640
+ isAuthenticated: !!user,
641
+ login,
642
+ logout
643
+ };
644
+ }
645
+ function useCredits() {
646
+ const sdk = GameSDK.getInstance();
647
+ const [balance, setBalance] = useState({ credits: 0 });
648
+ const [isLoading, setIsLoading] = useState(true);
649
+ useEffect(() => {
650
+ let mounted = true;
651
+ const loadBalance = async () => {
652
+ try {
653
+ const currentBalance = await sdk.payments.getBalance();
654
+ if (mounted) {
655
+ setBalance(currentBalance);
656
+ setIsLoading(false);
657
+ }
658
+ } catch (error) {
659
+ if (mounted) {
660
+ setIsLoading(false);
661
+ }
662
+ }
663
+ };
664
+ loadBalance();
665
+ const unsubscribe = sdk.payments.onBalanceChange((newBalance) => {
666
+ if (mounted) {
667
+ setBalance(newBalance);
668
+ }
669
+ });
670
+ return () => {
671
+ mounted = false;
672
+ unsubscribe();
673
+ };
674
+ }, [sdk]);
675
+ const charge = useCallback(
676
+ async (amount, description) => {
677
+ return await sdk.payments.charge(amount, description);
678
+ },
679
+ [sdk]
680
+ );
681
+ return {
682
+ balance: balance.credits,
683
+ isLoading,
684
+ charge
685
+ };
686
+ }
687
+
688
+ export { AuthModule, GameSDK, PaymentsModule, StorageModule, useAuth, useCredits, useGameSDK };
689
+ //# sourceMappingURL=index.mjs.map
690
+ //# sourceMappingURL=index.mjs.map