@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PersSDKProvider.d.ts","sourceRoot":"","sources":["../../src/providers/PersSDKProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAuC,SAAS,EAA2C,MAAM,OAAO,CAAC;AAEvH,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAIpF,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAG3D,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAE3D,MAAM,WAAW,cAAc;IAE7B,GAAG,EAAE,OAAO,GAAG,IAAI,CAAC;IAGpB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,eAAe,GAAG,IAAI,CAAC;IACnC,SAAS,EAAE,eAAe,GAAG,IAAI,CAAC;IAClC,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACtC,YAAY,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACxC,SAAS,EAAE,eAAe,GAAG,IAAI,CAAC;IAClC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACnC,SAAS,EAAE,eAAe,GAAG,IAAI,CAAC;IAGlC,YAAY,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAGzC,aAAa,EAAE,OAAO,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,CAAC;IAGhC,UAAU,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,sBAAsB,EAAE,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,EAAE,eAAe,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5F,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAMD,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC;IACrC,QAAQ,EAAE,SAAS,CAAC;IACpB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB,
|
|
1
|
+
{"version":3,"file":"PersSDKProvider.d.ts","sourceRoot":"","sources":["../../src/providers/PersSDKProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAuC,SAAS,EAA2C,MAAM,OAAO,CAAC;AAEvH,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAIpF,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAG3D,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAE3D,MAAM,WAAW,cAAc;IAE7B,GAAG,EAAE,OAAO,GAAG,IAAI,CAAC;IAGpB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,eAAe,GAAG,IAAI,CAAC;IACnC,SAAS,EAAE,eAAe,GAAG,IAAI,CAAC;IAClC,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACtC,YAAY,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACxC,SAAS,EAAE,eAAe,GAAG,IAAI,CAAC;IAClC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACnC,SAAS,EAAE,eAAe,GAAG,IAAI,CAAC;IAGlC,YAAY,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAGzC,aAAa,EAAE,OAAO,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,CAAC;IAGhC,UAAU,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,sBAAsB,EAAE,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,EAAE,eAAe,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5F,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAMD,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC;IACrC,QAAQ,EAAE,SAAS,CAAC;IACpB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB,CAiPA,CAAC;AAGF,eAAO,MAAM,UAAU,QAAO,cAQ7B,CAAC"}
|
|
@@ -68,18 +68,37 @@ export const PersSDKProvider = ({ children, config }) => {
|
|
|
68
68
|
initializingRef.current = false;
|
|
69
69
|
}
|
|
70
70
|
}, [isInitialized]);
|
|
71
|
+
const setAuthenticationState = useCallback((user, isAuthenticated) => {
|
|
72
|
+
setUser(user);
|
|
73
|
+
setIsAuthenticated(isAuthenticated);
|
|
74
|
+
}, []);
|
|
71
75
|
// Auto-initialize if config is provided
|
|
72
76
|
useEffect(() => {
|
|
73
77
|
if (config && !isInitialized && !initializingRef.current) {
|
|
74
|
-
initialize(config).
|
|
78
|
+
initialize(config).then(async () => {
|
|
79
|
+
// Validate stored tokens on startup
|
|
80
|
+
// SDK's initialize() already calls ensureValidToken() which handles expired tokens
|
|
81
|
+
// This provides an additional safety layer for missing/corrupted tokens
|
|
82
|
+
if (authProvider && sdk) {
|
|
83
|
+
try {
|
|
84
|
+
const hasToken = await sdk.auth.hasValidAuth();
|
|
85
|
+
if (!hasToken) {
|
|
86
|
+
console.log('[PersSDK] No tokens found on startup, ensuring clean state');
|
|
87
|
+
await authProvider.clearTokens();
|
|
88
|
+
setAuthenticationState(null, false);
|
|
89
|
+
}
|
|
90
|
+
// Note: Token expiration validation happens automatically in SDK's initialize()
|
|
91
|
+
// which calls ensureValidToken() → checks expiration → triggers AUTH_FAILED if needed
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.warn('[PersSDK] Token validation on startup failed:', error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}).catch(err => {
|
|
75
98
|
console.error('Auto-initialization failed:', err);
|
|
76
99
|
});
|
|
77
100
|
}
|
|
78
|
-
}, [config, isInitialized, initialize]);
|
|
79
|
-
const setAuthenticationState = useCallback((user, isAuthenticated) => {
|
|
80
|
-
setUser(user);
|
|
81
|
-
setIsAuthenticated(isAuthenticated);
|
|
82
|
-
}, []);
|
|
101
|
+
}, [config, isInitialized, initialize, authProvider, sdk, setAuthenticationState]);
|
|
83
102
|
const refreshUserData = useCallback(async () => {
|
|
84
103
|
if (!sdk || !isAuthenticated || !isInitialized) {
|
|
85
104
|
throw new Error('SDK not initialized or not authenticated. Cannot refresh user data.');
|
|
@@ -120,8 +139,13 @@ export const PersSDKProvider = ({ children, config }) => {
|
|
|
120
139
|
}
|
|
121
140
|
}
|
|
122
141
|
// If authentication failed, clear state
|
|
142
|
+
// Frontend app can observe isAuthenticated state change to show custom UI
|
|
123
143
|
if (status === 'auth_failed') {
|
|
124
|
-
console.log('[PersSDK] Authentication failed
|
|
144
|
+
console.log('[PersSDK] Authentication failed - session expired');
|
|
145
|
+
// Note: Token clearing already handled by SDK's handleAuthFailure()
|
|
146
|
+
// which calls authProvider.clearTokens() with robust retry logic
|
|
147
|
+
// Clear React state to sync with SDK
|
|
148
|
+
// This triggers re-render, allowing app to show login screen
|
|
125
149
|
setAuthenticationState(null, false);
|
|
126
150
|
}
|
|
127
151
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rn-secure-storage.d.ts","sourceRoot":"","sources":["../../src/storage/rn-secure-storage.ts"],"names":[],"mappings":"AACA,OAAO,EACL,YAAY,EAGb,MAAM,0BAA0B,CAAC;AAalC;;;GAGG;AACH,qBAAa,wBAAyB,YAAW,YAAY;IAc/C,OAAO,CAAC,SAAS;IAZ7B,QAAQ,CAAC,eAAe,SAAS;IAGjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAKzB;IAEH,OAAO,CAAC,eAAe,CAA2B;gBAE9B,SAAS,GAAE,MAAuB;IAIhD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B/C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAkCzC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBlC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"rn-secure-storage.d.ts","sourceRoot":"","sources":["../../src/storage/rn-secure-storage.ts"],"names":[],"mappings":"AACA,OAAO,EACL,YAAY,EAGb,MAAM,0BAA0B,CAAC;AAalC;;;GAGG;AACH,qBAAa,wBAAyB,YAAW,YAAY;IAc/C,OAAO,CAAC,SAAS;IAZ7B,QAAQ,CAAC,eAAe,SAAS;IAGjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAKzB;IAEH,OAAO,CAAC,eAAe,CAA2B;gBAE9B,SAAS,GAAE,MAAuB;IAIhD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B/C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAkCzC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBlC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAqC5B,OAAO,CAAC,UAAU;IAOlB,OAAO,CAAC,QAAQ;CAOjB"}
|
|
@@ -110,23 +110,37 @@ export class ReactNativeSecureStorage {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
async clear() {
|
|
113
|
-
|
|
113
|
+
const clearPromises = [];
|
|
114
|
+
// Clear all known secure keys with retry logic
|
|
114
115
|
for (const key of this.SECURE_KEYS) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
clearPromises.push((async () => {
|
|
117
|
+
try {
|
|
118
|
+
if (Keychain) {
|
|
119
|
+
await Keychain.resetGenericPassword({ service: this.getKeyName(key) });
|
|
120
|
+
}
|
|
118
121
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
catch (e) {
|
|
123
|
+
console.warn(`[ReactNativeSecureStorage] Retry clearing ${key}`);
|
|
124
|
+
// Retry once
|
|
125
|
+
try {
|
|
126
|
+
if (Keychain) {
|
|
127
|
+
await Keychain.resetGenericPassword({ service: this.getKeyName(key) });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (retryError) {
|
|
131
|
+
console.error(`[ReactNativeSecureStorage] Failed to clear ${key} after retry:`, retryError);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
})());
|
|
123
135
|
}
|
|
124
|
-
//
|
|
136
|
+
// Wait for all Keychain clearing to complete (or fail)
|
|
137
|
+
await Promise.allSettled(clearPromises);
|
|
138
|
+
// Always clear AsyncStorage fallback
|
|
125
139
|
try {
|
|
126
140
|
await this.fallbackStorage.clear();
|
|
127
141
|
}
|
|
128
142
|
catch (e) {
|
|
129
|
-
console.
|
|
143
|
+
console.error('[ReactNativeSecureStorage] Failed to clear AsyncStorage:', e);
|
|
130
144
|
}
|
|
131
145
|
}
|
|
132
146
|
getKeyName(key) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@explorins/pers-sdk-react-native",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "React Native SDK for PERS Platform - Tourism Loyalty System with Blockchain Transaction Signing and WebAuthn Authentication",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -37,10 +37,8 @@
|
|
|
37
37
|
"author": "eXplorins",
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@explorins/pers-sdk": "^2.
|
|
41
|
-
"@explorins/pers-shared": "^2.1.64",
|
|
40
|
+
"@explorins/pers-sdk": "^2.1.1",
|
|
42
41
|
"@explorins/pers-signer": "^1.0.33",
|
|
43
|
-
"@explorins/web3-ts": "^0.3.77",
|
|
44
42
|
"buffer": "^6.0.3",
|
|
45
43
|
"ethers": "^6.15.0",
|
|
46
44
|
"react-native-get-random-values": "^2.0.0",
|
package/src/hooks/index.ts
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';
|
|
@@ -28,4 +29,9 @@ export type {
|
|
|
28
29
|
SigningStatus as SigningStatusType
|
|
29
30
|
} from './useTransactionSigner';
|
|
30
31
|
export type { AccountOwnedTokensResult, Web3Hook } from './useWeb3';
|
|
31
|
-
export type { EventsHook, PersEvent, EventHandler, EventFilter, Unsubscribe } from './useEvents';
|
|
32
|
+
export type { EventsHook, PersEvent, EventHandler, EventFilter, Unsubscribe } from './useEvents';
|
|
33
|
+
export type {
|
|
34
|
+
TokenBalanceWithToken,
|
|
35
|
+
UseTokenBalancesOptions,
|
|
36
|
+
UseTokenBalancesResult
|
|
37
|
+
} from './useTokenBalances';
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { usePersSDK } from '../providers/PersSDKProvider';
|
|
3
|
+
import { useWeb3 } from './useWeb3';
|
|
4
|
+
import { NativeTokenTypes, type TokenDTO } from '@explorins/pers-shared';
|
|
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
|
+
export interface TokenBalanceWithToken {
|
|
15
|
+
/** The balance data from blockchain */
|
|
16
|
+
tokenBalance: TokenBalance;
|
|
17
|
+
/** The token definition (contract, symbol, metadata, etc.) */
|
|
18
|
+
token: TokenDTO;
|
|
19
|
+
/** Human-readable formatted balance string (e.g., "100 XPL" or "3 stamps") */
|
|
20
|
+
balanceFormatted: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Configuration options for useTokenBalances hook
|
|
25
|
+
*/
|
|
26
|
+
export interface UseTokenBalancesOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Wallet address to check balances for (REQUIRED)
|
|
29
|
+
* Apps must explicitly provide the wallet address - no automatic selection
|
|
30
|
+
* Get from: user.wallets[selectedIndex].address
|
|
31
|
+
*/
|
|
32
|
+
accountAddress: string;
|
|
33
|
+
/** Available tokens to check balances for (from SDK or filtered list) */
|
|
34
|
+
availableTokens?: TokenDTO[];
|
|
35
|
+
/** Whether to automatically load balances on mount/auth/token changes */
|
|
36
|
+
autoLoad?: boolean;
|
|
37
|
+
/** Optional refresh interval in milliseconds (0 = disabled) */
|
|
38
|
+
refreshInterval?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Token balance loading hook return value
|
|
43
|
+
*/
|
|
44
|
+
export interface UseTokenBalancesResult {
|
|
45
|
+
/** Array of token balances with formatted display data */
|
|
46
|
+
tokenBalances: TokenBalanceWithToken[];
|
|
47
|
+
/** Whether balances are currently loading */
|
|
48
|
+
isLoading: boolean;
|
|
49
|
+
/** Error message if loading failed */
|
|
50
|
+
error: string | null;
|
|
51
|
+
/** Manually trigger balance refresh */
|
|
52
|
+
refresh: () => Promise<void>;
|
|
53
|
+
/** Whether the hook is available (SDK initialized, user authenticated, has wallet) */
|
|
54
|
+
isAvailable: boolean;
|
|
55
|
+
/** User's wallet address being queried */
|
|
56
|
+
userAccountAddress: string | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* React hook for loading and managing token balances
|
|
61
|
+
*
|
|
62
|
+
* Automatically handles:
|
|
63
|
+
* - Loading balances for ERC20 (fungible tokens/points)
|
|
64
|
+
* - Loading collections for ERC721 (unique NFTs/stamps)
|
|
65
|
+
* - Loading collections for ERC1155 (semi-fungible tokens/rewards)
|
|
66
|
+
* - Parallel loading with error resilience
|
|
67
|
+
* - Auto-refresh on authentication or token list changes
|
|
68
|
+
* - Formatted balance strings for display
|
|
69
|
+
*
|
|
70
|
+
* This hook consolidates complex token balance loading logic that would otherwise
|
|
71
|
+
* need to be duplicated across every app. It handles the differences between
|
|
72
|
+
* fungible and non-fungible tokens, parallel loading, error handling, and
|
|
73
|
+
* provides formatted display strings.
|
|
74
|
+
*
|
|
75
|
+
* @param options - Configuration options
|
|
76
|
+
* @returns Token balances state and actions
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* **Basic Usage:**
|
|
80
|
+
* ```typescript
|
|
81
|
+
* import { useTokenBalances, useTokens, usePersSDK } from '@explorins/pers-sdk-react-native';
|
|
82
|
+
*
|
|
83
|
+
* function TokenBalancesScreen() {
|
|
84
|
+
* const { user } = usePersSDK();
|
|
85
|
+
* const { availableTokens } = useTokens();
|
|
86
|
+
*
|
|
87
|
+
* // App explicitly selects which wallet to use
|
|
88
|
+
* const walletAddress = user?.wallets?.[0]?.address;
|
|
89
|
+
*
|
|
90
|
+
* const {
|
|
91
|
+
* tokenBalances,
|
|
92
|
+
* isLoading,
|
|
93
|
+
* error,
|
|
94
|
+
* refresh
|
|
95
|
+
* } = useTokenBalances({
|
|
96
|
+
* accountAddress: walletAddress!,
|
|
97
|
+
* availableTokens,
|
|
98
|
+
* autoLoad: true
|
|
99
|
+
* });
|
|
100
|
+
*
|
|
101
|
+
* if (isLoading) return <Text>Loading balances...</Text>;
|
|
102
|
+
* if (error) return <Text>Error: {error}</Text>;
|
|
103
|
+
*
|
|
104
|
+
* return (
|
|
105
|
+
* <View>
|
|
106
|
+
* {tokenBalances.map(({ token, balanceFormatted }) => (
|
|
107
|
+
* <View key={token.id}>
|
|
108
|
+
* <Text>{token.name}: {balanceFormatted}</Text>
|
|
109
|
+
* </View>
|
|
110
|
+
* ))}
|
|
111
|
+
* <Button onPress={refresh} title="Refresh" />
|
|
112
|
+
* </View>
|
|
113
|
+
* );
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* **With Auto-Refresh:**
|
|
119
|
+
* ```typescript
|
|
120
|
+
* const { tokenBalances, isLoading } = useTokenBalances({
|
|
121
|
+
* accountAddress: walletAddress!,
|
|
122
|
+
* availableTokens,
|
|
123
|
+
* autoLoad: true,
|
|
124
|
+
* refreshInterval: 30000 // Refresh every 30 seconds
|
|
125
|
+
* });
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* **Multi-Wallet Support:**
|
|
130
|
+
* ```typescript
|
|
131
|
+
* function MultiWalletBalances() {
|
|
132
|
+
* const { user } = usePersSDK();
|
|
133
|
+
* const [selectedWalletIndex, setSelectedWalletIndex] = useState(0);
|
|
134
|
+
*
|
|
135
|
+
* const walletAddress = user?.wallets?.[selectedWalletIndex]?.address;
|
|
136
|
+
*
|
|
137
|
+
* const { tokenBalances } = useTokenBalances({
|
|
138
|
+
* accountAddress: walletAddress!,
|
|
139
|
+
* availableTokens
|
|
140
|
+
* });
|
|
141
|
+
*
|
|
142
|
+
* // UI lets user switch between wallets
|
|
143
|
+
* }
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export function useTokenBalances(options: UseTokenBalancesOptions): UseTokenBalancesResult {
|
|
147
|
+
const {
|
|
148
|
+
accountAddress,
|
|
149
|
+
availableTokens = [],
|
|
150
|
+
autoLoad = true,
|
|
151
|
+
refreshInterval = 0
|
|
152
|
+
} = options;
|
|
153
|
+
|
|
154
|
+
const { isAuthenticated } = usePersSDK();
|
|
155
|
+
const web3 = useWeb3();
|
|
156
|
+
|
|
157
|
+
const [tokenBalances, setTokenBalances] = useState<TokenBalanceWithToken[]>([]);
|
|
158
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
159
|
+
const [error, setError] = useState<string | null>(null);
|
|
160
|
+
|
|
161
|
+
// Check if the hook is available for use
|
|
162
|
+
const isAvailable = web3.isAvailable && isAuthenticated && !!accountAddress;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Process balance for ERC20 fungible tokens (points/credits)
|
|
166
|
+
*/
|
|
167
|
+
const processPointsBalance = useCallback(async (token: TokenDTO): Promise<TokenBalanceWithToken | null> => {
|
|
168
|
+
if (!web3.getTokenBalance || !accountAddress) return null;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const request: TokenBalanceRequest = {
|
|
172
|
+
accountAddress,
|
|
173
|
+
contractAddress: token.contractAddress,
|
|
174
|
+
abi: token.abi,
|
|
175
|
+
tokenId: '',
|
|
176
|
+
chainId: token.chainId
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const balanceResult = await web3.getTokenBalance(request);
|
|
180
|
+
|
|
181
|
+
if (!balanceResult || !balanceResult.hasBalance) return null;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
tokenBalance: balanceResult,
|
|
185
|
+
token,
|
|
186
|
+
balanceFormatted: `${balanceResult.balance} ${token.symbol || 'tokens'}`
|
|
187
|
+
};
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.warn(`[useTokenBalances] Failed to load balance for ${token.symbol}:`, err);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}, [web3.getTokenBalance, accountAddress]);
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Process collection for ERC721/ERC1155 non-fungible tokens (stamps/rewards)
|
|
196
|
+
*/
|
|
197
|
+
const processRewardsBalance = useCallback(async (token: TokenDTO): Promise<TokenBalanceWithToken[]> => {
|
|
198
|
+
if (!web3.getAccountOwnedTokensFromContract || !accountAddress) return [];
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const result = await web3.getAccountOwnedTokensFromContract(accountAddress, token);
|
|
202
|
+
|
|
203
|
+
if (result.totalOwned === 0) return [];
|
|
204
|
+
|
|
205
|
+
const balanceUnit = token.type === NativeTokenTypes.ERC1155 ? 'reward' : 'stamp';
|
|
206
|
+
|
|
207
|
+
// Return one item per tokenId (for individual display in lists)
|
|
208
|
+
return result.ownedTokens.map(ownedToken => ({
|
|
209
|
+
tokenBalance: ownedToken,
|
|
210
|
+
token,
|
|
211
|
+
balanceFormatted: `${ownedToken.balance} ${balanceUnit}${ownedToken.balance !== 1 ? 's' : ''}`
|
|
212
|
+
}));
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.warn(`[useTokenBalances] Failed to load collection for ${token.symbol}:`, err);
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
}, [web3.getAccountOwnedTokensFromContract, accountAddress]);
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Load all token balances
|
|
221
|
+
*/
|
|
222
|
+
const loadBalances = useCallback(async () => {
|
|
223
|
+
if (!isAvailable || availableTokens.length === 0) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
setIsLoading(true);
|
|
228
|
+
setError(null);
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
// Split by category: points (fungible credits) vs rewards (stamps/collectibles)
|
|
232
|
+
const points = availableTokens.filter(t => t.type === NativeTokenTypes.ERC20);
|
|
233
|
+
const rewards = availableTokens.filter(
|
|
234
|
+
t => t.type === NativeTokenTypes.ERC721 || t.type === NativeTokenTypes.ERC1155
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Load all balances in parallel (individual errors handled per-token)
|
|
238
|
+
const [pointsResults, rewardsResults] = await Promise.all([
|
|
239
|
+
Promise.all(points.map(processPointsBalance)),
|
|
240
|
+
Promise.all(rewards.map(processRewardsBalance))
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
// Combine results, filtering out nulls (failed loads return null/empty)
|
|
244
|
+
const allBalances = [
|
|
245
|
+
...pointsResults.filter(Boolean),
|
|
246
|
+
...rewardsResults.flat()
|
|
247
|
+
] as TokenBalanceWithToken[];
|
|
248
|
+
|
|
249
|
+
setTokenBalances(allBalances);
|
|
250
|
+
|
|
251
|
+
// Set error only if no balances loaded at all
|
|
252
|
+
if (allBalances.length === 0 && availableTokens.length > 0) {
|
|
253
|
+
setError('No token balances could be loaded');
|
|
254
|
+
}
|
|
255
|
+
} catch (err) {
|
|
256
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to load balances';
|
|
257
|
+
setError(errorMessage);
|
|
258
|
+
console.error('[useTokenBalances] Error loading balances:', err);
|
|
259
|
+
} finally {
|
|
260
|
+
setIsLoading(false);
|
|
261
|
+
}
|
|
262
|
+
}, [isAvailable, availableTokens, processPointsBalance, processRewardsBalance]);
|
|
263
|
+
|
|
264
|
+
// Auto-load balances when dependencies change
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
if (autoLoad && isAvailable && availableTokens.length > 0) {
|
|
267
|
+
loadBalances();
|
|
268
|
+
}
|
|
269
|
+
}, [autoLoad, isAvailable, availableTokens.length, loadBalances]);
|
|
270
|
+
|
|
271
|
+
// Optional auto-refresh interval
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
if (refreshInterval > 0 && isAvailable && availableTokens.length > 0) {
|
|
274
|
+
const intervalId = setInterval(() => {
|
|
275
|
+
loadBalances();
|
|
276
|
+
}, refreshInterval);
|
|
277
|
+
|
|
278
|
+
return () => clearInterval(intervalId);
|
|
279
|
+
}
|
|
280
|
+
}, [refreshInterval, isAvailable, availableTokens.length, loadBalances]);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
tokenBalances,
|
|
284
|
+
isLoading,
|
|
285
|
+
error,
|
|
286
|
+
refresh: loadBalances,
|
|
287
|
+
isAvailable,
|
|
288
|
+
userAccountAddress: accountAddress
|
|
289
|
+
};
|
|
290
|
+
}
|
package/src/hooks/useWeb3.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
2
|
import { usePersSDK } from '../providers/PersSDKProvider';
|
|
3
|
+
import { Web3Manager } from '@explorins/pers-sdk/web3';
|
|
3
4
|
import type {
|
|
4
5
|
TokenBalance,
|
|
5
6
|
TokenBalanceRequest,
|
|
6
7
|
TokenCollectionRequest,
|
|
7
8
|
TokenCollection,
|
|
8
|
-
TokenMetadata
|
|
9
|
+
TokenMetadata,
|
|
10
|
+
AccountOwnedTokensResult
|
|
9
11
|
} from '@explorins/pers-sdk/web3';
|
|
10
12
|
import type { ChainData } from '@explorins/pers-sdk/web3-chain';
|
|
11
13
|
import type { TokenDTO } from '@explorins/pers-shared';
|
|
12
|
-
import type { AccountOwnedTokensResult } from '@explorins/pers-sdk';
|
|
13
14
|
|
|
14
15
|
// Re-export for convenience
|
|
15
|
-
export type { AccountOwnedTokensResult } from '@explorins/pers-sdk';
|
|
16
|
+
export type { AccountOwnedTokensResult } from '@explorins/pers-sdk/web3';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* React hook for Web3 operations in the PERS SDK
|
|
@@ -71,6 +72,15 @@ export const useWeb3 = () => {
|
|
|
71
72
|
console.warn('SDK not authenticated. Some web3 operations may fail.');
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Create Web3Manager instance lazily
|
|
77
|
+
* Web3Manager is now separate from PersSDK since v2.0.11+
|
|
78
|
+
*/
|
|
79
|
+
const web3 = useMemo(() => {
|
|
80
|
+
if (!sdk) return null;
|
|
81
|
+
return new Web3Manager(sdk.api());
|
|
82
|
+
}, [sdk]);
|
|
83
|
+
|
|
74
84
|
/**
|
|
75
85
|
* Retrieves token balance for a specific wallet and contract
|
|
76
86
|
*
|
|
@@ -91,18 +101,18 @@ export const useWeb3 = () => {
|
|
|
91
101
|
* ```
|
|
92
102
|
*/
|
|
93
103
|
const getTokenBalance = useCallback(async (request: TokenBalanceRequest): Promise<TokenBalance> => {
|
|
94
|
-
if (!isInitialized || !
|
|
104
|
+
if (!isInitialized || !web3) {
|
|
95
105
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
try {
|
|
99
|
-
const result = await
|
|
109
|
+
const result = await web3.getTokenBalance(request);
|
|
100
110
|
return result;
|
|
101
111
|
} catch (error) {
|
|
102
112
|
console.error('Failed to fetch token balance:', error);
|
|
103
113
|
throw error;
|
|
104
114
|
}
|
|
105
|
-
}, [
|
|
115
|
+
}, [web3, isInitialized]);
|
|
106
116
|
|
|
107
117
|
/**
|
|
108
118
|
* Retrieves metadata for a specific token (useful for NFTs)
|
|
@@ -124,88 +134,88 @@ export const useWeb3 = () => {
|
|
|
124
134
|
* ```
|
|
125
135
|
*/
|
|
126
136
|
const getTokenMetadata = useCallback(async (request: TokenBalanceRequest): Promise<TokenMetadata | null> => {
|
|
127
|
-
if (!isInitialized || !
|
|
137
|
+
if (!isInitialized || !web3) {
|
|
128
138
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
try {
|
|
132
|
-
const result = await
|
|
142
|
+
const result = await web3.getTokenMetadata(request);
|
|
133
143
|
return result;
|
|
134
144
|
} catch (error) {
|
|
135
145
|
console.error('Failed to fetch token metadata:', error);
|
|
136
146
|
throw error;
|
|
137
147
|
}
|
|
138
|
-
}, [
|
|
148
|
+
}, [web3, isInitialized]);
|
|
139
149
|
|
|
140
150
|
const getTokenCollection = useCallback(async (request: TokenCollectionRequest): Promise<TokenCollection> => {
|
|
141
|
-
if (!isInitialized || !
|
|
151
|
+
if (!isInitialized || !web3) {
|
|
142
152
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
try {
|
|
146
|
-
const result = await
|
|
156
|
+
const result = await web3.getTokenCollection(request);
|
|
147
157
|
return result;
|
|
148
158
|
} catch (error) {
|
|
149
159
|
console.error('Failed to fetch token collection:', error);
|
|
150
160
|
throw error;
|
|
151
161
|
}
|
|
152
|
-
}, [
|
|
162
|
+
}, [web3, isInitialized]);
|
|
153
163
|
|
|
154
164
|
const resolveIPFSUrl = useCallback(async (url: string, chainId: number): Promise<string> => {
|
|
155
|
-
if (!isInitialized || !
|
|
165
|
+
if (!isInitialized || !web3) {
|
|
156
166
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
try {
|
|
160
|
-
const result = await
|
|
170
|
+
const result = await web3.resolveIPFSUrl(url, chainId);
|
|
161
171
|
return result;
|
|
162
172
|
} catch (error) {
|
|
163
173
|
console.error('Failed to resolve IPFS URL:', error);
|
|
164
174
|
throw error;
|
|
165
175
|
}
|
|
166
|
-
}, [
|
|
176
|
+
}, [web3, isInitialized]);
|
|
167
177
|
|
|
168
178
|
const fetchAndProcessMetadata = useCallback(async (tokenUri: string, chainId: number): Promise<TokenMetadata | null> => {
|
|
169
|
-
if (!isInitialized || !
|
|
179
|
+
if (!isInitialized || !web3) {
|
|
170
180
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
171
181
|
}
|
|
172
182
|
|
|
173
183
|
try {
|
|
174
|
-
const result = await
|
|
184
|
+
const result = await web3.fetchAndProcessMetadata(tokenUri, chainId);
|
|
175
185
|
return result;
|
|
176
186
|
} catch (error) {
|
|
177
187
|
console.error('Failed to fetch and process metadata:', error);
|
|
178
188
|
throw error;
|
|
179
189
|
}
|
|
180
|
-
}, [
|
|
190
|
+
}, [web3, isInitialized]);
|
|
181
191
|
|
|
182
192
|
const getChainDataById = useCallback(async (chainId: number): Promise<ChainData | null> => {
|
|
183
|
-
if (!isInitialized || !
|
|
193
|
+
if (!isInitialized || !web3) {
|
|
184
194
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
185
195
|
}
|
|
186
196
|
|
|
187
197
|
try {
|
|
188
|
-
const result = await
|
|
198
|
+
const result = await web3.getChainDataById(chainId);
|
|
189
199
|
return result;
|
|
190
200
|
} catch (error) {
|
|
191
201
|
console.error('Failed to fetch chain data:', error);
|
|
192
202
|
throw error;
|
|
193
203
|
}
|
|
194
|
-
}, [
|
|
204
|
+
}, [web3, isInitialized]);
|
|
195
205
|
|
|
196
206
|
const getExplorerUrl = useCallback(async (chainId: number, address: string, type: 'address' | 'tx'): Promise<string> => {
|
|
197
|
-
if (!isInitialized || !
|
|
207
|
+
if (!isInitialized || !web3) {
|
|
198
208
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
199
209
|
}
|
|
200
210
|
|
|
201
211
|
try {
|
|
202
|
-
const result = await
|
|
212
|
+
const result = await web3.getExplorerUrl(chainId, address, type);
|
|
203
213
|
return result;
|
|
204
214
|
} catch (error) {
|
|
205
215
|
console.error('Failed to generate explorer URL:', error);
|
|
206
216
|
throw error;
|
|
207
217
|
}
|
|
208
|
-
}, [
|
|
218
|
+
}, [web3, isInitialized]);
|
|
209
219
|
|
|
210
220
|
// ==========================================
|
|
211
221
|
// HELPER METHODS (delegating to core SDK)
|
|
@@ -227,9 +237,9 @@ export const useWeb3 = () => {
|
|
|
227
237
|
* @see {@link getAccountOwnedTokensFromContract} - Recommended helper that handles this automatically
|
|
228
238
|
*/
|
|
229
239
|
const extractTokenIds = useCallback((token: TokenDTO): string[] | undefined => {
|
|
230
|
-
// Pure function - delegates to
|
|
231
|
-
return
|
|
232
|
-
}, [
|
|
240
|
+
// Pure function - delegates to Web3Manager (no initialization required)
|
|
241
|
+
return web3?.extractTokenIds(token);
|
|
242
|
+
}, [web3]);
|
|
233
243
|
|
|
234
244
|
/**
|
|
235
245
|
* Get owned tokens from a specific token contract for any blockchain address.
|
|
@@ -269,12 +279,12 @@ export const useWeb3 = () => {
|
|
|
269
279
|
token: TokenDTO,
|
|
270
280
|
maxTokens: number = 50
|
|
271
281
|
): Promise<AccountOwnedTokensResult> => {
|
|
272
|
-
if (!isInitialized || !
|
|
282
|
+
if (!isInitialized || !web3) {
|
|
273
283
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
274
284
|
}
|
|
275
285
|
|
|
276
|
-
return
|
|
277
|
-
}, [
|
|
286
|
+
return web3.getAccountOwnedTokensFromContract(accountAddress, token, maxTokens);
|
|
287
|
+
}, [web3, isInitialized]);
|
|
278
288
|
|
|
279
289
|
/**
|
|
280
290
|
* Build a TokenCollectionRequest from a TokenDTO.
|
|
@@ -293,11 +303,11 @@ export const useWeb3 = () => {
|
|
|
293
303
|
token: TokenDTO,
|
|
294
304
|
maxTokens: number = 50
|
|
295
305
|
): TokenCollectionRequest => {
|
|
296
|
-
if (!
|
|
306
|
+
if (!web3) {
|
|
297
307
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
298
308
|
}
|
|
299
|
-
return
|
|
300
|
-
}, [
|
|
309
|
+
return web3.buildCollectionRequest(accountAddress, token, maxTokens);
|
|
310
|
+
}, [web3]);
|
|
301
311
|
|
|
302
312
|
return {
|
|
303
313
|
getTokenBalance,
|
|
@@ -311,7 +321,7 @@ export const useWeb3 = () => {
|
|
|
311
321
|
extractTokenIds,
|
|
312
322
|
getAccountOwnedTokensFromContract,
|
|
313
323
|
buildCollectionRequest,
|
|
314
|
-
isAvailable: isInitialized && !!
|
|
324
|
+
isAvailable: isInitialized && !!web3,
|
|
315
325
|
};
|
|
316
326
|
};
|
|
317
327
|
|