@explorins/pers-sdk-react-native 2.0.5 → 2.1.1
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 +117 -4
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useTokenBalances.d.ts +140 -0
- package/dist/hooks/useTokenBalances.d.ts.map +1 -0
- package/dist/hooks/useTokenBalances.js +213 -0
- package/dist/hooks/useWeb3.d.ts +2 -3
- package/dist/hooks/useWeb3.d.ts.map +1 -1
- package/dist/hooks/useWeb3.js +42 -32
- package/dist/index.d.ts +80 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29617 -29260
- package/dist/index.js.map +1 -1
- package/dist/providers/PersSDKProvider.d.ts.map +1 -1
- package/dist/providers/PersSDKProvider.js +31 -7
- package/dist/storage/rn-secure-storage.d.ts.map +1 -1
- package/dist/storage/rn-secure-storage.js +24 -10
- package/package.json +2 -4
- package/src/hooks/index.ts +7 -1
- package/src/hooks/useTokenBalances.ts +290 -0
- package/src/hooks/useWeb3.ts +45 -35
- package/src/index.ts +89 -8
- package/src/providers/PersSDKProvider.tsx +33 -8
- package/src/storage/rn-secure-storage.ts +27 -10
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
|
-
|
|
462
|
-
|
|
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
|
-
|
|
472
|
-
|
|
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.
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -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"}
|
package/dist/hooks/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/hooks/useWeb3.d.ts
CHANGED
|
@@ -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
|
-
|
|
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":"
|
|
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"}
|