@explorins/pers-sdk-react-native 2.3.7 → 2.3.8

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1266 -1244
  3. package/dist/__tests__/PersSDKProvider.test.js +33 -0
  4. package/dist/hooks/useBusiness.d.ts +2 -2
  5. package/dist/hooks/useBusiness.d.ts.map +1 -1
  6. package/dist/hooks/useBusiness.js +2 -2
  7. package/dist/hooks/useIPFSUrl.d.ts +3 -3
  8. package/dist/hooks/useIPFSUrl.d.ts.map +1 -1
  9. package/dist/hooks/useIPFSUrl.js +7 -7
  10. package/dist/hooks/useWeb3.js +4 -4
  11. package/dist/index.js +2400 -487
  12. package/dist/index.js.map +1 -1
  13. package/dist/providers/PersSDKProvider.d.ts +4 -0
  14. package/dist/providers/PersSDKProvider.d.ts.map +1 -1
  15. package/dist/providers/PersSDKProvider.js +14 -0
  16. package/dist/providers/react-native-http-client.d.ts +2 -0
  17. package/dist/providers/react-native-http-client.d.ts.map +1 -1
  18. package/dist/providers/react-native-http-client.js +5 -2
  19. package/dist/storage/rn-secure-storage.d.ts.map +1 -1
  20. package/dist/storage/rn-secure-storage.js +21 -8
  21. package/package.json +3 -3
  22. package/src/__tests__/PersSDKProvider.test.tsx +484 -420
  23. package/src/__tests__/setup.ts +40 -40
  24. package/src/hooks/index.ts +60 -60
  25. package/src/hooks/useAnalytics.ts +327 -327
  26. package/src/hooks/useAuth.ts +265 -265
  27. package/src/hooks/useBookings.ts +191 -191
  28. package/src/hooks/useBusiness.ts +251 -251
  29. package/src/hooks/useCampaigns.ts +500 -500
  30. package/src/hooks/useDonations.ts +66 -66
  31. package/src/hooks/useEvents.ts +372 -372
  32. package/src/hooks/useFiles.ts +155 -155
  33. package/src/hooks/useIPFSUrl.ts +192 -192
  34. package/src/hooks/usePurchases.ts +65 -65
  35. package/src/hooks/useRedemptions.ts +467 -467
  36. package/src/hooks/useTenants.ts +72 -72
  37. package/src/hooks/useTokenBalances.ts +385 -385
  38. package/src/hooks/useTokens.ts +313 -313
  39. package/src/hooks/useTransactionSigner.ts +611 -611
  40. package/src/hooks/useTransactions.ts +299 -299
  41. package/src/hooks/useTriggerSources.ts +301 -301
  42. package/src/hooks/useUserStatus.ts +61 -61
  43. package/src/hooks/useUsers.ts +167 -167
  44. package/src/hooks/useWeb3.ts +355 -355
  45. package/src/index.ts +777 -777
  46. package/src/polyfills/index.ts +160 -160
  47. package/src/polyfills/web3-polyfills.ts +97 -97
  48. package/src/providers/PersSDKProvider.tsx +393 -374
  49. package/src/providers/react-native-auth-provider.ts +110 -110
  50. package/src/providers/react-native-http-client.ts +150 -145
  51. package/src/providers/rn-dpop-provider.ts +190 -190
  52. package/src/storage/async-storage-token-storage.ts +185 -185
  53. package/src/storage/rn-secure-storage.ts +182 -170
  54. package/src/types/react-native-globals.d.ts +45 -45
  55. package/src/utils/index.ts +8 -8
  56. package/src/utils/platform-detection.ts +86 -86
  57. package/dist/index-CzLxqePZ.js +0 -7717
  58. package/dist/index-CzLxqePZ.js.map +0 -1
