@explorins/pers-sdk-react-native 2.0.5 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -208,6 +208,18 @@ const {
208
208
  getTokenByContract
209
209
  } = useTokens();
210
210
 
211
+ // Token balance loading (NEW in v2.1.1)
212
+ const {
213
+ tokenBalances, // Array of balances with token metadata
214
+ isLoading, // Loading state
215
+ error, // Error state
216
+ refresh // Manual refresh function
217
+ } = useTokenBalances({
218
+ availableTokens, // From useTokens()
219
+ autoLoad: true, // Auto-load on mount
220
+ refreshInterval: 30000 // Optional: refresh every 30s
221
+ });
222
+
211
223
  // Transaction history
212
224
  const {
213
225
  createTransaction,
@@ -458,8 +470,8 @@ function POSScreen() {
458
470
  }, [subscribe, isAvailable]);
459
471
 
460
472
  const handlePOSTransaction = async (
461
- userAccountId: string,
462
- businessAccountId: string,
473
+ userId: string,
474
+ businessId: string,
463
475
  amount: number,
464
476
  token: TokenDTO
465
477
  ) => {
@@ -468,8 +480,8 @@ function POSScreen() {
468
480
  amount,
469
481
  contractAddress: token.contractAddress,
470
482
  chainId: token.chainId,
471
- userAccountId, // User sending tokens
472
- businessAccountId // Business receiving & authorized to submit
483
+ userId, // User sending tokens
484
+ businessId // Business receiving & authorized to submit
473
485
  });
474
486
 
475
487
  // Create and sign transaction
@@ -653,7 +665,93 @@ function BlockchainRedemption() {
653
665
  );
654
666
  }
