@arcadiasol/sdk 1.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.
@@ -0,0 +1,929 @@
1
+ /**
2
+ * Message types for postMessage communication
3
+ */
4
+ var MessageType;
5
+ (function (MessageType) {
6
+ // SDK → Parent
7
+ MessageType["INIT_REQUEST"] = "INIT_REQUEST";
8
+ MessageType["GET_WALLET_ADDRESS"] = "GET_WALLET_ADDRESS";
9
+ MessageType["PAYMENT_REQUEST"] = "PAYMENT_REQUEST";
10
+ MessageType["GAME_READY"] = "GAME_READY";
11
+ // Parent → SDK
12
+ MessageType["INIT"] = "INIT";
13
+ MessageType["WALLET_ADDRESS_RESPONSE"] = "WALLET_ADDRESS_RESPONSE";
14
+ MessageType["WALLET_UPDATE"] = "WALLET_UPDATE";
15
+ MessageType["PAYMENT_RESPONSE"] = "PAYMENT_RESPONSE";
16
+ })(MessageType || (MessageType = {}));
17
+
18
+ /**
19
+ * Base error class for all SDK errors
20
+ */
21
+ class ArcadiaSDKError extends Error {
22
+ constructor(message, code) {
23
+ super(message);
24
+ this.code = code;
25
+ this.name = 'ArcadiaSDKError';
26
+ Object.setPrototypeOf(this, ArcadiaSDKError.prototype);
27
+ }
28
+ }
29
+ /**
30
+ * Error thrown when wallet is not connected
31
+ */
32
+ class WalletNotConnectedError extends ArcadiaSDKError {
33
+ constructor(message = 'Wallet is not connected. Please connect your wallet in Arcadia.') {
34
+ super(message, 'WALLET_NOT_CONNECTED');
35
+ this.name = 'WalletNotConnectedError';
36
+ Object.setPrototypeOf(this, WalletNotConnectedError.prototype);
37
+ }
38
+ }
39
+ /**
40
+ * Error thrown when payment fails
41
+ */
42
+ class PaymentFailedError extends ArcadiaSDKError {
43
+ constructor(message = 'Payment failed', txSignature) {
44
+ super(message, 'PAYMENT_FAILED');
45
+ this.txSignature = txSignature;
46
+ this.name = 'PaymentFailedError';
47
+ Object.setPrototypeOf(this, PaymentFailedError.prototype);
48
+ }
49
+ }
50
+ /**
51
+ * Error thrown when request times out
52
+ */
53
+ class TimeoutError extends ArcadiaSDKError {
54
+ constructor(message = 'Request timed out. Please try again.') {
55
+ super(message, 'TIMEOUT');
56
+ this.name = 'TimeoutError';
57
+ Object.setPrototypeOf(this, TimeoutError.prototype);
58
+ }
59
+ }
60
+ /**
61
+ * Error thrown when SDK configuration is invalid
62
+ */
63
+ class InvalidConfigError extends ArcadiaSDKError {
64
+ constructor(message = 'Invalid SDK configuration') {
65
+ super(message, 'INVALID_CONFIG');
66
+ this.name = 'InvalidConfigError';
67
+ Object.setPrototypeOf(this, InvalidConfigError.prototype);
68
+ }
69
+ }
70
+ /**
71
+ * Error thrown when SDK is not running in iframe
72
+ */
73
+ class NotInIframeError extends ArcadiaSDKError {
74
+ constructor(message = 'This function only works when the game is running in an Arcadia iframe') {
75
+ super(message, 'NOT_IN_IFRAME');
76
+ this.name = 'NotInIframeError';
77
+ Object.setPrototypeOf(this, NotInIframeError.prototype);
78
+ }
79
+ }
80
+ /**
81
+ * Error thrown when invalid amount is provided
82
+ */
83
+ class InvalidAmountError extends ArcadiaSDKError {
84
+ constructor(message = 'Invalid payment amount. Amount must be greater than 0.') {
85
+ super(message, 'INVALID_AMOUNT');
86
+ this.name = 'InvalidAmountError';
87
+ Object.setPrototypeOf(this, InvalidAmountError.prototype);
88
+ }
89
+ }
90
+ /**
91
+ * Error thrown when invalid token is provided
92
+ */
93
+ class InvalidTokenError extends ArcadiaSDKError {
94
+ constructor(message = "Invalid token. Must be 'SOL' or 'USDC'.") {
95
+ super(message, 'INVALID_TOKEN');
96
+ this.name = 'InvalidTokenError';
97
+ Object.setPrototypeOf(this, InvalidTokenError.prototype);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Handles postMessage communication with parent window
103
+ */
104
+ class MessageHandler {
105
+ constructor(parentOrigin = '*', timeout = 30000) {
106
+ this.messageId = 0;
107
+ this.pendingRequests = new Map();
108
+ this.messageListener = null;
109
+ this.parentOrigin = parentOrigin;
110
+ this.timeout = timeout;
111
+ }
112
+ /**
113
+ * Initialize message listener
114
+ */
115
+ init() {
116
+ if (this.messageListener) {
117
+ return; // Already initialized
118
+ }
119
+ this.messageListener = (event) => {
120
+ this.handleMessage(event);
121
+ };
122
+ window.addEventListener('message', this.messageListener);
123
+ }
124
+ /**
125
+ * Cleanup message listener
126
+ */
127
+ destroy() {
128
+ if (this.messageListener) {
129
+ window.removeEventListener('message', this.messageListener);
130
+ this.messageListener = null;
131
+ }
132
+ // Clear all pending requests
133
+ this.pendingRequests.forEach((request) => {
134
+ clearTimeout(request.timeout);
135
+ request.reject(new Error('Message handler destroyed'));
136
+ });
137
+ this.pendingRequests.clear();
138
+ }
139
+ /**
140
+ * Send message to parent window and wait for response
141
+ */
142
+ sendMessage(type, payload) {
143
+ return new Promise((resolve, reject) => {
144
+ const id = ++this.messageId;
145
+ // Set up timeout
146
+ const timeout = setTimeout(() => {
147
+ this.pendingRequests.delete(id);
148
+ reject(new TimeoutError(`Request ${type} timed out after ${this.timeout}ms`));
149
+ }, this.timeout);
150
+ // Store request
151
+ this.pendingRequests.set(id, {
152
+ resolve: (value) => {
153
+ clearTimeout(timeout);
154
+ resolve(value);
155
+ },
156
+ reject: (error) => {
157
+ clearTimeout(timeout);
158
+ reject(error);
159
+ },
160
+ timeout,
161
+ });
162
+ // Send message to parent
163
+ const message = {
164
+ type,
165
+ id,
166
+ payload,
167
+ };
168
+ if (window.parent) {
169
+ window.parent.postMessage(message, this.parentOrigin);
170
+ }
171
+ else {
172
+ clearTimeout(timeout);
173
+ this.pendingRequests.delete(id);
174
+ reject(new Error('No parent window found'));
175
+ }
176
+ });
177
+ }
178
+ /**
179
+ * Handle incoming messages from parent
180
+ */
181
+ handleMessage(event) {
182
+ // Validate origin if specified
183
+ if (this.parentOrigin !== '*' && event.origin !== this.parentOrigin) {
184
+ return; // Ignore messages from unauthorized origins
185
+ }
186
+ const message = event.data;
187
+ // Handle response messages (have ID)
188
+ if (message.id !== undefined) {
189
+ const request = this.pendingRequests.get(message.id);
190
+ if (request) {
191
+ this.pendingRequests.delete(message.id);
192
+ if (message.type === MessageType.PAYMENT_RESPONSE) {
193
+ const response = message.payload;
194
+ if (response.success) {
195
+ request.resolve(response);
196
+ }
197
+ else {
198
+ request.reject(new Error(response.error || 'Payment failed'));
199
+ }
200
+ }
201
+ else if (message.type === MessageType.WALLET_ADDRESS_RESPONSE) {
202
+ request.resolve(message.payload);
203
+ }
204
+ else if (message.type === MessageType.INIT) {
205
+ request.resolve(message.payload);
206
+ }
207
+ else {
208
+ // Generic response - resolve with payload
209
+ request.resolve(message.payload);
210
+ }
211
+ }
212
+ }
213
+ }
214
+ /**
215
+ * Send one-way message (no response expected)
216
+ */
217
+ sendOneWay(type, payload) {
218
+ const message = {
219
+ type,
220
+ payload,
221
+ };
222
+ if (window.parent) {
223
+ window.parent.postMessage(message, this.parentOrigin);
224
+ }
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Wallet address management
230
+ */
231
+ class WalletManager {
232
+ constructor(messageHandler, isIframe, apiClient) {
233
+ this.walletAddress = null;
234
+ this.walletConnected = false;
235
+ this.apiClient = null;
236
+ this.walletChangeCallbacks = [];
237
+ this.messageHandler = messageHandler;
238
+ this.isIframe = isIframe;
239
+ this.apiClient = apiClient || null;
240
+ }
241
+ /**
242
+ * Get wallet address from parent window (iframe) or API (non-iframe)
243
+ */
244
+ async getWalletAddress() {
245
+ if (this.isIframe) {
246
+ // Iframe mode: use postMessage
247
+ try {
248
+ const response = await this.messageHandler.sendMessage(MessageType.GET_WALLET_ADDRESS);
249
+ this.walletAddress = response.walletAddress;
250
+ this.walletConnected = response.connected;
251
+ return this.walletAddress;
252
+ }
253
+ catch (error) {
254
+ // If request fails, assume wallet not connected
255
+ this.walletAddress = null;
256
+ this.walletConnected = false;
257
+ return null;
258
+ }
259
+ }
260
+ else {
261
+ // Non-iframe mode: use API
262
+ if (!this.apiClient) {
263
+ throw new Error('API client not initialized. Ensure apiBaseURL is configured in SDK config.');
264
+ }
265
+ try {
266
+ const response = await this.apiClient.getWalletAddress();
267
+ this.walletAddress = response.walletAddress;
268
+ this.walletConnected = response.connected;
269
+ return this.walletAddress;
270
+ }
271
+ catch (error) {
272
+ this.walletAddress = null;
273
+ this.walletConnected = false;
274
+ return null;
275
+ }
276
+ }
277
+ }
278
+ /**
279
+ * Check if wallet is connected
280
+ */
281
+ async isWalletConnected() {
282
+ // If we have cached address, return true
283
+ if (this.walletAddress) {
284
+ return true;
285
+ }
286
+ // Otherwise, fetch from parent (iframe) or API (non-iframe)
287
+ const address = await this.getWalletAddress();
288
+ return address !== null;
289
+ }
290
+ /**
291
+ * Listen for wallet connection changes
292
+ */
293
+ onWalletChange(callback) {
294
+ this.walletChangeCallbacks.push(callback);
295
+ }
296
+ /**
297
+ * Remove wallet change listener
298
+ */
299
+ offWalletChange(callback) {
300
+ const index = this.walletChangeCallbacks.indexOf(callback);
301
+ if (index > -1) {
302
+ this.walletChangeCallbacks.splice(index, 1);
303
+ }
304
+ }
305
+ /**
306
+ * Update wallet status (called by SDK when receiving WALLET_UPDATE message)
307
+ */
308
+ updateWalletStatus(walletInfo) {
309
+ const oldAddress = this.walletAddress;
310
+ const oldConnected = this.walletConnected;
311
+ this.walletAddress = walletInfo.address;
312
+ this.walletConnected = walletInfo.connected;
313
+ // Notify callbacks if status changed
314
+ if (oldAddress !== this.walletAddress || oldConnected !== this.walletConnected) {
315
+ this.walletChangeCallbacks.forEach((callback) => {
316
+ try {
317
+ callback(this.walletConnected, this.walletAddress);
318
+ }
319
+ catch (error) {
320
+ console.error('Error in wallet change callback:', error);
321
+ }
322
+ });
323
+ }
324
+ }
325
+ /**
326
+ * Get current cached wallet address (without fetching)
327
+ */
328
+ getCachedWalletAddress() {
329
+ return this.walletAddress;
330
+ }
331
+ /**
332
+ * Get current cached connection status (without fetching)
333
+ */
334
+ getCachedConnectionStatus() {
335
+ return this.walletConnected;
336
+ }
337
+ /**
338
+ * Clear cached wallet data
339
+ */
340
+ clearCache() {
341
+ this.walletAddress = null;
342
+ this.walletConnected = false;
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Payment processing
348
+ */
349
+ class PaymentManager {
350
+ constructor(gameId, isIframe, messageHandler, walletManager, apiClient) {
351
+ this.apiClient = null;
352
+ this.gameId = gameId;
353
+ this.isIframe = isIframe;
354
+ this.messageHandler = messageHandler;
355
+ this.walletManager = walletManager;
356
+ this.apiClient = apiClient || null;
357
+ }
358
+ /**
359
+ * Pay to play - one-time payment to access game
360
+ * @param amount - Payment amount
361
+ * @param token - Token type: 'SOL', 'USDC', or custom token symbol/mint address
362
+ * @param txSignature - Transaction signature (required for non-iframe mode)
363
+ */
364
+ async payToPlay(amount, token, txSignature) {
365
+ this.validatePaymentParams(amount, token);
366
+ // Check wallet connection
367
+ const isConnected = await this.walletManager.isWalletConnected();
368
+ if (!isConnected) {
369
+ throw new WalletNotConnectedError();
370
+ }
371
+ const request = {
372
+ amount,
373
+ token,
374
+ type: 'pay_to_play',
375
+ gameId: this.gameId,
376
+ txSignature, // Optional for iframe mode, required for non-iframe
377
+ };
378
+ if (this.isIframe) {
379
+ // Iframe mode: use postMessage
380
+ try {
381
+ const response = await this.messageHandler.sendMessage(MessageType.PAYMENT_REQUEST, request);
382
+ if (!response.success) {
383
+ throw new PaymentFailedError(response.error || 'Payment failed', response.txSignature);
384
+ }
385
+ if (!response.txSignature) {
386
+ throw new PaymentFailedError('Payment succeeded but no transaction signature received');
387
+ }
388
+ if (!response.amount || !response.token || !response.timestamp) {
389
+ throw new PaymentFailedError('Payment succeeded but incomplete payment details received');
390
+ }
391
+ return {
392
+ success: true,
393
+ txSignature: response.txSignature,
394
+ amount: response.amount,
395
+ token: response.token,
396
+ timestamp: response.timestamp,
397
+ purchaseId: response.purchaseId,
398
+ platformFee: response.platformFee,
399
+ developerAmount: response.developerAmount,
400
+ };
401
+ }
402
+ catch (error) {
403
+ if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
404
+ throw error;
405
+ }
406
+ throw new PaymentFailedError(error instanceof Error ? error.message : 'Payment failed');
407
+ }
408
+ }
409
+ else {
410
+ // Non-iframe mode: use API
411
+ if (!this.apiClient) {
412
+ throw new Error('API client not initialized. Ensure apiBaseURL is configured in SDK config.');
413
+ }
414
+ if (!txSignature) {
415
+ throw new PaymentFailedError('Transaction signature is required for non-iframe payments. ' +
416
+ 'Please sign the transaction first and provide the signature.');
417
+ }
418
+ try {
419
+ const response = await this.apiClient.sendPaymentRequest(this.gameId, {
420
+ ...request,
421
+ txSignature,
422
+ });
423
+ if (!response.success) {
424
+ throw new PaymentFailedError(response.error || 'Payment failed', response.txSignature);
425
+ }
426
+ if (!response.txSignature) {
427
+ throw new PaymentFailedError('Payment succeeded but no transaction signature received');
428
+ }
429
+ if (!response.amount || !response.token || !response.timestamp) {
430
+ throw new PaymentFailedError('Payment succeeded but incomplete payment details received');
431
+ }
432
+ return {
433
+ success: true,
434
+ txSignature: response.txSignature,
435
+ amount: response.amount,
436
+ token: response.token,
437
+ timestamp: response.timestamp,
438
+ purchaseId: response.purchaseId,
439
+ platformFee: response.platformFee,
440
+ developerAmount: response.developerAmount,
441
+ };
442
+ }
443
+ catch (error) {
444
+ if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
445
+ throw error;
446
+ }
447
+ throw new PaymentFailedError(error instanceof Error ? error.message : 'Payment failed');
448
+ }
449
+ }
450
+ }
451
+ /**
452
+ * Purchase in-game item
453
+ * @param itemId - Item identifier
454
+ * @param amount - Payment amount
455
+ * @param token - Token type: 'SOL', 'USDC', or custom token symbol/mint address
456
+ * @param txSignature - Transaction signature (required for non-iframe mode)
457
+ */
458
+ async purchaseItem(itemId, amount, token, txSignature) {
459
+ this.validatePaymentParams(amount, token);
460
+ if (!itemId || typeof itemId !== 'string' || itemId.trim().length === 0) {
461
+ throw new Error('Item ID is required and must be a non-empty string');
462
+ }
463
+ // Check wallet connection
464
+ const isConnected = await this.walletManager.isWalletConnected();
465
+ if (!isConnected) {
466
+ throw new WalletNotConnectedError();
467
+ }
468
+ const request = {
469
+ amount,
470
+ token,
471
+ type: 'in_game_purchase',
472
+ gameId: this.gameId,
473
+ itemId: itemId.trim(),
474
+ txSignature, // Optional for iframe mode, required for non-iframe
475
+ };
476
+ if (this.isIframe) {
477
+ // Iframe mode: use postMessage
478
+ try {
479
+ const response = await this.messageHandler.sendMessage(MessageType.PAYMENT_REQUEST, request);
480
+ if (!response.success) {
481
+ throw new PaymentFailedError(response.error || 'Purchase failed', response.txSignature);
482
+ }
483
+ if (!response.txSignature) {
484
+ throw new PaymentFailedError('Purchase succeeded but no transaction signature received');
485
+ }
486
+ if (!response.amount || !response.token || !response.timestamp) {
487
+ throw new PaymentFailedError('Purchase succeeded but incomplete payment details received');
488
+ }
489
+ return {
490
+ success: true,
491
+ txSignature: response.txSignature,
492
+ amount: response.amount,
493
+ token: response.token,
494
+ timestamp: response.timestamp,
495
+ purchaseId: response.purchaseId,
496
+ platformFee: response.platformFee,
497
+ developerAmount: response.developerAmount,
498
+ };
499
+ }
500
+ catch (error) {
501
+ if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
502
+ throw error;
503
+ }
504
+ throw new PaymentFailedError(error instanceof Error ? error.message : 'Purchase failed');
505
+ }
506
+ }
507
+ else {
508
+ // Non-iframe mode: use API
509
+ if (!this.apiClient) {
510
+ throw new Error('API client not initialized. Ensure apiBaseURL is configured in SDK config.');
511
+ }
512
+ if (!txSignature) {
513
+ throw new PaymentFailedError('Transaction signature is required for non-iframe purchases. ' +
514
+ 'Please sign the transaction first and provide the signature.');
515
+ }
516
+ try {
517
+ const response = await this.apiClient.sendPaymentRequest(this.gameId, {
518
+ ...request,
519
+ txSignature,
520
+ });
521
+ if (!response.success) {
522
+ throw new PaymentFailedError(response.error || 'Purchase failed', response.txSignature);
523
+ }
524
+ if (!response.txSignature) {
525
+ throw new PaymentFailedError('Purchase succeeded but no transaction signature received');
526
+ }
527
+ if (!response.amount || !response.token || !response.timestamp) {
528
+ throw new PaymentFailedError('Purchase succeeded but incomplete payment details received');
529
+ }
530
+ return {
531
+ success: true,
532
+ txSignature: response.txSignature,
533
+ amount: response.amount,
534
+ token: response.token,
535
+ timestamp: response.timestamp,
536
+ purchaseId: response.purchaseId,
537
+ platformFee: response.platformFee,
538
+ developerAmount: response.developerAmount,
539
+ };
540
+ }
541
+ catch (error) {
542
+ if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
543
+ throw error;
544
+ }
545
+ throw new PaymentFailedError(error instanceof Error ? error.message : 'Purchase failed');
546
+ }
547
+ }
548
+ }
549
+ /**
550
+ * Validate payment parameters
551
+ */
552
+ validatePaymentParams(amount, token) {
553
+ if (typeof amount !== 'number' || isNaN(amount) || amount <= 0) {
554
+ throw new InvalidAmountError();
555
+ }
556
+ if (!token || typeof token !== 'string' || token.trim().length === 0) {
557
+ throw new InvalidTokenError();
558
+ }
559
+ // SOL and USDC are always valid
560
+ // Custom tokens will be validated by the backend
561
+ }
562
+ }
563
+
564
+ /**
565
+ * API Client for non-iframe environments
566
+ * Handles direct REST API calls when SDK is not running in iframe
567
+ */
568
+ class APIClient {
569
+ constructor(baseURL) {
570
+ this.authToken = null;
571
+ this.walletAddress = null;
572
+ // Use provided base URL or detect from environment
573
+ this.baseURL = baseURL || this.detectBaseURL();
574
+ }
575
+ /**
576
+ * Detect Arcadia base URL from environment or use default
577
+ */
578
+ detectBaseURL() {
579
+ // Check for explicit base URL in config
580
+ if (typeof window !== 'undefined' && window.ARCADIA_API_URL) {
581
+ return window.ARCADIA_API_URL;
582
+ }
583
+ // Try to detect from current page (if game is hosted on Arcadia subdomain)
584
+ if (typeof window !== 'undefined') {
585
+ const hostname = window.location.hostname;
586
+ // If on arcadia.com or subdomain, use same origin
587
+ if (hostname.includes('arcadia')) {
588
+ return `${window.location.protocol}//${hostname}`;
589
+ }
590
+ }
591
+ // Default fallback (should be configured by developer)
592
+ return 'https://arcadia.com';
593
+ }
594
+ /**
595
+ * Set authentication token (from wallet authentication)
596
+ */
597
+ setAuthToken(token) {
598
+ this.authToken = token;
599
+ }
600
+ /**
601
+ * Set wallet address (for wallet-based auth)
602
+ */
603
+ setWalletAddress(address) {
604
+ this.walletAddress = address;
605
+ }
606
+ /**
607
+ * Authenticate with wallet
608
+ * Returns auth token for subsequent API calls
609
+ */
610
+ async authenticateWithWallet(walletAddress, signature, message) {
611
+ const response = await this.request('/api/auth/wallet/connect', {
612
+ method: 'POST',
613
+ body: {
614
+ walletAddress,
615
+ signature,
616
+ message,
617
+ },
618
+ });
619
+ // Store auth token from response
620
+ // Note: You may need to extract token from response based on your auth implementation
621
+ if (response.token) {
622
+ this.authToken = response.token;
623
+ }
624
+ return response;
625
+ }
626
+ /**
627
+ * Get wallet address (for non-iframe)
628
+ * Requires authentication
629
+ */
630
+ async getWalletAddress() {
631
+ if (!this.authToken && !this.walletAddress) {
632
+ return {
633
+ walletAddress: null,
634
+ connected: false,
635
+ };
636
+ }
637
+ // If we have wallet address cached, return it
638
+ if (this.walletAddress) {
639
+ return {
640
+ walletAddress: this.walletAddress,
641
+ connected: true,
642
+ };
643
+ }
644
+ // Otherwise, fetch from profile endpoint
645
+ try {
646
+ const profile = await this.request('/api/profile', {
647
+ method: 'GET',
648
+ });
649
+ if (profile?.walletAddress) {
650
+ this.walletAddress = profile.walletAddress;
651
+ return {
652
+ walletAddress: profile.walletAddress,
653
+ connected: true,
654
+ };
655
+ }
656
+ }
657
+ catch (error) {
658
+ console.warn('Failed to fetch wallet address:', error);
659
+ }
660
+ return {
661
+ walletAddress: null,
662
+ connected: false,
663
+ };
664
+ }
665
+ /**
666
+ * Send payment request via API
667
+ */
668
+ async sendPaymentRequest(gameId, request) {
669
+ const endpoint = request.type === 'pay_to_play'
670
+ ? `/api/games/${gameId}/pay-to-play`
671
+ : `/api/games/${gameId}/purchase-item`;
672
+ return await this.request(endpoint, {
673
+ method: 'POST',
674
+ body: {
675
+ amount: request.amount,
676
+ token: request.token,
677
+ txSignature: request.txSignature, // App must provide this
678
+ itemId: request.itemId,
679
+ },
680
+ });
681
+ }
682
+ /**
683
+ * Update playtime/stats
684
+ */
685
+ async updatePlaytime(gameId, playtimeHours, status) {
686
+ await this.request('/api/library', {
687
+ method: 'POST',
688
+ body: {
689
+ gameId,
690
+ playtimeHours,
691
+ status: status || 'playing',
692
+ },
693
+ });
694
+ }
695
+ /**
696
+ * Update online status
697
+ */
698
+ async updateOnlineStatus(isOnline, gameId) {
699
+ await this.request('/api/online-status', {
700
+ method: 'POST',
701
+ body: {
702
+ isOnline,
703
+ gameId,
704
+ },
705
+ });
706
+ }
707
+ /**
708
+ * Generic request method
709
+ */
710
+ async request(endpoint, options = {}) {
711
+ const url = `${this.baseURL}${endpoint}`;
712
+ const headers = {
713
+ 'Content-Type': 'application/json',
714
+ ...options.headers,
715
+ };
716
+ // Add auth token if available
717
+ if (this.authToken) {
718
+ headers['Authorization'] = `Bearer ${this.authToken}`;
719
+ }
720
+ const response = await fetch(url, {
721
+ method: options.method || 'GET',
722
+ headers,
723
+ body: options.body ? JSON.stringify(options.body) : undefined,
724
+ });
725
+ if (!response.ok) {
726
+ const error = await response.json().catch(() => ({ error: 'Request failed' }));
727
+ throw new Error(error.error || `Request failed with status ${response.status}`);
728
+ }
729
+ return await response.json();
730
+ }
731
+ }
732
+
733
+ /**
734
+ * Main Arcadia Game SDK class
735
+ */
736
+ class ArcadiaSDK {
737
+ /**
738
+ * Create new SDK instance
739
+ */
740
+ constructor(config) {
741
+ this.initialized = false;
742
+ this.apiClient = null;
743
+ // Validate config
744
+ if (!config || !config.gameId || typeof config.gameId !== 'string' || config.gameId.trim().length === 0) {
745
+ throw new InvalidConfigError('gameId is required and must be a non-empty string');
746
+ }
747
+ this.config = {
748
+ gameId: config.gameId.trim(),
749
+ parentOrigin: config.parentOrigin || '*',
750
+ timeout: config.timeout || 30000,
751
+ };
752
+ // Detect iframe environment
753
+ this.isIframe = typeof window !== 'undefined' && window.self !== window.top;
754
+ // Initialize message handler
755
+ this.messageHandler = new MessageHandler(this.config.parentOrigin, this.config.timeout);
756
+ // Initialize managers
757
+ this.walletManager = new WalletManager(this.messageHandler, this.isIframe, this.apiClient);
758
+ this.paymentManager = new PaymentManager(this.config.gameId, this.isIframe, this.messageHandler, this.walletManager, this.apiClient);
759
+ // Set up message listener if in iframe
760
+ if (this.isIframe) {
761
+ this.messageHandler.init();
762
+ this.setupMessageListener();
763
+ }
764
+ else {
765
+ // Initialize API client for non-iframe environments
766
+ this.apiClient = new APIClient(this.config.apiBaseURL);
767
+ // Set auth token if provided
768
+ if (this.config.authToken) {
769
+ this.apiClient.setAuthToken(this.config.authToken);
770
+ }
771
+ // Set wallet address if provided
772
+ if (this.config.walletAddress) {
773
+ this.apiClient.setWalletAddress(this.config.walletAddress);
774
+ }
775
+ }
776
+ }
777
+ /**
778
+ * Initialize SDK and request initialization data from parent (iframe) or API (non-iframe)
779
+ */
780
+ async init() {
781
+ if (this.initialized) {
782
+ return; // Already initialized
783
+ }
784
+ if (this.isIframe) {
785
+ // Iframe mode: use postMessage
786
+ try {
787
+ // Request initialization from parent
788
+ const initData = await this.messageHandler.sendMessage(MessageType.INIT_REQUEST);
789
+ // Update wallet status from init data
790
+ if (initData.wallet) {
791
+ this.walletManager.updateWalletStatus(initData.wallet);
792
+ }
793
+ // Notify parent that game is ready
794
+ this.messageHandler.sendOneWay(MessageType.GAME_READY);
795
+ this.initialized = true;
796
+ }
797
+ catch (error) {
798
+ // Initialization failed, but SDK can still be used
799
+ // Wallet functions will fetch on demand
800
+ this.initialized = true;
801
+ console.warn('SDK initialization failed:', error);
802
+ }
803
+ }
804
+ else {
805
+ // Non-iframe mode: use API
806
+ if (!this.apiClient) {
807
+ throw new Error('API client not initialized. Ensure apiBaseURL is configured.');
808
+ }
809
+ try {
810
+ // Fetch wallet address from API
811
+ const walletResponse = await this.apiClient.getWalletAddress();
812
+ // Update wallet manager with API response
813
+ this.walletManager.updateWalletStatus({
814
+ connected: walletResponse.connected,
815
+ address: walletResponse.walletAddress,
816
+ });
817
+ this.initialized = true;
818
+ }
819
+ catch (error) {
820
+ // Initialization failed, but SDK can still be used
821
+ this.initialized = true;
822
+ console.warn('SDK API initialization failed:', error);
823
+ }
824
+ }
825
+ }
826
+ /**
827
+ * Get wallet address - use this as user identifier
828
+ * Games should link all save data to this wallet address
829
+ * Returns null if wallet not connected
830
+ */
831
+ async getWalletAddress() {
832
+ return this.walletManager.getWalletAddress();
833
+ }
834
+ /**
835
+ * Check if wallet is connected
836
+ */
837
+ async isWalletConnected() {
838
+ return this.walletManager.isWalletConnected();
839
+ }
840
+ /**
841
+ * Listen for wallet connection changes
842
+ */
843
+ onWalletChange(callback) {
844
+ this.walletManager.onWalletChange(callback);
845
+ }
846
+ /**
847
+ * Remove wallet change listener
848
+ */
849
+ offWalletChange(callback) {
850
+ this.walletManager.offWalletChange(callback);
851
+ }
852
+ /**
853
+ * Payment methods
854
+ */
855
+ get payment() {
856
+ return {
857
+ /**
858
+ * Pay to play - one-time payment to start/access game
859
+ */
860
+ payToPlay: (amount, token) => {
861
+ return this.paymentManager.payToPlay(amount, token);
862
+ },
863
+ /**
864
+ * In-game purchase - buy items, upgrades, etc.
865
+ */
866
+ purchaseItem: (itemId, amount, token) => {
867
+ return this.paymentManager.purchaseItem(itemId, amount, token);
868
+ },
869
+ };
870
+ }
871
+ /**
872
+ * Get SDK configuration
873
+ */
874
+ getConfig() {
875
+ return { ...this.config };
876
+ }
877
+ /**
878
+ * Check if SDK is running in iframe
879
+ */
880
+ isInIframe() {
881
+ return this.isIframe;
882
+ }
883
+ /**
884
+ * Check if SDK is initialized
885
+ */
886
+ isInitialized() {
887
+ return this.initialized;
888
+ }
889
+ /**
890
+ * Cleanup SDK resources
891
+ */
892
+ destroy() {
893
+ this.messageHandler.destroy();
894
+ this.walletManager.clearCache();
895
+ this.initialized = false;
896
+ }
897
+ /**
898
+ * Set up message listener for wallet updates
899
+ */
900
+ setupMessageListener() {
901
+ // Additional listener for wallet updates (not request/response)
902
+ const listener = (event) => {
903
+ // Validate origin if specified
904
+ if (this.config.parentOrigin !== '*' && event.origin !== this.config.parentOrigin) {
905
+ return;
906
+ }
907
+ const message = event.data;
908
+ // Handle wallet update messages
909
+ if (message.type === MessageType.WALLET_UPDATE && message.payload) {
910
+ const walletInfo = message.payload;
911
+ this.walletManager.updateWalletStatus(walletInfo);
912
+ }
913
+ };
914
+ window.addEventListener('message', listener);
915
+ // Store listener for cleanup (though SDK doesn't typically need cleanup)
916
+ // This is handled by messageHandler.destroy()
917
+ }
918
+ }
919
+
920
+ /**
921
+ * Arcadia Game SDK
922
+ *
923
+ * SDK for integrating Arcadia wallet and payment features into Web3 games.
924
+ * Works in iframe environments by communicating with parent window via postMessage.
925
+ */
926
+ // Main SDK class
927
+
928
+ export { APIClient, ArcadiaSDK, ArcadiaSDKError, InvalidAmountError, InvalidConfigError, InvalidTokenError, NotInIframeError, PaymentFailedError, TimeoutError, WalletNotConnectedError, ArcadiaSDK as default };
929
+ //# sourceMappingURL=index.js.map