@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.
@@ -235,45 +235,61 @@
235
235
  * Wallet address management
236
236
  */
237
237
  class WalletManager {
238
- constructor(messageHandler, isIframe) {
238
+ constructor(messageHandler, isIframe, apiClient) {
239
239
  this.walletAddress = null;
240
240
  this.walletConnected = false;
241
+ this.apiClient = null;
241
242
  this.walletChangeCallbacks = [];
242
243
  this.messageHandler = messageHandler;
243
244
  this.isIframe = isIframe;
245
+ this.apiClient = apiClient || null;
244
246
  }
245
247
  /**
246
- * Get wallet address from parent window
248
+ * Get wallet address from parent window (iframe) or API (non-iframe)
247
249
  */
248
250
  async getWalletAddress() {
249
- if (!this.isIframe) {
250
- throw new NotInIframeError();
251
- }
252
- try {
253
- const response = await this.messageHandler.sendMessage(MessageType.GET_WALLET_ADDRESS);
254
- this.walletAddress = response.walletAddress;
255
- this.walletConnected = response.connected;
256
- return this.walletAddress;
251
+ if (this.isIframe) {
252
+ // Iframe mode: use postMessage
253
+ try {
254
+ const response = await this.messageHandler.sendMessage(MessageType.GET_WALLET_ADDRESS);
255
+ this.walletAddress = response.walletAddress;
256
+ this.walletConnected = response.connected;
257
+ return this.walletAddress;
258
+ }
259
+ catch (error) {
260
+ // If request fails, assume wallet not connected
261
+ this.walletAddress = null;
262
+ this.walletConnected = false;
263
+ return null;
264
+ }
257
265
  }
258
- catch (error) {
259
- // If request fails, assume wallet not connected
260
- this.walletAddress = null;
261
- this.walletConnected = false;
262
- return null;
266
+ else {
267
+ // Non-iframe mode: use API
268
+ if (!this.apiClient) {
269
+ throw new Error('API client not initialized. Ensure apiBaseURL is configured in SDK config.');
270
+ }
271
+ try {
272
+ const response = await this.apiClient.getWalletAddress();
273
+ this.walletAddress = response.walletAddress;
274
+ this.walletConnected = response.connected;
275
+ return this.walletAddress;
276
+ }
277
+ catch (error) {
278
+ this.walletAddress = null;
279
+ this.walletConnected = false;
280
+ return null;
281
+ }
263
282
  }
264
283
  }
265
284
  /**
266
285
  * Check if wallet is connected
267
286
  */
268
287
  async isWalletConnected() {
269
- if (!this.isIframe) {
270
- return false;
271
- }
272
288
  // If we have cached address, return true
273
289
  if (this.walletAddress) {
274
290
  return true;
275
291
  }
276
- // Otherwise, fetch from parent
292
+ // Otherwise, fetch from parent (iframe) or API (non-iframe)
277
293
  const address = await this.getWalletAddress();
278
294
  return address !== null;
279
295
  }
@@ -337,20 +353,22 @@
337
353
  * Payment processing
338
354
  */
339
355
  class PaymentManager {
340
- constructor(gameId, isIframe, messageHandler, walletManager) {
356
+ constructor(gameId, isIframe, messageHandler, walletManager, apiClient) {
357
+ this.apiClient = null;
341
358
  this.gameId = gameId;
342
359
  this.isIframe = isIframe;
343
360
  this.messageHandler = messageHandler;
344
361
  this.walletManager = walletManager;
362
+ this.apiClient = apiClient || null;
345
363
  }
346
364
  /**
347
365
  * Pay to play - one-time payment to access game
366
+ * @param amount - Payment amount
367
+ * @param token - Token type: 'SOL', 'USDC', or custom token symbol/mint address
368
+ * @param txSignature - Transaction signature (required for non-iframe mode)
348
369
  */
349
- async payToPlay(amount, token) {
370
+ async payToPlay(amount, token, txSignature) {
350
371
  this.validatePaymentParams(amount, token);
351
- if (!this.isIframe) {
352
- throw new NotInIframeError('payToPlay only works when the game is running in an Arcadia iframe');
353
- }
354
372
  // Check wallet connection
355
373
  const isConnected = await this.walletManager.isWalletConnected();
356
374
  if (!isConnected) {
@@ -361,47 +379,93 @@
361
379
  token,
362
380
  type: 'pay_to_play',
363
381
  gameId: this.gameId,
382
+ txSignature, // Optional for iframe mode, required for non-iframe
364
383
  };
365
- try {
366
- const response = await this.messageHandler.sendMessage(MessageType.PAYMENT_REQUEST, request);
367
- if (!response.success) {
368
- throw new PaymentFailedError(response.error || 'Payment failed', response.txSignature);
369
- }
370
- if (!response.txSignature) {
371
- throw new PaymentFailedError('Payment succeeded but no transaction signature received');
384
+ if (this.isIframe) {
385
+ // Iframe mode: use postMessage
386
+ try {
387
+ const response = await this.messageHandler.sendMessage(MessageType.PAYMENT_REQUEST, request);
388
+ if (!response.success) {
389
+ throw new PaymentFailedError(response.error || 'Payment failed', response.txSignature);
390
+ }
391
+ if (!response.txSignature) {
392
+ throw new PaymentFailedError('Payment succeeded but no transaction signature received');
393
+ }
394
+ if (!response.amount || !response.token || !response.timestamp) {
395
+ throw new PaymentFailedError('Payment succeeded but incomplete payment details received');
396
+ }
397
+ return {
398
+ success: true,
399
+ txSignature: response.txSignature,
400
+ amount: response.amount,
401
+ token: response.token,
402
+ timestamp: response.timestamp,
403
+ purchaseId: response.purchaseId,
404
+ platformFee: response.platformFee,
405
+ developerAmount: response.developerAmount,
406
+ };
372
407
  }
373
- if (!response.amount || !response.token || !response.timestamp) {
374
- throw new PaymentFailedError('Payment succeeded but incomplete payment details received');
408
+ catch (error) {
409
+ if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
410
+ throw error;
411
+ }
412
+ throw new PaymentFailedError(error instanceof Error ? error.message : 'Payment failed');
375
413
  }
376
- return {
377
- success: true,
378
- txSignature: response.txSignature,
379
- amount: response.amount,
380
- token: response.token,
381
- timestamp: response.timestamp,
382
- purchaseId: response.purchaseId,
383
- platformFee: response.platformFee,
384
- developerAmount: response.developerAmount,
385
- };
386
414
  }
387
- catch (error) {
388
- if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
389
- throw error;
415
+ else {
416
+ // Non-iframe mode: use API
417
+ if (!this.apiClient) {
418
+ throw new Error('API client not initialized. Ensure apiBaseURL is configured in SDK config.');
419
+ }
420
+ if (!txSignature) {
421
+ throw new PaymentFailedError('Transaction signature is required for non-iframe payments. ' +
422
+ 'Please sign the transaction first and provide the signature.');
423
+ }
424
+ try {
425
+ const response = await this.apiClient.sendPaymentRequest(this.gameId, {
426
+ ...request,
427
+ txSignature,
428
+ });
429
+ if (!response.success) {
430
+ throw new PaymentFailedError(response.error || 'Payment failed', response.txSignature);
431
+ }
432
+ if (!response.txSignature) {
433
+ throw new PaymentFailedError('Payment succeeded but no transaction signature received');
434
+ }
435
+ if (!response.amount || !response.token || !response.timestamp) {
436
+ throw new PaymentFailedError('Payment succeeded but incomplete payment details received');
437
+ }
438
+ return {
439
+ success: true,
440
+ txSignature: response.txSignature,
441
+ amount: response.amount,
442
+ token: response.token,
443
+ timestamp: response.timestamp,
444
+ purchaseId: response.purchaseId,
445
+ platformFee: response.platformFee,
446
+ developerAmount: response.developerAmount,
447
+ };
448
+ }
449
+ catch (error) {
450
+ if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
451
+ throw error;
452
+ }
453
+ throw new PaymentFailedError(error instanceof Error ? error.message : 'Payment failed');
390
454
  }
391
- throw new PaymentFailedError(error instanceof Error ? error.message : 'Payment failed');
392
455
  }
393
456
  }
394
457
  /**
395
458
  * Purchase in-game item
459
+ * @param itemId - Item identifier
460
+ * @param amount - Payment amount
461
+ * @param token - Token type: 'SOL', 'USDC', or custom token symbol/mint address
462
+ * @param txSignature - Transaction signature (required for non-iframe mode)
396
463
  */
397
- async purchaseItem(itemId, amount, token) {
464
+ async purchaseItem(itemId, amount, token, txSignature) {
398
465
  this.validatePaymentParams(amount, token);
399
466
  if (!itemId || typeof itemId !== 'string' || itemId.trim().length === 0) {
400
467
  throw new Error('Item ID is required and must be a non-empty string');
401
468
  }
402
- if (!this.isIframe) {
403
- throw new NotInIframeError('purchaseItem only works when the game is running in an Arcadia iframe');
404
- }
405
469
  // Check wallet connection
406
470
  const isConnected = await this.walletManager.isWalletConnected();
407
471
  if (!isConnected) {
@@ -413,34 +477,79 @@
413
477
  type: 'in_game_purchase',
414
478
  gameId: this.gameId,
415
479
  itemId: itemId.trim(),
480
+ txSignature, // Optional for iframe mode, required for non-iframe
416
481
  };
417
- try {
418
- const response = await this.messageHandler.sendMessage(MessageType.PAYMENT_REQUEST, request);
419
- if (!response.success) {
420
- throw new PaymentFailedError(response.error || 'Purchase failed', response.txSignature);
421
- }
422
- if (!response.txSignature) {
423
- throw new PaymentFailedError('Purchase succeeded but no transaction signature received');
482
+ if (this.isIframe) {
483
+ // Iframe mode: use postMessage
484
+ try {
485
+ const response = await this.messageHandler.sendMessage(MessageType.PAYMENT_REQUEST, request);
486
+ if (!response.success) {
487
+ throw new PaymentFailedError(response.error || 'Purchase failed', response.txSignature);
488
+ }
489
+ if (!response.txSignature) {
490
+ throw new PaymentFailedError('Purchase succeeded but no transaction signature received');
491
+ }
492
+ if (!response.amount || !response.token || !response.timestamp) {
493
+ throw new PaymentFailedError('Purchase succeeded but incomplete payment details received');
494
+ }
495
+ return {
496
+ success: true,
497
+ txSignature: response.txSignature,
498
+ amount: response.amount,
499
+ token: response.token,
500
+ timestamp: response.timestamp,
501
+ purchaseId: response.purchaseId,
502
+ platformFee: response.platformFee,
503
+ developerAmount: response.developerAmount,
504
+ };
424
505
  }
425
- if (!response.amount || !response.token || !response.timestamp) {
426
- throw new PaymentFailedError('Purchase succeeded but incomplete payment details received');
506
+ catch (error) {
507
+ if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
508
+ throw error;
509
+ }
510
+ throw new PaymentFailedError(error instanceof Error ? error.message : 'Purchase failed');
427
511
  }
428
- return {
429
- success: true,
430
- txSignature: response.txSignature,
431
- amount: response.amount,
432
- token: response.token,
433
- timestamp: response.timestamp,
434
- purchaseId: response.purchaseId,
435
- platformFee: response.platformFee,
436
- developerAmount: response.developerAmount,
437
- };
438
512
  }
439
- catch (error) {
440
- if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
441
- throw error;
513
+ else {
514
+ // Non-iframe mode: use API
515
+ if (!this.apiClient) {
516
+ throw new Error('API client not initialized. Ensure apiBaseURL is configured in SDK config.');
517
+ }
518
+ if (!txSignature) {
519
+ throw new PaymentFailedError('Transaction signature is required for non-iframe purchases. ' +
520
+ 'Please sign the transaction first and provide the signature.');
521
+ }
522
+ try {
523
+ const response = await this.apiClient.sendPaymentRequest(this.gameId, {
524
+ ...request,
525
+ txSignature,
526
+ });
527
+ if (!response.success) {
528
+ throw new PaymentFailedError(response.error || 'Purchase failed', response.txSignature);
529
+ }
530
+ if (!response.txSignature) {
531
+ throw new PaymentFailedError('Purchase succeeded but no transaction signature received');
532
+ }
533
+ if (!response.amount || !response.token || !response.timestamp) {
534
+ throw new PaymentFailedError('Purchase succeeded but incomplete payment details received');
535
+ }
536
+ return {
537
+ success: true,
538
+ txSignature: response.txSignature,
539
+ amount: response.amount,
540
+ token: response.token,
541
+ timestamp: response.timestamp,
542
+ purchaseId: response.purchaseId,
543
+ platformFee: response.platformFee,
544
+ developerAmount: response.developerAmount,
545
+ };
546
+ }
547
+ catch (error) {
548
+ if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
549
+ throw error;
550
+ }
551
+ throw new PaymentFailedError(error instanceof Error ? error.message : 'Purchase failed');
442
552
  }
443
- throw new PaymentFailedError(error instanceof Error ? error.message : 'Purchase failed');
444
553
  }
445
554
  }
446
555
  /**
@@ -450,9 +559,180 @@
450
559
  if (typeof amount !== 'number' || isNaN(amount) || amount <= 0) {
451
560
  throw new InvalidAmountError();
452
561
  }
453
- if (token !== 'SOL' && token !== 'USDC') {
562
+ if (!token || typeof token !== 'string' || token.trim().length === 0) {
454
563
  throw new InvalidTokenError();
455
564
  }
565
+ // SOL and USDC are always valid
566
+ // Custom tokens will be validated by the backend
567
+ }
568
+ }
569
+
570
+ /**
571
+ * API Client for non-iframe environments
572
+ * Handles direct REST API calls when SDK is not running in iframe
573
+ */
574
+ class APIClient {
575
+ constructor(baseURL) {
576
+ this.authToken = null;
577
+ this.walletAddress = null;
578
+ // Use provided base URL or detect from environment
579
+ this.baseURL = baseURL || this.detectBaseURL();
580
+ }
581
+ /**
582
+ * Detect Arcadia base URL from environment or use default
583
+ */
584
+ detectBaseURL() {
585
+ // Check for explicit base URL in config
586
+ if (typeof window !== 'undefined' && window.ARCADIA_API_URL) {
587
+ return window.ARCADIA_API_URL;
588
+ }
589
+ // Try to detect from current page (if game is hosted on Arcadia subdomain)
590
+ if (typeof window !== 'undefined') {
591
+ const hostname = window.location.hostname;
592
+ // If on arcadia.com or subdomain, use same origin
593
+ if (hostname.includes('arcadia')) {
594
+ return `${window.location.protocol}//${hostname}`;
595
+ }
596
+ }
597
+ // Default fallback (should be configured by developer)
598
+ return 'https://arcadia.com';
599
+ }
600
+ /**
601
+ * Set authentication token (from wallet authentication)
602
+ */
603
+ setAuthToken(token) {
604
+ this.authToken = token;
605
+ }
606
+ /**
607
+ * Set wallet address (for wallet-based auth)
608
+ */
609
+ setWalletAddress(address) {
610
+ this.walletAddress = address;
611
+ }
612
+ /**
613
+ * Authenticate with wallet
614
+ * Returns auth token for subsequent API calls
615
+ */
616
+ async authenticateWithWallet(walletAddress, signature, message) {
617
+ const response = await this.request('/api/auth/wallet/connect', {
618
+ method: 'POST',
619
+ body: {
620
+ walletAddress,
621
+ signature,
622
+ message,
623
+ },
624
+ });
625
+ // Store auth token from response
626
+ // Note: You may need to extract token from response based on your auth implementation
627
+ if (response.token) {
628
+ this.authToken = response.token;
629
+ }
630
+ return response;
631
+ }
632
+ /**
633
+ * Get wallet address (for non-iframe)
634
+ * Requires authentication
635
+ */
636
+ async getWalletAddress() {
637
+ if (!this.authToken && !this.walletAddress) {
638
+ return {
639
+ walletAddress: null,
640
+ connected: false,
641
+ };
642
+ }
643
+ // If we have wallet address cached, return it
644
+ if (this.walletAddress) {
645
+ return {
646
+ walletAddress: this.walletAddress,
647
+ connected: true,
648
+ };
649
+ }
650
+ // Otherwise, fetch from profile endpoint
651
+ try {
652
+ const profile = await this.request('/api/profile', {
653
+ method: 'GET',
654
+ });
655
+ if (profile?.walletAddress) {
656
+ this.walletAddress = profile.walletAddress;
657
+ return {
658
+ walletAddress: profile.walletAddress,
659
+ connected: true,
660
+ };
661
+ }
662
+ }
663
+ catch (error) {
664
+ console.warn('Failed to fetch wallet address:', error);
665
+ }
666
+ return {
667
+ walletAddress: null,
668
+ connected: false,
669
+ };
670
+ }
671
+ /**
672
+ * Send payment request via API
673
+ */
674
+ async sendPaymentRequest(gameId, request) {
675
+ const endpoint = request.type === 'pay_to_play'
676
+ ? `/api/games/${gameId}/pay-to-play`
677
+ : `/api/games/${gameId}/purchase-item`;
678
+ return await this.request(endpoint, {
679
+ method: 'POST',
680
+ body: {
681
+ amount: request.amount,
682
+ token: request.token,
683
+ txSignature: request.txSignature, // App must provide this
684
+ itemId: request.itemId,
685
+ },
686
+ });
687
+ }
688
+ /**
689
+ * Update playtime/stats
690
+ */
691
+ async updatePlaytime(gameId, playtimeHours, status) {
692
+ await this.request('/api/library', {
693
+ method: 'POST',
694
+ body: {
695
+ gameId,
696
+ playtimeHours,
697
+ status: status || 'playing',
698
+ },
699
+ });
700
+ }
701
+ /**
702
+ * Update online status
703
+ */
704
+ async updateOnlineStatus(isOnline, gameId) {
705
+ await this.request('/api/online-status', {
706
+ method: 'POST',
707
+ body: {
708
+ isOnline,
709
+ gameId,
710
+ },
711
+ });
712
+ }
713
+ /**
714
+ * Generic request method
715
+ */
716
+ async request(endpoint, options = {}) {
717
+ const url = `${this.baseURL}${endpoint}`;
718
+ const headers = {
719
+ 'Content-Type': 'application/json',
720
+ ...options.headers,
721
+ };
722
+ // Add auth token if available
723
+ if (this.authToken) {
724
+ headers['Authorization'] = `Bearer ${this.authToken}`;
725
+ }
726
+ const response = await fetch(url, {
727
+ method: options.method || 'GET',
728
+ headers,
729
+ body: options.body ? JSON.stringify(options.body) : undefined,
730
+ });
731
+ if (!response.ok) {
732
+ const error = await response.json().catch(() => ({ error: 'Request failed' }));
733
+ throw new Error(error.error || `Request failed with status ${response.status}`);
734
+ }
735
+ return await response.json();
456
736
  }
457
737
  }
458
738
 
@@ -465,6 +745,7 @@
465
745
  */
466
746
  constructor(config) {
467
747
  this.initialized = false;
748
+ this.apiClient = null;
468
749
  // Validate config
469
750
  if (!config || !config.gameId || typeof config.gameId !== 'string' || config.gameId.trim().length === 0) {
470
751
  throw new InvalidConfigError('gameId is required and must be a non-empty string');
@@ -479,42 +760,73 @@
479
760
  // Initialize message handler
480
761
  this.messageHandler = new MessageHandler(this.config.parentOrigin, this.config.timeout);
481
762
  // Initialize managers
482
- this.walletManager = new WalletManager(this.messageHandler, this.isIframe);
483
- this.paymentManager = new PaymentManager(this.config.gameId, this.isIframe, this.messageHandler, this.walletManager);
763
+ this.walletManager = new WalletManager(this.messageHandler, this.isIframe, this.apiClient);
764
+ this.paymentManager = new PaymentManager(this.config.gameId, this.isIframe, this.messageHandler, this.walletManager, this.apiClient);
484
765
  // Set up message listener if in iframe
485
766
  if (this.isIframe) {
486
767
  this.messageHandler.init();
487
768
  this.setupMessageListener();
488
769
  }
770
+ else {
771
+ // Initialize API client for non-iframe environments
772
+ this.apiClient = new APIClient(this.config.apiBaseURL);
773
+ // Set auth token if provided
774
+ if (this.config.authToken) {
775
+ this.apiClient.setAuthToken(this.config.authToken);
776
+ }
777
+ // Set wallet address if provided
778
+ if (this.config.walletAddress) {
779
+ this.apiClient.setWalletAddress(this.config.walletAddress);
780
+ }
781
+ }
489
782
  }
490
783
  /**
491
- * Initialize SDK and request initialization data from parent
784
+ * Initialize SDK and request initialization data from parent (iframe) or API (non-iframe)
492
785
  */
493
786
  async init() {
494
787
  if (this.initialized) {
495
788
  return; // Already initialized
496
789
  }
497
- if (!this.isIframe) {
498
- // Not in iframe - can't initialize
499
- this.initialized = true;
500
- return;
501
- }
502
- try {
503
- // Request initialization from parent
504
- const initData = await this.messageHandler.sendMessage(MessageType.INIT_REQUEST);
505
- // Update wallet status from init data
506
- if (initData.wallet) {
507
- this.walletManager.updateWalletStatus(initData.wallet);
790
+ if (this.isIframe) {
791
+ // Iframe mode: use postMessage
792
+ try {
793
+ // Request initialization from parent
794
+ const initData = await this.messageHandler.sendMessage(MessageType.INIT_REQUEST);
795
+ // Update wallet status from init data
796
+ if (initData.wallet) {
797
+ this.walletManager.updateWalletStatus(initData.wallet);
798
+ }
799
+ // Notify parent that game is ready
800
+ this.messageHandler.sendOneWay(MessageType.GAME_READY);
801
+ this.initialized = true;
802
+ }
803
+ catch (error) {
804
+ // Initialization failed, but SDK can still be used
805
+ // Wallet functions will fetch on demand
806
+ this.initialized = true;
807
+ console.warn('SDK initialization failed:', error);
508
808
  }
509
- // Notify parent that game is ready
510
- this.messageHandler.sendOneWay(MessageType.GAME_READY);
511
- this.initialized = true;
512
809
  }
513
- catch (error) {
514
- // Initialization failed, but SDK can still be used
515
- // Wallet functions will fetch on demand
516
- this.initialized = true;
517
- console.warn('SDK initialization failed:', error);
810
+ else {
811
+ // Non-iframe mode: use API
812
+ if (!this.apiClient) {
813
+ throw new Error('API client not initialized. Ensure apiBaseURL is configured.');
814
+ }
815
+ try {
816
+ // Fetch wallet address from API
817
+ const walletResponse = await this.apiClient.getWalletAddress();
818
+ // Update wallet manager with API response
819
+ this.walletManager.updateWalletStatus({
820
+ connected: walletResponse.connected,
821
+ address: walletResponse.walletAddress,
822
+ });
823
+ this.initialized = true;
824
+ }
825
+ catch (error) {
826
+ // Initialization failed, but SDK can still be used
827
+ this.initialized = true;
828
+ console.warn('SDK API initialization failed:', error);
829
+ }
518
830
  }
519
831
  }
520
832
  /**
@@ -619,6 +931,7 @@
619
931
  */
620
932
  // Main SDK class
621
933
 
934
+ exports.APIClient = APIClient;
622
935
  exports.ArcadiaSDK = ArcadiaSDK;
623
936
  exports.ArcadiaSDKError = ArcadiaSDKError;
624
937
  exports.InvalidAmountError = InvalidAmountError;