655
667
  ```
668
+ ### Error Handling
669
+
670
+ The SDK provides comprehensive structured error handling with utilities for consistent error management:
671
+
672
+ ```typescript
673
+ import {
674
+ PersApiError,
675
+ ErrorUtils
676
+ } from '@explorins/pers-sdk-react-native';
677
+ import { Alert } from 'react-native';
678
+
679
+ // Structured error handling
680
+ try {
681
+ await sdk.campaigns.claimCampaign({ campaignId });
682
+ } catch (error) {
683
+ if (error instanceof PersApiError) {
684
+ // Structured error with backend details
685
+ console.log('Error code:', error.code); // 'CAMPAIGN_BUSINESS_REQUIRED'
686
+ console.log('Status:', error.status); // 400
687
+ console.log('User message:', error.userMessage); // User-friendly message
688
+
689
+ // Show user-friendly error
690
+ Alert.alert('Error', error.userMessage || error.message);
691
+
692
+ // Check if retryable
693
+ if (error.retryable) {
694
+ Alert.alert('Error', error.message, [
695
+ { text: 'Cancel' },
696
+ { text: 'Retry', onPress: () => retry() }
697
+ ]);
698
+ }
699
+ } else {
700
+ // Generic error fallback with ErrorUtils
701
+ const message = ErrorUtils.getMessage(error);
702
+ Alert.alert('Error', message);
703
+ }
704
+ }
705
+
706
+ // Error utilities for any error type
707
+ const status = ErrorUtils.getStatus(error); // Extract HTTP status
708
+ const message = ErrorUtils.getMessage(error); // Extract user message
709
+ const retryable = ErrorUtils.isRetryable(error); // Check if retryable
710
+ const tokenExpired = ErrorUtils.isTokenExpiredError(error); // Detect token expiration
711
+ ```
712
+
713
+ ### Transaction Helpers
656
714
 
715
+ **NEW in v2.1.1:** Factory functions for client-side transaction workflows:
716
+
717
+ ```typescript
718
+ import {
719
+ ClientTransactionType,
720
+ buildPendingTransactionData,
721
+ extractDeadlineFromSigningData,
722
+ type PendingTransactionParams
723
+ } from '@explorins/pers-sdk-react-native';
724
+
725
+ // Build pending transaction data for QR codes, NFC, deep links, etc.
726
+ const qrData = buildPendingTransactionData(
727
+ signingResult.transactionId,
728
+ signingResult.signature,
729
+ 'EIP-712' // Default format
730
+ );
731
+
732
+ console.log(qrData);
733
+ // {
734
+ // transactionId: '...',
735
+ // signature: '0x...',
736
+ // transactionFormat: 'EIP-712',
737
+ // txType: 'PENDING_SUBMISSION'
738
+ // }
739
+
740
+ // Serialize for transfer (QR code, NFC, etc.)
741
+ const qrCodeValue = JSON.stringify(qrData);
742
+
743
+ // Extract deadline from EIP-712 signing data
744
+ const deadline = extractDeadlineFromSigningData(signingResult.signingData);
745
+ if (deadline) {
746
+ const expiryTime = new Date(deadline * 1000);
747
+ console.log('Transaction expires at:', expiryTime);
748
+ }
749
+
750
+ // Client transaction types
751
+ if (transaction.txType === ClientTransactionType.PENDING_SUBMISSION) {
752
+ // Handle pending submission flow
753
+ }
754
+ ```
657
755
  ## Security Features
658
756
 
659
757
  ### WebAuthn Authentication
@@ -927,6 +1025,21 @@ const tokens = (await getTokens()).data;
927
1025
 
928
1026
  ---
929
1027
 
1028
+ ## Documentation
1029
+
1030
+ ### Core Guides
1031
+ - **[AUTH_STATE_HANDLING.md](./docs/AUTH_STATE_HANDLING.md)** - Comprehensive guide for handling authentication state changes
1032
+ - Pattern examples for observing `isAuthenticated` state
1033
+ - Custom UI for session expiration
1034
+ - Platform-specific details (iOS Keychain, Android Keystore, Web)
1035
+ - Best practices for auth state observation
1036
+
1037
+ ### Setup & Configuration
1038
+ - **[REACT_NATIVE_PASSKEY_SETUP.md](./REACT_NATIVE_PASSKEY_SETUP.md)** - Required setup for WebAuthn/Passkey authentication
1039
+ - **[DPOP_IMPLEMENTATION_GUIDE.md](./DPOP_IMPLEMENTATION_GUIDE.md)** - DPoP security implementation details
1040
+
1041
+ ---
1042
+
930
1043
  ## Contributing
931
1044
 
932
1045
  We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
@@ -1,5 +1,6 @@
1
1
  export { useAuth } from './useAuth';
2
2
  export { useTokens } from './useTokens';
3
+ export { useTokenBalances } from './useTokenBalances';
3
4
  export { useTransactions } from './useTransactions';
4
5
  export { useTransactionSigner, SigningStatus } from './useTransactionSigner';
5
6
  export { useBusiness } from './useBusiness';
@@ -18,4 +19,5 @@ export type { RawUserData } from './useAuth';
18
19
  export type { TransactionSignerHook, SubmissionResult, AuthenticatedUser, TransactionSigningResult, StatusUpdateData, OnStatusUpdateFn, SigningStatus as SigningStatusType } from './useTransactionSigner';
19
20
  export type { AccountOwnedTokensResult, Web3Hook } from './useWeb3';
20
21
  export type { EventsHook, PersEvent, EventHandler, EventFilter, Unsubscribe } from './useEvents';
22
+ export type { TokenBalanceWithToken, UseTokenBalancesOptions, UseTokenBalancesResult } from './useTokenBalances';
21
23
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGxC,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,YAAY,EACV,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,EACjB,wBAAwB,EACxB,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,IAAI,iBAAiB,EACnC,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,wBAAwB,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACpE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGxC,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,YAAY,EACV,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,EACjB,wBAAwB,EACxB,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,IAAI,iBAAiB,EACnC,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,wBAAwB,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACpE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjG,YAAY,EACV,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,oBAAoB,CAAC"}
@@ -1,6 +1,7 @@
1
1
  // Export all hooks (only hooks, no providers to avoid circular dependency)
2
2
  export { useAuth } from './useAuth';
3
3
  export { useTokens } from './useTokens';
4
+ export { useTokenBalances } from './useTokenBalances';
4
5
  export { useTransactions } from './useTransactions';
5
6
  export { useTransactionSigner, SigningStatus } from './useTransactionSigner';
6
7
  export { useBusiness } from './useBusiness';
@@ -0,0 +1,140 @@
1
+ import { type TokenDTO } from '@explorins/pers-shared';
2
+ import type { TokenBalance } from '@explorins/pers-sdk/web3';
3
+ /**
4
+ * Token balance with associated token information for display
5
+ *
6
+ * This composite type combines blockchain balance data with token metadata,
7
+ * making it easy to display token information in UI components without
8
+ * needing to join data from multiple sources.
9
+ */
10
+ export interface TokenBalanceWithToken {
11
+ /** The balance data from blockchain */
12
+ tokenBalance: TokenBalance;
13
+ /** The token definition (contract, symbol, metadata, etc.) */
14
+ token: TokenDTO;
15
+ /** Human-readable formatted balance string (e.g., "100 XPL" or "3 stamps") */
16
+ balanceFormatted: string;
17
+ }
18
+ /**
19
+ * Configuration options for useTokenBalances hook
20
+ */
21
+ export interface UseTokenBalancesOptions {
22
+ /**
23
+ * Wallet address to check balances for (REQUIRED)
24
+ * Apps must explicitly provide the wallet address - no automatic selection
25
+ * Get from: user.wallets[selectedIndex].address
26
+ */
27
+ accountAddress: string;
28
+ /** Available tokens to check balances for (from SDK or filtered list) */
29
+ availableTokens?: TokenDTO[];
30
+ /** Whether to automatically load balances on mount/auth/token changes */
31
+ autoLoad?: boolean;
32
+ /** Optional refresh interval in milliseconds (0 = disabled) */
33
+ refreshInterval?: number;
34
+ }
35
+ /**
36
+ * Token balance loading hook return value
37
+ */
38
+ export interface UseTokenBalancesResult {
39
+ /** Array of token balances with formatted display data */
40
+ tokenBalances: TokenBalanceWithToken[];
41
+ /** Whether balances are currently loading */
42
+ isLoading: boolean;
43
+ /** Error message if loading failed */
44
+ error: string | null;
45
+ /** Manually trigger balance refresh */
46
+ refresh: () => Promise<void>;
47
+ /** Whether the hook is available (SDK initialized, user authenticated, has wallet) */
48
+ isAvailable: boolean;
49
+ /** User's wallet address being queried */
50
+ userAccountAddress: string | null;
51
+ }
52
+ /**
53
+ * React hook for loading and managing token balances
54
+ *
55
+ * Automatically handles:
56
+ * - Loading balances for ERC20 (fungible tokens/points)
57
+ * - Loading collections for ERC721 (unique NFTs/stamps)
58
+ * - Loading collections for ERC1155 (semi-fungible tokens/rewards)
59
+ * - Parallel loading with error resilience
60
+ * - Auto-refresh on authentication or token list changes
61
+ * - Formatted balance strings for display
62
+ *
63
+ * This hook consolidates complex token balance loading logic that would otherwise
64
+ * need to be duplicated across every app. It handles the differences between
65
+ * fungible and non-fungible tokens, parallel loading, error handling, and
66
+ * provides formatted display strings.
67
+ *
68
+ * @param options - Configuration options
69
+ * @returns Token balances state and actions
70
+ *
71
+ * @example
72
+ * **Basic Usage:**
73
+ * ```typescript
74
+ * import { useTokenBalances, useTokens, usePersSDK } from '@explorins/pers-sdk-react-native';
75
+ *
76
+ * function TokenBalancesScreen() {
77
+ * const { user } = usePersSDK();
78
+ * const { availableTokens } = useTokens();
79
+ *
80
+ * // App explicitly selects which wallet to use
81
+ * const walletAddress = user?.wallets?.[0]?.address;
82
+ *
83
+ * const {
84
+ * tokenBalances,
85
+ * isLoading,
86
+ * error,
87
+ * refresh
88
+ * } = useTokenBalances({
89
+ * accountAddress: walletAddress!,
90
+ * availableTokens,
91
+ * autoLoad: true
92
+ * });
93
+ *
94
+ * if (isLoading) return <Text>Loading balances...</Text>;
95
+ * if (error) return <Text>Error: {error}</Text>;
96
+ *
97
+ * return (
98
+ * <View>
99
+ * {tokenBalances.map(({ token, balanceFormatted }) => (
100
+ * <View key={token.id}>
101
+ * <Text>{token.name}: {balanceFormatted}</Text>
102
+ * </View>
103
+ * ))}
104
+ * <Button onPress={refresh} title="Refresh" />
105
+ * </View>
106
+ * );
107
+ * }
108
+ * ```
109
+ *
110
+ * @example
111
+ * **With Auto-Refresh:**
112
+ * ```typescript
113
+ * const { tokenBalances, isLoading } = useTokenBalances({
114
+ * accountAddress: walletAddress!,
115
+ * availableTokens,
116
+ * autoLoad: true,
117
+ * refreshInterval: 30000 // Refresh every 30 seconds
118
+ * });
119
+ * ```
120
+ *
121
+ * @example
122
+ * **Multi-Wallet Support:**
123
+ * ```typescript
124
+ * function MultiWalletBalances() {
125
+ * const { user } = usePersSDK();
126
+ * const [selectedWalletIndex, setSelectedWalletIndex] = useState(0);
127
+ *
128
+ * const walletAddress = user?.wallets?.[selectedWalletIndex]?.address;
129
+ *
130
+ * const { tokenBalances } = useTokenBalances({
131
+ * accountAddress: walletAddress!,
132
+ * availableTokens
133
+ * });
134
+ *
135
+ * // UI lets user switch between wallets
136
+ * }
137
+ * ```
138
+ */
139
+ export declare function useTokenBalances(options: UseTokenBalancesOptions): UseTokenBalancesResult;
140
+ //# sourceMappingURL=useTokenBalances.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useTokenBalances.d.ts","sourceRoot":"","sources":["../../src/hooks/useTokenBalances.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoB,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,0BAA0B,CAAC;AAElF;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,uCAAuC;IACvC,YAAY,EAAE,YAAY,CAAC;IAC3B,8DAA8D;IAC9D,KAAK,EAAE,QAAQ,CAAC;IAChB,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,eAAe,CAAC,EAAE,QAAQ,EAAE,CAAC;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,0DAA0D;IAC1D,aAAa,EAAE,qBAAqB,EAAE,CAAC;IACvC,6CAA6C;IAC7C,SAAS,EAAE,OAAO,CAAC;IACnB,sCAAsC;IACtC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,uCAAuC;IACvC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,sFAAsF;IACtF,WAAW,EAAE,OAAO,CAAC;IACrB,0CAA0C;IAC1C,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,sBAAsB,CAgJzF"}
@@ -0,0 +1,213 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { usePersSDK } from '../providers/PersSDKProvider';
3
+ import { useWeb3 } from './useWeb3';
4
+ import { NativeTokenTypes } from '@explorins/pers-shared';
5
+ /**
6
+ * React hook for loading and managing token balances
7
+ *
8
+ * Automatically handles:
9
+ * - Loading balances for ERC20 (fungible tokens/points)
10
+ * - Loading collections for ERC721 (unique NFTs/stamps)
11
+ * - Loading collections for ERC1155 (semi-fungible tokens/rewards)
12
+ * - Parallel loading with error resilience
13
+ * - Auto-refresh on authentication or token list changes
14
+ * - Formatted balance strings for display
15
+ *
16
+ * This hook consolidates complex token balance loading logic that would otherwise
17
+ * need to be duplicated across every app. It handles the differences between
18
+ * fungible and non-fungible tokens, parallel loading, error handling, and
19
+ * provides formatted display strings.
20
+ *
21
+ * @param options - Configuration options
22
+ * @returns Token balances state and actions
23
+ *
24
+ * @example
25
+ * **Basic Usage:**
26
+ * ```typescript
27
+ * import { useTokenBalances, useTokens, usePersSDK } from '@explorins/pers-sdk-react-native';
28
+ *
29
+ * function TokenBalancesScreen() {
30
+ * const { user } = usePersSDK();
31
+ * const { availableTokens } = useTokens();
32
+ *
33
+ * // App explicitly selects which wallet to use
34
+ * const walletAddress = user?.wallets?.[0]?.address;
35
+ *
36
+ * const {
37
+ * tokenBalances,
38
+ * isLoading,
39
+ * error,
40
+ * refresh
41
+ * } = useTokenBalances({
42
+ * accountAddress: walletAddress!,
43
+ * availableTokens,
44
+ * autoLoad: true
45
+ * });
46
+ *
47
+ * if (isLoading) return <Text>Loading balances...</Text>;
48
+ * if (error) return <Text>Error: {error}</Text>;
49
+ *
50
+ * return (
51
+ * <View>
52
+ * {tokenBalances.map(({ token, balanceFormatted }) => (
53
+ * <View key={token.id}>
54
+ * <Text>{token.name}: {balanceFormatted}</Text>
55
+ * </View>
56
+ * ))}
57
+ * <Button onPress={refresh} title="Refresh" />
58
+ * </View>
59
+ * );
60
+ * }
61
+ * ```
62
+ *
63
+ * @example
64
+ * **With Auto-Refresh:**
65
+ * ```typescript
66
+ * const { tokenBalances, isLoading } = useTokenBalances({
67
+ * accountAddress: walletAddress!,
68
+ * availableTokens,
69
+ * autoLoad: true,
70
+ * refreshInterval: 30000 // Refresh every 30 seconds
71
+ * });
72
+ * ```
73
+ *
74
+ * @example
75
+ * **Multi-Wallet Support:**
76
+ * ```typescript
77
+ * function MultiWalletBalances() {
78
+ * const { user } = usePersSDK();
79
+ * const [selectedWalletIndex, setSelectedWalletIndex] = useState(0);
80
+ *
81
+ * const walletAddress = user?.wallets?.[selectedWalletIndex]?.address;
82
+ *
83
+ * const { tokenBalances } = useTokenBalances({
84
+ * accountAddress: walletAddress!,
85
+ * availableTokens
86
+ * });
87
+ *
88
+ * // UI lets user switch between wallets
89
+ * }
90
+ * ```
91
+ */
92
+ export function useTokenBalances(options) {
93
+ const { accountAddress, availableTokens = [], autoLoad = true, refreshInterval = 0 } = options;
94
+ const { isAuthenticated } = usePersSDK();
95
+ const web3 = useWeb3();
96
+ const [tokenBalances, setTokenBalances] = useState([]);
97
+ const [isLoading, setIsLoading] = useState(false);
98
+ const [error, setError] = useState(null);
99
+ // Check if the hook is available for use
100
+ const isAvailable = web3.isAvailable && isAuthenticated && !!accountAddress;
101
+ /**
102
+ * Process balance for ERC20 fungible tokens (points/credits)
103
+ */
104
+ const processPointsBalance = useCallback(async (token) => {
105
+ if (!web3.getTokenBalance || !accountAddress)
106
+ return null;
107
+ try {
108
+ const request = {
109
+ accountAddress,
110
+ contractAddress: token.contractAddress,
111
+ abi: token.abi,
112
+ tokenId: '',
113
+ chainId: token.chainId
114
+ };
115
+ const balanceResult = await web3.getTokenBalance(request);
116
+ if (!balanceResult || !balanceResult.hasBalance)
117
+ return null;
118
+ return {
119
+ tokenBalance: balanceResult,
120
+ token,
121
+ balanceFormatted: `${balanceResult.balance} ${token.symbol || 'tokens'}`
122
+ };
123
+ }
124
+ catch (err) {
125
+ console.warn(`[useTokenBalances] Failed to load balance for ${token.symbol}:`, err);
126
+ return null;
127
+ }
128
+ }, [web3.getTokenBalance, accountAddress]);
129
+ /**
130
+ * Process collection for ERC721/ERC1155 non-fungible tokens (stamps/rewards)
131
+ */
132
+ const processRewardsBalance = useCallback(async (token) => {
133
+ if (!web3.getAccountOwnedTokensFromContract || !accountAddress)
134
+ return [];
135
+ try {
136
+ const result = await web3.getAccountOwnedTokensFromContract(accountAddress, token);
137
+ if (result.totalOwned === 0)
138
+ return [];
139
+ const balanceUnit = token.type === NativeTokenTypes.ERC1155 ? 'reward' : 'stamp';
140
+ // Return one item per tokenId (for individual display in lists)
141
+ return result.ownedTokens.map(ownedToken => ({
142
+ tokenBalance: ownedToken,
143
+ token,
144
+ balanceFormatted: `${ownedToken.balance} ${balanceUnit}${ownedToken.balance !== 1 ? 's' : ''}`
145
+ }));
146
+ }
147
+ catch (err) {
148
+ console.warn(`[useTokenBalances] Failed to load collection for ${token.symbol}:`, err);
149
+ return [];
150
+ }
151
+ }, [web3.getAccountOwnedTokensFromContract, accountAddress]);
152
+ /**
153
+ * Load all token balances
154
+ */
155
+ const loadBalances = useCallback(async () => {
156
+ if (!isAvailable || availableTokens.length === 0) {
157
+ return;
158
+ }
159
+ setIsLoading(true);
160
+ setError(null);
161
+ try {
162
+ // Split by category: points (fungible credits) vs rewards (stamps/collectibles)
163
+ const points = availableTokens.filter(t => t.type === NativeTokenTypes.ERC20);
164
+ const rewards = availableTokens.filter(t => t.type === NativeTokenTypes.ERC721 || t.type === NativeTokenTypes.ERC1155);
165
+ // Load all balances in parallel (individual errors handled per-token)
166
+ const [pointsResults, rewardsResults] = await Promise.all([
167
+ Promise.all(points.map(processPointsBalance)),
168
+ Promise.all(rewards.map(processRewardsBalance))
169
+ ]);
170
+ // Combine results, filtering out nulls (failed loads return null/empty)
171
+ const allBalances = [
172
+ ...pointsResults.filter(Boolean),
173
+ ...rewardsResults.flat()
174
+ ];
175
+ setTokenBalances(allBalances);
176
+ // Set error only if no balances loaded at all
177
+ if (allBalances.length === 0 && availableTokens.length > 0) {
178
+ setError('No token balances could be loaded');
179
+ }
180
+ }
181
+ catch (err) {
182
+ const errorMessage = err instanceof Error ? err.message : 'Failed to load balances';
183
+ setError(errorMessage);
184
+ console.error('[useTokenBalances] Error loading balances:', err);
185
+ }
186
+ finally {
187
+ setIsLoading(false);
188
+ }
189
+ }, [isAvailable, availableTokens, processPointsBalance, processRewardsBalance]);
190
+ // Auto-load balances when dependencies change
191
+ useEffect(() => {
192
+ if (autoLoad && isAvailable && availableTokens.length > 0) {
193
+ loadBalances();
194
+ }
195
+ }, [autoLoad, isAvailable, availableTokens.length, loadBalances]);
196
+ // Optional auto-refresh interval
197
+ useEffect(() => {
198
+ if (refreshInterval > 0 && isAvailable && availableTokens.length > 0) {
199
+ const intervalId = setInterval(() => {
200
+ loadBalances();
201
+ }, refreshInterval);
202
+ return () => clearInterval(intervalId);
203
+ }
204
+ }, [refreshInterval, isAvailable, availableTokens.length, loadBalances]);
205
+ return {
206
+ tokenBalances,
207
+ isLoading,
208
+ error,
209
+ refresh: loadBalances,
210
+ isAvailable,
211
+ userAccountAddress: accountAddress
212
+ };
213
+ }
@@ -1,8 +1,7 @@
1
- import type { TokenBalance, TokenBalanceRequest, TokenCollectionRequest, TokenCollection, TokenMetadata } from '@explorins/pers-sdk/web3';
1
+ import type { TokenBalance, TokenBalanceRequest, TokenCollectionRequest, TokenCollection, TokenMetadata, AccountOwnedTokensResult } from '@explorins/pers-sdk/web3';
2
2
  import type { ChainData } from '@explorins/pers-sdk/web3-chain';
3
3
  import type { TokenDTO } from '@explorins/pers-shared';
4
- import type { AccountOwnedTokensResult } from '@explorins/pers-sdk';
5
- export type { AccountOwnedTokensResult } from '@explorins/pers-sdk';
4
+ export type { AccountOwnedTokensResult } from '@explorins/pers-sdk/web3';
6
5
  /**
7
6
  * React hook for Web3 operations in the PERS SDK
8
7
  *
@@ -1 +1 @@
1
- {"version":3,"file":"useWeb3.d.ts","sourceRoot":"","sources":["../../src/hooks/useWeb3.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,sBAAsB,EACtB,eAAe,EACf,aAAa,EACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAGpE,YAAY,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,eAAO,MAAM,OAAO;+BA0BkC,mBAAmB,KAAG,QAAQ,YAAY,CAAC;gCAiC1C,mBAAmB,KAAG,QAAQ,aAAa,GAAG,IAAI,CAAC;kCAcjD,sBAAsB,KAAG,QAAQ,eAAe,CAAC;0BAczD,MAAM,WAAW,MAAM,KAAG,QAAQ,MAAM,CAAC;wCAc3B,MAAM,WAAW,MAAM,KAAG,QAAQ,aAAa,GAAG,IAAI,CAAC;gCAc/D,MAAM,KAAG,QAAQ,SAAS,GAAG,IAAI,CAAC;8BAcpC,MAAM,WAAW,MAAM,QAAQ,SAAS,GAAG,IAAI,KAAG,QAAQ,MAAM,CAAC;6BAiCxE,QAAQ,KAAG,MAAM,EAAE,GAAG,SAAS;wDAuCzD,MAAM,SACf,QAAQ,cACJ,MAAM,KAChB,QAAQ,wBAAwB,CAAC;6CAqBlB,MAAM,SACf,QAAQ,cACJ,MAAM,KAChB,sBAAsB;;CAqB1B,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC"}
1
+ {"version":3,"file":"useWeb3.d.ts","sourceRoot":"","sources":["../../src/hooks/useWeb3.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,sBAAsB,EACtB,eAAe,EACf,aAAa,EACb,wBAAwB,EACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAGvD,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,eAAO,MAAM,OAAO;+BAmCkC,mBAAmB,KAAG,QAAQ,YAAY,CAAC;gCAiC1C,mBAAmB,KAAG,QAAQ,aAAa,GAAG,IAAI,CAAC;kCAcjD,sBAAsB,KAAG,QAAQ,eAAe,CAAC;0BAczD,MAAM,WAAW,MAAM,KAAG,QAAQ,MAAM,CAAC;wCAc3B,MAAM,WAAW,MAAM,KAAG,QAAQ,aAAa,GAAG,IAAI,CAAC;gCAc/D,MAAM,KAAG,QAAQ,SAAS,GAAG,IAAI,CAAC;8BAcpC,MAAM,WAAW,MAAM,QAAQ,SAAS,GAAG,IAAI,KAAG,QAAQ,MAAM,CAAC;6BAiCxE,QAAQ,KAAG,MAAM,EAAE,GAAG,SAAS;wDAuCzD,MAAM,SACf,QAAQ,cACJ,MAAM,KAChB,QAAQ,wBAAwB,CAAC;6CAqBlB,MAAM,SACf,QAAQ,cACJ,MAAM,KAChB,sBAAsB;;CAqB1B,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC"}