@@ -1,385 +1,385 @@
1
- import { useCallback, useEffect, useState, useRef } from 'react';
2
- import { usePersSDK } from '../providers/PersSDKProvider';
3
- import { useWeb3 } from './useWeb3';
4
- import { NativeTokenTypes, type TokenDTO } from '@explorins/pers-sdk';
5
- import type { TokenBalance, TokenBalanceRequest } from '@explorins/pers-sdk/web3';
6
-
7
- /**
8
- * Token balance with associated token information for display
9
- *
10
- * This composite type combines blockchain balance data with token metadata,
11
- * making it easy to display token information in UI components without
12
- * needing to join data from multiple sources.
13
- *
14
- * ## CRITICAL: Token ID for Transactions
15
- *
16
- * For ERC721 (stamps) and ERC1155 (rewards), use `tokenBalance.tokenId` as the
17
- * `contractTokenId` when creating transactions. This is the **on-chain token ID**
18
- * assigned by the blockchain, NOT the metadata definition ID.
19
- *
20
- * | Token Type | tokenId meaning | Example |
21
- * |------------|-----------------|---------|
22
- * | ERC20 | `null` or `""` | N/A (fungible points) |
23
- * | ERC721 | NFT instance ID | `"46"` (on-chain ID) |
24
- * | ERC1155 | Token type ID | `"16"` (semi-fungible type) |
25
- *
26
- * @example Correct usage for token burn transaction
27
- * ```typescript
28
- * const stampBalance = tokenBalances.find(b => b.token.type === 'ERC721');
29
- *
30
- * const request = buildPOSBurnRequest({
31
- * amount: 1,
32
- * contractAddress: stampBalance.token.contractAddress,
33
- * contractTokenId: stampBalance.tokenBalance.tokenId, // ✅ Correct
34
- * chainId: stampBalance.token.chainId,
35
- * userId: userId,
36
- * businessId: businessId
37
- * });
38
- * ```
39
- *
40
- *
41
- * */
42
- //@see {@link https://docs.explorins.com/pers-sdk/troubleshooting#stamp-voucher-transaction-fails | Troubleshooting: Stamp Transaction Fails}
43
- export interface TokenBalanceWithToken {
44
- /**
45
- * The balance data from blockchain
46
- *
47
- * **Important:** For NFT operations, use `tokenBalance.tokenId` as the `contractTokenId`
48
- * in transaction requests. This is the actual on-chain ID, not the metadata definition.
49
- */
50
- tokenBalance: TokenBalance;
51
- /** The token definition (contract, symbol, metadata, etc.) */
52
- token: TokenDTO;
53
- /** Human-readable formatted balance string (e.g., "100 XPL" or "3 stamps") */
54
- balanceFormatted: string;
55
- }
56
-
57
- /**
58
- * Configuration options for useTokenBalances hook
59
- */
60
- export interface UseTokenBalancesOptions {
61
- /**
62
- * Wallet address to check balances for (REQUIRED)
63
- * Apps must explicitly provide the wallet address - no automatic selection
64
- * Get from: user.wallets[selectedIndex].address
65
- */
66
- accountAddress: string;
67
- /** Available tokens to check balances for (from SDK or filtered list) */
68
- availableTokens?: TokenDTO[];
69
- /** Whether to automatically load balances on mount/auth/token changes */
70
- autoLoad?: boolean;
71
- /**
72
- * Fallback polling interval in milliseconds (0 = disabled)
73
- * @deprecated Use wallet events instead (refreshOnWalletEvents). Polling kept as fallback only.
74
- * @default 0
75
- */
76
- refreshInterval?: number;
77
- /**
78
- * Auto-refresh balances when wallet events are received (Transfer, Approval, etc.)
79
- * Requires `captureWalletEvents: true` in SDK config (default)
80
- * @default true
81
- */
82
- refreshOnWalletEvents?: boolean;
83
- /**
84
- * Debounce time for wallet event-triggered refreshes (milliseconds)
85
- * Prevents rapid refresh calls during batch transfers
86
- * @default 1000
87
- */
88
- walletEventDebounceMs?: number;
89
- }
90
-
91
- /**
92
- * Token balance loading hook return value
93
- */
94
- export interface UseTokenBalancesResult {
95
- /** Array of token balances with formatted display data */
96
- tokenBalances: TokenBalanceWithToken[];
97
- /** Whether balances are currently loading */
98
- isLoading: boolean;
99
- /** Error message if loading failed */
100
- error: string | null;
101
- /** Manually trigger balance refresh */
102
- refresh: () => Promise<void>;
103
- /** Whether the hook is available (SDK initialized, user authenticated, has wallet) */
104
- isAvailable: boolean;
105
- /** User's wallet address being queried */
106
- userAccountAddress: string | null;
107
- }
108
-
109
- /**
110
- * React hook for loading and managing token balances
111
- *
112
- * Automatically handles:
113
- * - Loading balances for ERC20 (fungible tokens/points)
114
- * - Loading collections for ERC721 (unique NFTs/stamps)
115
- * - Loading collections for ERC1155 (semi-fungible tokens/rewards)
116
- * - Parallel loading with error resilience
117
- * - Auto-refresh on authentication or token list changes
118
- * - Formatted balance strings for display
119
- *
120
- * This hook consolidates complex token balance loading logic that would otherwise
121
- * need to be duplicated across every app. It handles the differences between
122
- * fungible and non-fungible tokens, parallel loading, error handling, and
123
- * provides formatted display strings.
124
- *
125
- * @param options - Configuration options
126
- * @returns Token balances state and actions
127
- *
128
- * @example
129
- * **Basic Usage:**
130
- * ```typescript
131
- * import { useTokenBalances, useTokens, usePersSDK } from '@explorins/pers-sdk-react-native';
132
- * import { useState, useEffect } from 'react';
133
- *
134
- * function TokenBalancesScreen() {
135
- * const { user } = usePersSDK();
136
- * const { getTokens } = useTokens();
137
- * const [availableTokens, setAvailableTokens] = useState([]);
138
- *
139
- * // Load tokens on mount
140
- * useEffect(() => {
141
- * getTokens().then(result => setAvailableTokens(result.data));
142
- * }, []);
143
- *
144
- * // App explicitly selects which wallet to use
145
- * const walletAddress = user?.wallets?.[0]?.address;
146
- *
147
- * const {
148
- * tokenBalances,
149
- * isLoading,
150
- * error,
151
- * refresh
152
- * } = useTokenBalances({
153
- * accountAddress: walletAddress!,
154
- * availableTokens,
155
- * autoLoad: true
156
- * });
157
- *
158
- * if (isLoading) return <Text>Loading balances...</Text>;
159
- * if (error) return <Text>Error: {error}</Text>;
160
- *
161
- * return (
162
- * <View>
163
- * {tokenBalances.map(({ token, balanceFormatted }) => (
164
- * <View key={token.id}>
165
- * <Text>{token.name}: {balanceFormatted}</Text>
166
- * </View>
167
- * ))}
168
- * <Button onPress={refresh} title="Refresh" />
169
- * </View>
170
- * );
171
- * }
172
- * ```
173
- *
174
- * @example
175
- * **With Wallet Events (Real-time):
176
- * ```typescript
177
- * // Auto-refresh on Transfer, Approval, and other blockchain events
178
- * const { tokenBalances } = useTokenBalances({
179
- * accountAddress: walletAddress!,
180
- * availableTokens,
181
- * refreshOnWalletEvents: true, // Enable real-time refresh (default: true)
182
- * walletEventDebounceMs: 1000 // Debounce rapid events (default: 1000ms)
183
- * });
184
- * ```
185
- *
186
- * @example
187
- * **Multi-Wallet Support:**
188
- * ```typescript
189
- * function MultiWalletBalances() {
190
- * const { user } = usePersSDK();
191
- * const [selectedWalletIndex, setSelectedWalletIndex] = useState(0);
192
- *
193
- * const walletAddress = user?.wallets?.[selectedWalletIndex]?.address;
194
- *
195
- * const { tokenBalances } = useTokenBalances({
196
- * accountAddress: walletAddress!,
197
- * availableTokens
198
- * });
199
- *
200
- * // UI lets user switch between wallets
201
- * }
202
- * ```
203
- */
204
- export function useTokenBalances(options: UseTokenBalancesOptions): UseTokenBalancesResult {
205
- const {
206
- accountAddress,
207
- availableTokens = [],
208
- autoLoad = true,
209
- refreshInterval = 0,
210
- refreshOnWalletEvents = true,
211
- walletEventDebounceMs = 1000
212
- } = options;
213
-
214
- const { isAuthenticated, sdk } = usePersSDK();
215
- const web3 = useWeb3();
216
-
217
- const [tokenBalances, setTokenBalances] = useState<TokenBalanceWithToken[]>([]);
218
- const [isLoading, setIsLoading] = useState(false);
219
- const [error, setError] = useState<string | null>(null);
220
-
221
- // Debounce ref for wallet events
222
- const walletEventDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
223
-
224
- // Check if the hook is available for use
225
- const isAvailable = web3.isAvailable && isAuthenticated && !!accountAddress;
226
-
227
- /**
228
- * Process balance for ERC20 fungible tokens (points/credits)
229
- */
230
- const processPointsBalance = useCallback(async (token: TokenDTO): Promise<TokenBalanceWithToken | null> => {
231
- if (!web3.getTokenBalance || !accountAddress) return null;
232
-
233
- try {
234
- const request: TokenBalanceRequest = {
235
- accountAddress,
236
- contractAddress: token.contractAddress,
237
- abi: token.abi,
238
- tokenId: '',
239
- chainId: token.chainId
240
- };
241
-
242
- const balanceResult = await web3.getTokenBalance(request);
243
-
244
- if (!balanceResult || !balanceResult.hasBalance) return null;
245
-
246
- return {
247
- tokenBalance: balanceResult,
248
- token,
249
- balanceFormatted: `${balanceResult.balance} ${token.symbol || 'tokens'}`
250
- };
251
- } catch (err) {
252
- console.warn(`[useTokenBalances] Failed to load balance for ${token.symbol}:`, err);
253
- return null;
254
- }
255
- }, [web3.getTokenBalance, accountAddress]);
256
-
257
- /**
258
- * Process collection for ERC721/ERC1155 non-fungible tokens (stamps/rewards)
259
- */
260
- const processRewardsBalance = useCallback(async (token: TokenDTO): Promise<TokenBalanceWithToken[]> => {
261
- if (!web3.getAccountOwnedTokensFromContract || !accountAddress) return [];
262
-
263
- try {
264
- const result = await web3.getAccountOwnedTokensFromContract(accountAddress, token);
265
-
266
- if (result.totalOwned === 0) return [];
267
-
268
- const balanceUnit = token.type === NativeTokenTypes.ERC1155 ? 'reward' : 'stamp';
269
-
270
- // Return one item per tokenId (for individual display in lists)
271
- return result.ownedTokens.map(ownedToken => ({
272
- tokenBalance: ownedToken,
273
- token,
274
- balanceFormatted: `${ownedToken.balance} ${balanceUnit}${ownedToken.balance !== 1 ? 's' : ''}`
275
- }));
276
- } catch (err) {
277
- console.warn(`[useTokenBalances] Failed to load collection for ${token.symbol}:`, err);
278
- return [];
279
- }
280
- }, [web3.getAccountOwnedTokensFromContract, accountAddress]);
281
-
282
- /**
283
- * Load all token balances
284
- */
285
- const loadBalances = useCallback(async () => {
286
- if (!isAvailable || availableTokens.length === 0) {
287
- return;
288
- }
289
-
290
- setIsLoading(true);
291
- setError(null);
292
-
293
- try {
294
- // Split by category: points (fungible credits) vs rewards (stamps/collectibles)
295
- const points = availableTokens.filter(t => t.type === NativeTokenTypes.ERC20);
296
- const rewards = availableTokens.filter(
297
- t => t.type === NativeTokenTypes.ERC721 || t.type === NativeTokenTypes.ERC1155
298
- );
299
-
300
- // Load all balances in parallel (individual errors handled per-token)
301
- const [pointsResults, rewardsResults] = await Promise.all([
302
- Promise.all(points.map(processPointsBalance)),
303
- Promise.all(rewards.map(processRewardsBalance))
304
- ]);
305
-
306
- // Combine results, filtering out nulls (failed loads return null/empty)
307
- const allBalances = [
308
- ...pointsResults.filter(Boolean),
309
- ...rewardsResults.flat()
310
- ] as TokenBalanceWithToken[];
311
-
312
- setTokenBalances(allBalances);
313
-
314
- // Set error only if no balances loaded at all
315
- if (allBalances.length === 0 && availableTokens.length > 0) {
316
- setError('No token balances could be loaded');
317
- }
318
- } catch (err) {
319
- const errorMessage = err instanceof Error ? err.message : 'Failed to load balances';
320
- setError(errorMessage);
321
- console.error('[useTokenBalances] Error loading balances:', err);
322
- } finally {
323
- setIsLoading(false);
324
- }
325
- }, [isAvailable, availableTokens, processPointsBalance, processRewardsBalance]);
326
-
327
- // Auto-load balances when dependencies change
328
- useEffect(() => {
329
- if (autoLoad && isAvailable && availableTokens.length > 0) {
330
- loadBalances();
331
- }
332
- }, [autoLoad, isAvailable, availableTokens.length, loadBalances]);
333
-
334
- // Deprecated: Fallback polling interval (prefer wallet events)
335
- // Kept for backward compatibility and edge cases where events might not work
336
- useEffect(() => {
337
- if (refreshInterval <= 0 || !isAvailable) return;
338
-
339
- const intervalId = setInterval(() => {
340
- loadBalances();
341
- }, refreshInterval);
342
-
343
- return () => {
344
- clearInterval(intervalId);
345
- };
346
- }, [refreshInterval, isAvailable, loadBalances]);
347
-
348
- // Wallet events refresh: listen for real-time blockchain events
349
- // Refreshes on ANY wallet domain event (Transfer, TransferSingle, etc.)
350
- // Debouncing prevents excessive API calls during rapid events
351
- useEffect(() => {
352
- if (!sdk || !refreshOnWalletEvents || !isAvailable) return;
353
-
354
- const unsubscribe = sdk.events.subscribe((event) => {
355
- if (event.domain === 'wallet') {
356
- // Debounce rapid events (batch transfers, etc.)
357
- if (walletEventDebounceRef.current) {
358
- clearTimeout(walletEventDebounceRef.current);
359
- }
360
-
361
- walletEventDebounceRef.current = setTimeout(() => {
362
- console.log(`[useTokenBalances] Wallet event (${event.type}), refreshing balances...`);
363
- loadBalances();
364
- walletEventDebounceRef.current = null;
365
- }, walletEventDebounceMs);
366
- }
367
- }, { domains: ['wallet'] });
368
-
369
- return () => {
370
- unsubscribe();
371
- if (walletEventDebounceRef.current) {
372
- clearTimeout(walletEventDebounceRef.current);
373
- }
374
- };
375
- }, [sdk, refreshOnWalletEvents, walletEventDebounceMs, isAvailable, loadBalances]);
376
-
377
- return {
378
- tokenBalances,
379
- isLoading,
380
- error,
381
- refresh: loadBalances,
382
- isAvailable,
383
- userAccountAddress: accountAddress
384
- };
385
- }
1
+ import { useCallback, useEffect, useState, useRef } from 'react';
2
+ import { usePersSDK } from '../providers/PersSDKProvider';
3
+ import { useWeb3 } from './useWeb3';
4
+ import { NativeTokenTypes, type TokenDTO } from '@explorins/pers-sdk';
5
+ import type { TokenBalance, TokenBalanceRequest } from '@explorins/pers-sdk/web3';
6
+
7
+ /**
8
+ * Token balance with associated token information for display
9
+ *
10
+ * This composite type combines blockchain balance data with token metadata,
11
+ * making it easy to display token information in UI components without
12
+ * needing to join data from multiple sources.
13
+ *
14
+ * ## CRITICAL: Token ID for Transactions
15
+ *
16
+ * For ERC721 (stamps) and ERC1155 (rewards), use `tokenBalance.tokenId` as the
17
+ * `contractTokenId` when creating transactions. This is the **on-chain token ID**
18
+ * assigned by the blockchain, NOT the metadata definition ID.
19
+ *
20
+ * | Token Type | tokenId meaning | Example |
21
+ * |------------|-----------------|---------|
22
+ * | ERC20 | `null` or `""` | N/A (fungible points) |
23
+ * | ERC721 | NFT instance ID | `"46"` (on-chain ID) |
24
+ * | ERC1155 | Token type ID | `"16"` (semi-fungible type) |
25
+ *
26
+ * @example Correct usage for token burn transaction
27
+ * ```typescript
28
+ * const stampBalance = tokenBalances.find(b => b.token.type === 'ERC721');
29
+ *
30
+ * const request = buildPOSBurnRequest({
31
+ * amount: 1,
32
+ * contractAddress: stampBalance.token.contractAddress,
33
+ * contractTokenId: stampBalance.tokenBalance.tokenId, // ✅ Correct
34
+ * chainId: stampBalance.token.chainId,
35
+ * userId: userId,
36
+ * businessId: businessId
37
+ * });
38
+ * ```
39
+ *
40
+ *
41
+ * */
42
+ //@see {@link https://docs.explorins.com/pers-sdk/troubleshooting#stamp-voucher-transaction-fails | Troubleshooting: Stamp Transaction Fails}
43
+ export interface TokenBalanceWithToken {
44
+ /**
45
+ * The balance data from blockchain
46
+ *
47
+ * **Important:** For NFT operations, use `tokenBalance.tokenId` as the `contractTokenId`
48
+ * in transaction requests. This is the actual on-chain ID, not the metadata definition.
49
+ */
50
+ tokenBalance: TokenBalance;
51
+ /** The token definition (contract, symbol, metadata, etc.) */
52
+ token: TokenDTO;
53
+ /** Human-readable formatted balance string (e.g., "100 XPL" or "3 stamps") */
54
+ balanceFormatted: string;
55
+ }
56
+
57
+ /**
58
+ * Configuration options for useTokenBalances hook
59
+ */
60
+ export interface UseTokenBalancesOptions {
61
+ /**
62
+ * Wallet address to check balances for (REQUIRED)
63
+ * Apps must explicitly provide the wallet address - no automatic selection
64
+ * Get from: user.wallets[selectedIndex].address
65
+ */
66
+ accountAddress: string;
67
+ /** Available tokens to check balances for (from SDK or filtered list) */
68
+ availableTokens?: TokenDTO[];
69
+ /** Whether to automatically load balances on mount/auth/token changes */
70
+ autoLoad?: boolean;
71
+ /**
72
+ * Fallback polling interval in milliseconds (0 = disabled)
73
+ * @deprecated Use wallet events instead (refreshOnWalletEvents). Polling kept as fallback only.
74
+ * @default 0
75
+ */
76
+ refreshInterval?: number;
77
+ /**
78
+ * Auto-refresh balances when wallet events are received (Transfer, Approval, etc.)
79
+ * Requires `captureWalletEvents: true` in SDK config (default)
80
+ * @default true
81
+ */
82
+ refreshOnWalletEvents?: boolean;
83
+ /**
84
+ * Debounce time for wallet event-triggered refreshes (milliseconds)
85
+ * Prevents rapid refresh calls during batch transfers
86
+ * @default 1000
87
+ */
88
+ walletEventDebounceMs?: number;
89
+ }
90
+
91
+ /**
92
+ * Token balance loading hook return value
93
+ */
94
+ export interface UseTokenBalancesResult {
95
+ /** Array of token balances with formatted display data */
96
+ tokenBalances: TokenBalanceWithToken[];
97
+ /** Whether balances are currently loading */
98
+ isLoading: boolean;
99
+ /** Error message if loading failed */
100
+ error: string | null;
101
+ /** Manually trigger balance refresh */
102
+ refresh: () => Promise<void>;
103
+ /** Whether the hook is available (SDK initialized, user authenticated, has wallet) */
104
+ isAvailable: boolean;
105
+ /** User's wallet address being queried */
106
+ userAccountAddress: string | null;
107
+ }
108
+
109
+ /**
110
+ * React hook for loading and managing token balances
111
+ *
112
+ * Automatically handles:
113
+ * - Loading balances for ERC20 (fungible tokens/points)
114
+ * - Loading collections for ERC721 (unique NFTs/stamps)
115
+ * - Loading collections for ERC1155 (semi-fungible tokens/rewards)
116
+ * - Parallel loading with error resilience
117
+ * - Auto-refresh on authentication or token list changes
118
+ * - Formatted balance strings for display
119
+ *
120
+ * This hook consolidates complex token balance loading logic that would otherwise
121
+ * need to be duplicated across every app. It handles the differences between
122
+ * fungible and non-fungible tokens, parallel loading, error handling, and
123
+ * provides formatted display strings.
124
+ *
125
+ * @param options - Configuration options
126
+ * @returns Token balances state and actions
127
+ *
128
+ * @example
129
+ * **Basic Usage:**
130
+ * ```typescript
131
+ * import { useTokenBalances, useTokens, usePersSDK } from '@explorins/pers-sdk-react-native';
132
+ * import { useState, useEffect } from 'react';
133
+ *
134
+ * function TokenBalancesScreen() {
135
+ * const { user } = usePersSDK();
136
+ * const { getTokens } = useTokens();
137
+ * const [availableTokens, setAvailableTokens] = useState([]);
138
+ *
139
+ * // Load tokens on mount
140
+ * useEffect(() => {
141
+ * getTokens().then(result => setAvailableTokens(result.data));
142
+ * }, []);
143
+ *
144
+ * // App explicitly selects which wallet to use
145
+ * const walletAddress = user?.wallets?.[0]?.address;
146
+ *
147
+ * const {
148
+ * tokenBalances,
149
+ * isLoading,
150
+ * error,
151
+ * refresh
152
+ * } = useTokenBalances({
153
+ * accountAddress: walletAddress!,
154
+ * availableTokens,
155
+ * autoLoad: true
156
+ * });
157
+ *
158
+ * if (isLoading) return <Text>Loading balances...</Text>;
159
+ * if (error) return <Text>Error: {error}</Text>;
160
+ *
161
+ * return (
162
+ * <View>
163
+ * {tokenBalances.map(({ token, balanceFormatted }) => (
164
+ * <View key={token.id}>
165
+ * <Text>{token.name}: {balanceFormatted}</Text>
166
+ * </View>
167
+ * ))}
168
+ * <Button onPress={refresh} title="Refresh" />
169
+ * </View>
170
+ * );
171
+ * }
172
+ * ```
173
+ *
174
+ * @example
175
+ * **With Wallet Events (Real-time):
176
+ * ```typescript
177
+ * // Auto-refresh on Transfer, Approval, and other blockchain events
178
+ * const { tokenBalances } = useTokenBalances({
179
+ * accountAddress: walletAddress!,
180
+ * availableTokens,
181
+ * refreshOnWalletEvents: true, // Enable real-time refresh (default: true)
182
+ * walletEventDebounceMs: 1000 // Debounce rapid events (default: 1000ms)
183
+ * });
184
+ * ```
185
+ *
186
+ * @example
187
+ * **Multi-Wallet Support:**
188
+ * ```typescript
189
+ * function MultiWalletBalances() {
190
+ * const { user } = usePersSDK();
191
+ * const [selectedWalletIndex, setSelectedWalletIndex] = useState(0);
192
+ *
193
+ * const walletAddress = user?.wallets?.[selectedWalletIndex]?.address;
194
+ *
195
+ * const { tokenBalances } = useTokenBalances({
196
+ * accountAddress: walletAddress!,
197
+ * availableTokens
198
+ * });
199
+ *
200
+ * // UI lets user switch between wallets
201
+ * }
202
+ * ```
203
+ */
204
+ export function useTokenBalances(options: UseTokenBalancesOptions): UseTokenBalancesResult {
205
+ const {
206
+ accountAddress,
207
+ availableTokens = [],
208
+ autoLoad = true,
209
+ refreshInterval = 0,
210
+ refreshOnWalletEvents = true,
211
+ walletEventDebounceMs = 1000
212
+ } = options;
213
+
214
+ const { isAuthenticated, sdk } = usePersSDK();
215
+ const web3 = useWeb3();
216
+
217
+ const [tokenBalances, setTokenBalances] = useState<TokenBalanceWithToken[]>([]);
218
+ const [isLoading, setIsLoading] = useState(false);
219
+ const [error, setError] = useState<string | null>(null);
220
+
221
+ // Debounce ref for wallet events
222
+ const walletEventDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
223
+
224
+ // Check if the hook is available for use
225
+ const isAvailable = web3.isAvailable && isAuthenticated && !!accountAddress;
226
+
227
+ /**
228
+ * Process balance for ERC20 fungible tokens (points/credits)
229
+ */
230
+ const processPointsBalance = useCallback(async (token: TokenDTO): Promise<TokenBalanceWithToken | null> => {
231
+ if (!web3.getTokenBalance || !accountAddress) return null;
232
+
233
+ try {
234
+ const request: TokenBalanceRequest = {
235
+ accountAddress,
236
+ contractAddress: token.contractAddress,
237
+ abi: token.abi,
238
+ tokenId: '',
239
+ chainId: token.chainId
240
+ };
241
+
242
+ const balanceResult = await web3.getTokenBalance(request);
243
+
244
+ if (!balanceResult || !balanceResult.hasBalance) return null;
245
+
246
+ return {
247
+ tokenBalance: balanceResult,
248
+ token,
249
+ balanceFormatted: `${balanceResult.balance} ${token.symbol || 'tokens'}`
250
+ };
251
+ } catch (err) {
252
+ console.warn(`[useTokenBalances] Failed to load balance for ${token.symbol}:`, err);
253
+ return null;
254
+ }
255
+ }, [web3.getTokenBalance, accountAddress]);
256
+
257
+ /**
258
+ * Process collection for ERC721/ERC1155 non-fungible tokens (stamps/rewards)
259
+ */
260
+ const processRewardsBalance = useCallback(async (token: TokenDTO): Promise<TokenBalanceWithToken[]> => {
261
+ if (!web3.getAccountOwnedTokensFromContract || !accountAddress) return [];
262
+
263
+ try {
264
+ const result = await web3.getAccountOwnedTokensFromContract(accountAddress, token);
265
+
266
+ if (result.totalOwned === 0) return [];
267
+
268
+ const balanceUnit = token.type === NativeTokenTypes.ERC1155 ? 'reward' : 'stamp';
269
+
270
+ // Return one item per tokenId (for individual display in lists)
271
+ return result.ownedTokens.map(ownedToken => ({
272
+ tokenBalance: ownedToken,
273
+ token,
274
+ balanceFormatted: `${ownedToken.balance} ${balanceUnit}${ownedToken.balance !== 1 ? 's' : ''}`
275
+ }));
276
+ } catch (err) {
277
+ console.warn(`[useTokenBalances] Failed to load collection for ${token.symbol}:`, err);
278
+ return [];
279
+ }
280
+ }, [web3.getAccountOwnedTokensFromContract, accountAddress]);
281
+
282
+ /**
283
+ * Load all token balances
284
+ */
285
+ const loadBalances = useCallback(async () => {
286
+ if (!isAvailable || availableTokens.length === 0) {
287
+ return;
288
+ }
289
+
290
+ setIsLoading(true);
291
+ setError(null);
292
+
293
+ try {
294
+ // Split by category: points (fungible credits) vs rewards (stamps/collectibles)
295
+ const points = availableTokens.filter(t => t.type === NativeTokenTypes.ERC20);
296
+ const rewards = availableTokens.filter(
297
+ t => t.type === NativeTokenTypes.ERC721 || t.type === NativeTokenTypes.ERC1155
298
+ );
299
+
300
+ // Load all balances in parallel (individual errors handled per-token)
301
+ const [pointsResults, rewardsResults] = await Promise.all([
302
+ Promise.all(points.map(processPointsBalance)),
303
+ Promise.all(rewards.map(processRewardsBalance))
304
+ ]);
305
+
306
+ // Combine results, filtering out nulls (failed loads return null/empty)
307
+ const allBalances = [
308
+ ...pointsResults.filter(Boolean),
309
+ ...rewardsResults.flat()
310
+ ] as TokenBalanceWithToken[];
311
+
312
+ setTokenBalances(allBalances);
313
+
314
+ // Set error only if no balances loaded at all
315
+ if (allBalances.length === 0 && availableTokens.length > 0) {
316
+ setError('No token balances could be loaded');
317
+ }
318
+ } catch (err) {
319
+ const errorMessage = err instanceof Error ? err.message : 'Failed to load balances';
320
+ setError(errorMessage);
321
+ console.error('[useTokenBalances] Error loading balances:', err);
322
+ } finally {
323
+ setIsLoading(false);
324
+ }
325
+ }, [isAvailable, availableTokens, processPointsBalance, processRewardsBalance]);
326
+
327
+ // Auto-load balances when dependencies change
328
+ useEffect(() => {
329
+ if (autoLoad && isAvailable && availableTokens.length > 0) {
330
+ loadBalances();
331
+ }
332
+ }, [autoLoad, isAvailable, availableTokens.length, loadBalances]);
333
+
334
+ // Deprecated: Fallback polling interval (prefer wallet events)
335
+ // Kept for backward compatibility and edge cases where events might not work
336
+ useEffect(() => {
337
+ if (refreshInterval <= 0 || !isAvailable) return;
338
+
339
+ const intervalId = setInterval(() => {
340
+ loadBalances();
341
+ }, refreshInterval);
342
+
343
+ return () => {
344
+ clearInterval(intervalId);
345
+ };
346
+ }, [refreshInterval, isAvailable, loadBalances]);
347
+
348
+ // Wallet events refresh: listen for real-time blockchain events
349
+ // Refreshes on ANY wallet domain event (Transfer, TransferSingle, etc.)
350
+ // Debouncing prevents excessive API calls during rapid events
351
+ useEffect(() => {
352
+ if (!sdk || !refreshOnWalletEvents || !isAvailable) return;
353
+
354
+ const unsubscribe = sdk.events.subscribe((event) => {
355
+ if (event.domain === 'wallet') {
356
+ // Debounce rapid events (batch transfers, etc.)
357
+ if (walletEventDebounceRef.current) {
358
+ clearTimeout(walletEventDebounceRef.current);
359
+ }
360
+
361
+ walletEventDebounceRef.current = setTimeout(() => {
362
+ console.log(`[useTokenBalances] Wallet event (${event.type}), refreshing balances...`);
363
+ loadBalances();
364
+ walletEventDebounceRef.current = null;
365
+ }, walletEventDebounceMs);
366
+ }
367
+ }, { domains: ['wallet'] });
368
+
369
+ return () => {
370
+ unsubscribe();
371
+ if (walletEventDebounceRef.current) {
372
+ clearTimeout(walletEventDebounceRef.current);
373
+ }
374
+ };
375
+ }, [sdk, refreshOnWalletEvents, walletEventDebounceMs, isAvailable, loadBalances]);
376
+
377
+ return {
378
+ tokenBalances,
379
+ isLoading,
380
+ error,
381
+ refresh: loadBalances,
382
+ isAvailable,
383
+ userAccountAddress: accountAddress
384
+ };
385
+ }