@arcadiasol/game-sdk 1.0.0 → 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.
package/dist/esm/index.js CHANGED
@@ -229,45 +229,61 @@ class MessageHandler {
229
229
  * Wallet address management
230
230
  */
231
231
  class WalletManager {
232
- constructor(messageHandler, isIframe) {
232
+ constructor(messageHandler, isIframe, apiClient) {
233
233
  this.walletAddress = null;
234
234
  this.walletConnected = false;
235
+ this.apiClient = null;
235
236
  this.walletChangeCallbacks = [];
236
237
  this.messageHandler = messageHandler;
237
238
  this.isIframe = isIframe;
239
+ this.apiClient = apiClient || null;
238
240
  }
239
241
  /**
240
- * Get wallet address from parent window
242
+ * Get wallet address from parent window (iframe) or API (non-iframe)
241
243
  */
242
244
  async getWalletAddress() {
243
- if (!this.isIframe) {
244
- throw new NotInIframeError();
245
- }
246
- try {
247
- const response = await this.messageHandler.sendMessage(MessageType.GET_WALLET_ADDRESS);
248
- this.walletAddress = response.walletAddress;
249
- this.walletConnected = response.connected;
250
- return this.walletAddress;
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
+ }
251
259
  }
252
- catch (error) {
253
- // If request fails, assume wallet not connected
254
- this.walletAddress = null;
255
- this.walletConnected = false;
256
- return null;
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
+ }
257
276
  }
258
277
  }
259
278
  /**
260
279
  * Check if wallet is connected
261
280
  */
262
281
  async isWalletConnected() {
263
- if (!this.isIframe) {
264
- return false;
265
- }
266
282
  // If we have cached address, return true
267
283
  if (this.walletAddress) {
268
284
  return true;
269
285
  }
270
- // Otherwise, fetch from parent
286
+ // Otherwise, fetch from parent (iframe) or API (non-iframe)
271
287
  const address = await this.getWalletAddress();
272
288
  return address !== null;
273
289
  }
@@ -331,20 +347,22 @@ class WalletManager {
331
347
  * Payment processing
332
348
  */
333
349
  class PaymentManager {
334
- constructor(gameId, isIframe, messageHandler, walletManager) {
350
+ constructor(gameId, isIframe, messageHandler, walletManager, apiClient) {
351
+ this.apiClient = null;
335
352
  this.gameId = gameId;
336
353
  this.isIframe = isIframe;
337
354
  this.messageHandler = messageHandler;
338
355
  this.walletManager = walletManager;
356
+ this.apiClient = apiClient || null;
339
357
  }
340
358
  /**
341
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)
342
363
  */
343
- async payToPlay(amount, token) {
364
+ async payToPlay(amount, token, txSignature) {
344
365
  this.validatePaymentParams(amount, token);
345
- if (!this.isIframe) {
346
- throw new NotInIframeError('payToPlay only works when the game is running in an Arcadia iframe');
347
- }
348
366
  // Check wallet connection
349
367
  const isConnected = await this.walletManager.isWalletConnected();
350
368
  if (!isConnected) {
@@ -355,47 +373,93 @@ class PaymentManager {
355
373
  token,
356
374
  type: 'pay_to_play',
357
375
  gameId: this.gameId,
376
+ txSignature, // Optional for iframe mode, required for non-iframe
358
377
  };
359
- try {
360
- const response = await this.messageHandler.sendMessage(MessageType.PAYMENT_REQUEST, request);
361
- if (!response.success) {
362
- throw new PaymentFailedError(response.error || 'Payment failed', response.txSignature);
363
- }
364
- if (!response.txSignature) {
365
- throw new PaymentFailedError('Payment succeeded but no transaction signature received');
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
+ };
366
401
  }
367
- if (!response.amount || !response.token || !response.timestamp) {
368
- throw new PaymentFailedError('Payment succeeded but incomplete payment details received');
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');
369
407
  }
370
- return {
371
- success: true,
372
- txSignature: response.txSignature,
373
- amount: response.amount,
374
- token: response.token,
375
- timestamp: response.timestamp,
376
- purchaseId: response.purchaseId,
377
- platformFee: response.platformFee,
378
- developerAmount: response.developerAmount,
379
- };
380
408
  }
381
- catch (error) {
382
- if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
383
- throw error;
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');
384
448
  }
385
- throw new PaymentFailedError(error instanceof Error ? error.message : 'Payment failed');
386
449
  }
387
450
  }
388
451
  /**
389
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)
390
457
  */
391
- async purchaseItem(itemId, amount, token) {
458
+ async purchaseItem(itemId, amount, token, txSignature) {
392
459
  this.validatePaymentParams(amount, token);
393
460
  if (!itemId || typeof itemId !== 'string' || itemId.trim().length === 0) {
394
461
  throw new Error('Item ID is required and must be a non-empty string');
395
462
  }
396
- if (!this.isIframe) {
397
- throw new NotInIframeError('purchaseItem only works when the game is running in an Arcadia iframe');
398
- }
399
463
  // Check wallet connection
400
464
  const isConnected = await this.walletManager.isWalletConnected();
401
465
  if (!isConnected) {
@@ -407,34 +471,79 @@ class PaymentManager {
407
471
  type: 'in_game_purchase',
408
472
  gameId: this.gameId,
409
473
  itemId: itemId.trim(),
474
+ txSignature, // Optional for iframe mode, required for non-iframe
410
475
  };
411
- try {
412
- const response = await this.messageHandler.sendMessage(MessageType.PAYMENT_REQUEST, request);
413
- if (!response.success) {
414
- throw new PaymentFailedError(response.error || 'Purchase failed', response.txSignature);
415
- }
416
- if (!response.txSignature) {
417
- throw new PaymentFailedError('Purchase succeeded but no transaction signature received');
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
+ };
418
499
  }
419
- if (!response.amount || !response.token || !response.timestamp) {
420
- throw new PaymentFailedError('Purchase succeeded but incomplete payment details received');
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');
421
505
  }
422
- return {
423
- success: true,
424
- txSignature: response.txSignature,
425
- amount: response.amount,
426
- token: response.token,
427
- timestamp: response.timestamp,
428
- purchaseId: response.purchaseId,
429
- platformFee: response.platformFee,
430
- developerAmount: response.developerAmount,
431
- };
432
506
  }
433
- catch (error) {
434
- if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
435
- throw error;
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');
436
546
  }
437
- throw new PaymentFailedError(error instanceof Error ? error.message : 'Purchase failed');
438
547
  }
439
548
  }
440
549
  /**
@@ -444,9 +553,180 @@ class PaymentManager {
444
553
  if (typeof amount !== 'number' || isNaN(amount) || amount <= 0) {
445
554
  throw new InvalidAmountError();
446
555
  }
447
- if (token !== 'SOL' && token !== 'USDC') {
556
+ if (!token || typeof token !== 'string' || token.trim().length === 0) {
448
557
  throw new InvalidTokenError();
449
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();
450
730
  }
451
731
  }
452
732
 
@@ -459,6 +739,7 @@ class ArcadiaSDK {
459
739
  */
460
740
  constructor(config) {
461
741
  this.initialized = false;
742
+ this.apiClient = null;
462
743
  // Validate config
463
744
  if (!config || !config.gameId || typeof config.gameId !== 'string' || config.gameId.trim().length === 0) {
464
745
  throw new InvalidConfigError('gameId is required and must be a non-empty string');
@@ -473,42 +754,73 @@ class ArcadiaSDK {
473
754
  // Initialize message handler
474
755
  this.messageHandler = new MessageHandler(this.config.parentOrigin, this.config.timeout);
475
756
  // Initialize managers
476
- this.walletManager = new WalletManager(this.messageHandler, this.isIframe);
477
- this.paymentManager = new PaymentManager(this.config.gameId, this.isIframe, this.messageHandler, this.walletManager);
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);
478
759
  // Set up message listener if in iframe
479
760
  if (this.isIframe) {
480
761
  this.messageHandler.init();
481
762
  this.setupMessageListener();
482
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
+ }
483
776
  }
484
777
  /**
485
- * Initialize SDK and request initialization data from parent
778
+ * Initialize SDK and request initialization data from parent (iframe) or API (non-iframe)
486
779
  */
487
780
  async init() {
488
781
  if (this.initialized) {
489
782
  return; // Already initialized
490
783
  }
491
- if (!this.isIframe) {
492
- // Not in iframe - can't initialize
493
- this.initialized = true;
494
- return;
495
- }
496
- try {
497
- // Request initialization from parent
498
- const initData = await this.messageHandler.sendMessage(MessageType.INIT_REQUEST);
499
- // Update wallet status from init data
500
- if (initData.wallet) {
501
- this.walletManager.updateWalletStatus(initData.wallet);
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);
502
802
  }
503
- // Notify parent that game is ready
504
- this.messageHandler.sendOneWay(MessageType.GAME_READY);
505
- this.initialized = true;
506
803
  }
507
- catch (error) {
508
- // Initialization failed, but SDK can still be used
509
- // Wallet functions will fetch on demand
510
- this.initialized = true;
511
- console.warn('SDK initialization failed:', error);
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
+ }
512
824
  }
513
825
  }
514
826
  /**
@@ -613,5 +925,5 @@ class ArcadiaSDK {
613
925
  */
614
926
  // Main SDK class
615
927
 
616
- export { ArcadiaSDK, ArcadiaSDKError, InvalidAmountError, InvalidConfigError, InvalidTokenError, NotInIframeError, PaymentFailedError, TimeoutError, WalletNotConnectedError, ArcadiaSDK as default };
928
+ export { APIClient, ArcadiaSDK, ArcadiaSDKError, InvalidAmountError, InvalidConfigError, InvalidTokenError, NotInIframeError, PaymentFailedError, TimeoutError, WalletNotConnectedError, ArcadiaSDK as default };
617
929
  //# sourceMappingURL=index.js.map