@explorins/pers-sdk-react-native 2.1.3 → 2.1.6

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 (51) hide show
  1. package/README.md +7 -7
  2. package/dist/hooks/index.d.ts +6 -0
  3. package/dist/hooks/index.d.ts.map +1 -1
  4. package/dist/hooks/index.js +1 -0
  5. package/dist/hooks/useAnalytics.d.ts +37 -14
  6. package/dist/hooks/useAnalytics.d.ts.map +1 -1
  7. package/dist/hooks/useAnalytics.js +239 -19
  8. package/dist/hooks/useCampaigns.d.ts +14 -6
  9. package/dist/hooks/useCampaigns.d.ts.map +1 -1
  10. package/dist/hooks/useCampaigns.js +144 -10
  11. package/dist/hooks/useEvents.d.ts +17 -5
  12. package/dist/hooks/useEvents.d.ts.map +1 -1
  13. package/dist/hooks/useEvents.js +17 -5
  14. package/dist/hooks/useRedemptions.d.ts +5 -2
  15. package/dist/hooks/useRedemptions.d.ts.map +1 -1
  16. package/dist/hooks/useRedemptions.js +53 -2
  17. package/dist/hooks/useTokenBalances.d.ts +24 -0
  18. package/dist/hooks/useTokenBalances.d.ts.map +1 -1
  19. package/dist/hooks/useTokenBalances.js +42 -2
  20. package/dist/hooks/useTransactions.d.ts +8 -5
  21. package/dist/hooks/useTransactions.d.ts.map +1 -1
  22. package/dist/hooks/useTransactions.js +70 -27
  23. package/dist/hooks/useTriggerSources.d.ts +76 -0
  24. package/dist/hooks/useTriggerSources.d.ts.map +1 -0
  25. package/dist/hooks/useTriggerSources.js +272 -0
  26. package/dist/hooks/useUsers.d.ts +5 -4
  27. package/dist/hooks/useUsers.d.ts.map +1 -1
  28. package/dist/hooks/useUsers.js +47 -8
  29. package/dist/hooks/useWeb3.d.ts +6 -5
  30. package/dist/hooks/useWeb3.d.ts.map +1 -1
  31. package/dist/hooks/useWeb3.js +23 -10
  32. package/dist/index.d.ts +12 -3
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +5500 -893
  35. package/dist/index.js.map +1 -1
  36. package/dist/providers/PersSDKProvider.d.ts +38 -0
  37. package/dist/providers/PersSDKProvider.d.ts.map +1 -1
  38. package/dist/providers/PersSDKProvider.js +29 -1
  39. package/package.json +3 -2
  40. package/src/hooks/index.ts +17 -1
  41. package/src/hooks/useAnalytics.ts +268 -21
  42. package/src/hooks/useCampaigns.ts +176 -14
  43. package/src/hooks/useEvents.ts +17 -5
  44. package/src/hooks/useRedemptions.ts +66 -3
  45. package/src/hooks/useTokenBalances.ts +60 -2
  46. package/src/hooks/useTransactions.ts +84 -29
  47. package/src/hooks/useTriggerSources.ts +301 -0
  48. package/src/hooks/useUsers.ts +51 -9
  49. package/src/hooks/useWeb3.ts +28 -13
  50. package/src/index.ts +33 -3
  51. package/src/providers/PersSDKProvider.tsx +38 -1
@@ -5,11 +5,14 @@ import type {
5
5
  CampaignDTO,
6
6
  CampaignClaimDTO,
7
7
  CampaignTriggerDTO,
8
- CampaignCreateRequestDTO,
9
- TokenUnitCreateRequestDTO,
10
- CampaignBusinessEngagementCreateRequestDTO,
11
- PaginatedResponseDTO
8
+ PaginatedResponseDTO,
9
+ CampaignClaimIncludeRelation,
10
+ CampaignIncludeRelation
12
11
  } from '@explorins/pers-shared';
12
+ import type { CampaignClaimFilters } from '@explorins/pers-sdk/campaign';
13
+
14
+ // Re-export for consumers
15
+ export type { CampaignClaimFilters } from '@explorins/pers-sdk/campaign';
13
16
 
14
17
  export const useCampaigns = () => {
15
18
  const { sdk, isInitialized, isAuthenticated } = usePersSDK();
@@ -28,13 +31,30 @@ export const useCampaigns = () => {
28
31
  }
29
32
  }, [sdk, isInitialized]);
30
33
 
31
- const getCampaignById = useCallback(async (campaignId: string): Promise<CampaignDTO | null> => {
34
+ /**
35
+ * Get campaign by ID with optional include relations
36
+ *
37
+ * @param campaignId - The campaign ID
38
+ * @param include - Relations to include: 'triggerSources', 'businesses'
39
+ * @returns Promise resolving to campaign data
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * // Get campaign with all related data
44
+ * const campaign = await getCampaignById('campaign-123', ['triggerSources', 'businesses']);
45
+ * console.log('Trigger sources:', campaign.included?.triggerSources);
46
+ * ```
47
+ */
48
+ const getCampaignById = useCallback(async (
49
+ campaignId: string,
50
+ include?: CampaignIncludeRelation[]
51
+ ): Promise<CampaignDTO | null> => {
32
52
  if (!isInitialized || !sdk) {
33
53
  throw new Error('SDK not initialized. Call initialize() first.');
34
54
  }
35
55
 
36
56
  try {
37
- const result = await sdk.campaigns.getCampaignById(campaignId);
57
+ const result = await sdk.campaigns.getCampaignById(campaignId, include);
38
58
  return result;
39
59
  } catch (error) {
40
60
  console.error('Failed to fetch campaign:', error);
@@ -59,7 +79,29 @@ export const useCampaigns = () => {
59
79
  }
60
80
  }, [sdk, isInitialized, isAuthenticated]);
61
81
 
62
- const getUserClaims = useCallback(async (): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
82
+ /**
83
+ * Get user's campaign claims with optional pagination and include relations
84
+ *
85
+ * @param options - Optional options including pagination and include relations
86
+ * @returns Promise resolving to paginated campaign claims
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * // Get user's claims with campaign details
91
+ * const { data: claims } = await getUserClaims({ include: ['campaign', 'business'] });
92
+ * claims.forEach(claim => {
93
+ * console.log('Campaign:', claim.included?.campaign?.title);
94
+ * });
95
+ *
96
+ * // With pagination
97
+ * const { data } = await getUserClaims({ page: 1, limit: 10, include: ['campaign'] });
98
+ * ```
99
+ */
100
+ const getUserClaims = useCallback(async (options?: {
101
+ page?: number;
102
+ limit?: number;
103
+ include?: CampaignClaimIncludeRelation[];
104
+ }): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
63
105
  if (!isInitialized || !sdk) {
64
106
  throw new Error('SDK not initialized. Call initialize() first.');
65
107
  }
@@ -69,7 +111,7 @@ export const useCampaigns = () => {
69
111
  }
70
112
 
71
113
  try {
72
- const result = await sdk.campaigns.getUserClaims();
114
+ const result = await sdk.campaigns.getUserClaims(options);
73
115
  return result;
74
116
  } catch (error) {
75
117
  console.error('Failed to fetch user claims:', error);
@@ -106,13 +148,32 @@ export const useCampaigns = () => {
106
148
  }
107
149
  }, [sdk, isInitialized]);
108
150
 
109
- const getCampaignClaims = useCallback(async (): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
151
+ /**
152
+ * Admin: Get campaign claims with optional filters and include relations
153
+ *
154
+ * @param filters - Optional filters for campaign, user, or business
155
+ * @param include - Relations to include: 'campaign', 'user', 'business'
156
+ * @returns Promise resolving to paginated campaign claims
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * // Get all claims with user details
161
+ * const { data: claims } = await getCampaignClaims({}, ['user']);
162
+ *
163
+ * // Get claims for a specific campaign
164
+ * const { data } = await getCampaignClaims({ campaignId: 'campaign-123' }, ['user', 'business']);
165
+ * ```
166
+ */
167
+ const getCampaignClaims = useCallback(async (
168
+ filters?: CampaignClaimFilters,
169
+ include?: CampaignClaimIncludeRelation[]
170
+ ): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
110
171
  if (!isInitialized || !sdk) {
111
172
  throw new Error('SDK not initialized. Call initialize() first.');
112
173
  }
113
174
 
114
175
  try {
115
- const result = await sdk.campaigns.getCampaignClaims();
176
+ const result = await sdk.campaigns.getCampaignClaims(filters, include);
116
177
  return result;
117
178
  } catch (error) {
118
179
  console.error('Failed to fetch campaign claims:', error);
@@ -120,13 +181,23 @@ export const useCampaigns = () => {
120
181
  }
121
182
  }, [sdk, isInitialized]);
122
183
 
123
- const getCampaignClaimsByUserId = useCallback(async (userId: string): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
184
+ /**
185
+ * Admin: Get campaign claims by user ID with optional include relations
186
+ *
187
+ * @param userId - The user ID
188
+ * @param include - Relations to include: 'campaign', 'user', 'business'
189
+ * @returns Promise resolving to paginated campaign claims
190
+ */
191
+ const getCampaignClaimsByUserId = useCallback(async (
192
+ userId: string,
193
+ include?: CampaignClaimIncludeRelation[]
194
+ ): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
124
195
  if (!isInitialized || !sdk) {
125
196
  throw new Error('SDK not initialized. Call initialize() first.');
126
197
  }
127
198
 
128
199
  try {
129
- const result = await sdk.campaigns.getCampaignClaimsByUserId(userId);
200
+ const result = await sdk.campaigns.getCampaignClaimsByUserId(userId, undefined, include);
130
201
  return result;
131
202
  } catch (error) {
132
203
  console.error('Failed to fetch campaign claims by user ID:', error);
@@ -134,13 +205,23 @@ export const useCampaigns = () => {
134
205
  }
135
206
  }, [sdk, isInitialized]);
136
207
 
137
- const getCampaignClaimsByBusinessId = useCallback(async (businessId: string): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
208
+ /**
209
+ * Admin: Get campaign claims by business ID with optional include relations
210
+ *
211
+ * @param businessId - The business ID
212
+ * @param include - Relations to include: 'campaign', 'user', 'business'
213
+ * @returns Promise resolving to paginated campaign claims
214
+ */
215
+ const getCampaignClaimsByBusinessId = useCallback(async (
216
+ businessId: string,
217
+ include?: CampaignClaimIncludeRelation[]
218
+ ): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
138
219
  if (!isInitialized || !sdk) {
139
220
  throw new Error('SDK not initialized. Call initialize() first.');
140
221
  }
141
222
 
142
223
  try {
143
- const result = await sdk.campaigns.getCampaignClaimsByBusinessId(businessId);
224
+ const result = await sdk.campaigns.getCampaignClaimsByBusinessId(businessId, undefined, include);
144
225
  return result;
145
226
  } catch (error) {
146
227
  console.error('Failed to fetch campaign claims by business ID:', error);
@@ -148,6 +229,85 @@ export const useCampaigns = () => {
148
229
  }
149
230
  }, [sdk, isInitialized]);
150
231
 
232
+ // ==========================================
233
+ // TRIGGER SOURCE ASSIGNMENT (Admin)
234
+ // Note: TriggerSource CRUD is in useTriggerSources hook
235
+ // ==========================================
236
+
237
+ /**
238
+ * Admin: Assign a trigger source to a campaign
239
+ *
240
+ * Associates a trigger source (QR code, NFC tag, etc.) with a campaign.
241
+ * A campaign can have multiple trigger sources.
242
+ *
243
+ * Note: To create/update/delete trigger sources, use `useTriggerSources` hook.
244
+ *
245
+ * @param campaignId - Campaign UUID
246
+ * @param triggerSourceId - Trigger source UUID
247
+ * @returns Promise resolving to updated campaign
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * const { create } = useTriggerSources();
252
+ * const { assignTriggerSource } = useCampaigns();
253
+ *
254
+ * // Create trigger source first
255
+ * const source = await create({ type: 'QR_CODE', name: 'Store QR' });
256
+ *
257
+ * // Then assign to campaign
258
+ * const updated = await assignTriggerSource('campaign-123', source.id);
259
+ * ```
260
+ */
261
+ const assignTriggerSource = useCallback(async (
262
+ campaignId: string,
263
+ triggerSourceId: string
264
+ ): Promise<CampaignDTO> => {
265
+ if (!isInitialized || !sdk) {
266
+ throw new Error('SDK not initialized. Call initialize() first.');
267
+ }
268
+ if (!isAuthenticated) {
269
+ throw new Error('SDK not authenticated. assignTriggerSource requires admin authentication.');
270
+ }
271
+
272
+ try {
273
+ const result = await sdk.campaigns.assignTriggerSource(campaignId, triggerSourceId);
274
+ return result;
275
+ } catch (error) {
276
+ console.error('Failed to assign trigger source:', error);
277
+ throw error;
278
+ }
279
+ }, [sdk, isInitialized, isAuthenticated]);
280
+
281
+ /**
282
+ * Admin: Remove a trigger source from a campaign
283
+ *
284
+ * Removes the association between a trigger source and a campaign.
285
+ * The trigger source itself is not deleted.
286
+ *
287
+ * @param campaignId - Campaign UUID
288
+ * @param triggerSourceId - Trigger source UUID
289
+ * @returns Promise resolving to updated campaign
290
+ */
291
+ const removeTriggerSource = useCallback(async (
292
+ campaignId: string,
293
+ triggerSourceId: string
294
+ ): Promise<CampaignDTO> => {
295
+ if (!isInitialized || !sdk) {
296
+ throw new Error('SDK not initialized. Call initialize() first.');
297
+ }
298
+ if (!isAuthenticated) {
299
+ throw new Error('SDK not authenticated. removeTriggerSource requires admin authentication.');
300
+ }
301
+
302
+ try {
303
+ const result = await sdk.campaigns.removeTriggerSource(campaignId, triggerSourceId);
304
+ return result;
305
+ } catch (error) {
306
+ console.error('Failed to remove trigger source:', error);
307
+ throw error;
308
+ }
309
+ }, [sdk, isInitialized, isAuthenticated]);
310
+
151
311
  return {
152
312
  getActiveCampaigns,
153
313
  getCampaignById,
@@ -158,6 +318,8 @@ export const useCampaigns = () => {
158
318
  getCampaignClaims,
159
319
  getCampaignClaimsByUserId,
160
320
  getCampaignClaimsByBusinessId,
321
+ assignTriggerSource,
322
+ removeTriggerSource,
161
323
  isAvailable: isInitialized && !!sdk?.campaigns,
162
324
  };
163
325
  };
@@ -35,23 +35,35 @@ export interface EventsHook {
35
35
  * React Native hook for PERS SDK event system
36
36
  *
37
37
  * This hook provides access to the platform-agnostic event system for subscribing to
38
- * transaction, authentication, campaign, and system events. All events include a
39
- * `userMessage` field ready for display to end users.
38
+ * transaction, authentication, campaign, blockchain, and system events. All events
39
+ * include a `userMessage` field ready for display to end users.
40
40
  *
41
41
  * **Event Domains:**
42
42
  * - `auth` - Authentication events (login, logout, token refresh)
43
43
  * - `user` - User profile events (update, create)
44
- * - `transaction` - Transaction events (created, completed, failed)
44
+ * - `transaction` - Transaction events (created, submitted, confirmed)
45
45
  * - `campaign` - Campaign events (claimed, activated)
46
46
  * - `redemption` - Redemption events (redeemed, expired)
47
47
  * - `business` - Business events (created, updated, membership)
48
+ * - `wallet` - Real-time blockchain events (Transfer, Approval, etc.) - Auto-enabled on auth
48
49
  * - `api` - API error events (network, validation, server errors)
49
50
  *
51
+ * **Blockchain Events (Wallet Domain):**
52
+ * When `sdk.connectWalletEvents()` is called (or auto-enabled via `captureWalletEvents: true`),
53
+ * real-time blockchain events are automatically routed through the same event system:
54
+ * ```typescript
55
+ * const { subscribe } = useEvents();
56
+ *
57
+ * subscribe((event) => {
58
+ * if (event.domain === 'wallet') {
59
+ * console.log('Blockchain event:', event.type); // wallet_transfer, wallet_approval, etc.
60
+ * }
61
+ * });
62
+ * ```
63
+ *
50
64
  * **Notification Levels:**
51
65
  * - `success` - Operation completed successfully
52
66
  * - `error` - Operation failed
53
- * - `warning` - Operation completed with warnings
54
- * - `info` - Informational event
55
67
  *
56
68
  * **Cleanup:**
57
69
  * All subscriptions created through this hook are automatically cleaned up when
@@ -7,8 +7,13 @@ import type {
7
7
  RedemptionRedeemDTO,
8
8
  RedemptionRedeemRequestResponseDTO,
9
9
  RedemptionTypeDTO,
10
- PaginatedResponseDTO
10
+ PaginatedResponseDTO,
11
+ RedemptionRedeemIncludeRelation
11
12
  } from '@explorins/pers-shared';
13
+ import type { RedemptionRedeemFilters } from '@explorins/pers-sdk/redemption';
14
+
15
+ // Re-export for consumers
16
+ export type { RedemptionRedeemFilters } from '@explorins/pers-sdk/redemption';
12
17
 
13
18
  export const useRedemptions = () => {
14
19
  const { sdk, isInitialized, isAuthenticated } = usePersSDK();
@@ -36,7 +41,25 @@ export const useRedemptions = () => {
36
41
  }
37
42
  }, [sdk, isInitialized]);
38
43
 
39
- const getUserRedemptions = useCallback(async (): Promise<PaginatedResponseDTO<RedemptionRedeemDTO>> => {
44
+ /**
45
+ * Get user's redemption history with optional include relations
46
+ *
47
+ * @param include - Relations to include: 'redemption', 'user', 'business'
48
+ * @returns Promise resolving to paginated redemption history
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * // Get user redemptions with full redemption details
53
+ * const { data: redeems } = await getUserRedemptions(['redemption', 'business']);
54
+ * redeems.forEach(redeem => {
55
+ * console.log('Redemption:', redeem.included?.redemption?.title);
56
+ * console.log('Business:', redeem.included?.business?.displayName);
57
+ * });
58
+ * ```
59
+ */
60
+ const getUserRedemptions = useCallback(async (
61
+ include?: RedemptionRedeemIncludeRelation[]
62
+ ): Promise<PaginatedResponseDTO<RedemptionRedeemDTO>> => {
40
63
  if (!isInitialized || !sdk) {
41
64
  throw new Error('SDK not initialized. Call initialize() first.');
42
65
  }
@@ -46,7 +69,7 @@ export const useRedemptions = () => {
46
69
  }
47
70
 
48
71
  try {
49
- const result = await sdk.redemptions.getUserRedemptions();
72
+ const result = await sdk.redemptions.getUserRedemptions(undefined, include);
50
73
  return result;
51
74
  } catch (error) {
52
75
  console.error('Failed to fetch user redemptions:', error);
@@ -100,6 +123,45 @@ export const useRedemptions = () => {
100
123
  }, [sdk, isInitialized, isAuthenticated, signAndSubmitTransactionWithJWT, isSignerAvailable]);
101
124
 
102
125
  // Admin methods
126
+
127
+ /**
128
+ * Admin: Get all redemption redeems with filters and include relations
129
+ *
130
+ * Retrieves all redemption redeems across the platform with filtering.
131
+ *
132
+ * @param filters - Optional filters for user, redemption, and pagination
133
+ * @param include - Relations to include: 'redemption', 'user', 'business'
134
+ * @returns Promise resolving to paginated redemption redeems
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * // Get all redeems with user details
139
+ * const { data: redeems } = await getRedemptionRedeems({}, ['user', 'redemption']);
140
+ *
141
+ * // Filter by specific user
142
+ * const { data: userRedeems } = await getRedemptionRedeems(
143
+ * { userId: 'user-123' },
144
+ * ['redemption']
145
+ * );
146
+ * ```
147
+ */
148
+ const getRedemptionRedeems = useCallback(async (
149
+ filters?: RedemptionRedeemFilters,
150
+ include?: RedemptionRedeemIncludeRelation[]
151
+ ): Promise<PaginatedResponseDTO<RedemptionRedeemDTO>> => {
152
+ if (!isInitialized || !sdk) {
153
+ throw new Error('SDK not initialized. Call initialize() first.');
154
+ }
155
+
156
+ try {
157
+ const result = await sdk.redemptions.getRedemptionRedeems(filters, include);
158
+ return result;
159
+ } catch (error) {
160
+ console.error('Failed to fetch redemption redeems:', error);
161
+ throw error;
162
+ }
163
+ }, [sdk, isInitialized]);
164
+
103
165
  const createRedemption = useCallback(async (redemptionData: RedemptionCreateRequestDTO): Promise<RedemptionDTO> => {
104
166
  if (!isInitialized || !sdk) {
105
167
  throw new Error('SDK not initialized. Call initialize() first.');
@@ -161,6 +223,7 @@ export const useRedemptions = () => {
161
223
  getUserRedemptions,
162
224
  redeem,
163
225
  getRedemptionTypes,
226
+ getRedemptionRedeems,
164
227
  createRedemption,
165
228
  updateRedemption,
166
229
  toggleRedemptionStatus,
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from 'react';
1
+ import { useCallback, useEffect, useState, useRef } from 'react';
2
2
  import { usePersSDK } from '../providers/PersSDKProvider';
3
3
  import { useWeb3 } from './useWeb3';
4
4
  import { NativeTokenTypes, type TokenDTO } from '@explorins/pers-shared';
@@ -36,6 +36,18 @@ export interface UseTokenBalancesOptions {
36
36
  autoLoad?: boolean;
37
37
  /** Optional refresh interval in milliseconds (0 = disabled) */
38
38
  refreshInterval?: number;
39
+ /**
40
+ * Auto-refresh balances when wallet events are received (Transfer, Approval, etc.)
41
+ * Requires `captureWalletEvents: true` in SDK config (default)
42
+ * @default true
43
+ */
44
+ refreshOnWalletEvents?: boolean;
45
+ /**
46
+ * Debounce time for wallet event-triggered refreshes (milliseconds)
47
+ * Prevents rapid refresh calls during batch transfers
48
+ * @default 1000
49
+ */
50
+ walletEventDebounceMs?: number;
39
51
  }
40
52
 
41
53
  /**
@@ -126,6 +138,18 @@ export interface UseTokenBalancesResult {
126
138
  * ```
127
139
  *
128
140
  * @example
141
+ * **With Wallet Events (Real-time):**
142
+ * ```typescript
143
+ * // Auto-refresh on Transfer, Approval, and other blockchain events
144
+ * const { tokenBalances } = useTokenBalances({
145
+ * accountAddress: walletAddress!,
146
+ * availableTokens,
147
+ * refreshOnWalletEvents: true, // Enable real-time refresh (default: true)
148
+ * walletEventDebounceMs: 1000 // Debounce rapid events (default: 1000ms)
149
+ * });
150
+ * ```
151
+ *
152
+ * @example
129
153
  * **Multi-Wallet Support:**
130
154
  * ```typescript
131
155
  * function MultiWalletBalances() {
@@ -148,7 +172,9 @@ export function useTokenBalances(options: UseTokenBalancesOptions): UseTokenBala
148
172
  accountAddress,
149
173
  availableTokens = [],
150
174
  autoLoad = true,
151
- refreshInterval = 0
175
+ refreshInterval = 0,
176
+ refreshOnWalletEvents = true,
177
+ walletEventDebounceMs = 1000
152
178
  } = options;
153
179
 
154
180
  const { isAuthenticated, sdk } = usePersSDK();
@@ -157,6 +183,9 @@ export function useTokenBalances(options: UseTokenBalancesOptions): UseTokenBala
157
183
  const [tokenBalances, setTokenBalances] = useState<TokenBalanceWithToken[]>([]);
158
184
  const [isLoading, setIsLoading] = useState(false);
159
185
  const [error, setError] = useState<string | null>(null);
186
+
187
+ // Debounce ref for wallet events
188
+ const walletEventDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
160
189
 
161
190
  // Check if the hook is available for use
162
191
  const isAvailable = web3.isAvailable && isAuthenticated && !!accountAddress;
@@ -293,6 +322,35 @@ export function useTokenBalances(options: UseTokenBalancesOptions): UseTokenBala
293
322
  };
294
323
  }, [sdk, refreshInterval, isAvailable, loadBalances]);
295
324
 
325
+ // Wallet events refresh: listen for real-time blockchain events
326
+ // Refreshes on ANY wallet domain event (Transfer, TransferSingle, etc.)
327
+ // Debouncing prevents excessive API calls during rapid events
328
+ useEffect(() => {
329
+ if (!sdk || !refreshOnWalletEvents || !isAvailable) return;
330
+
331
+ const unsubscribe = sdk.events.subscribe((event) => {
332
+ if (event.domain === 'wallet') {
333
+ // Debounce rapid events (batch transfers, etc.)
334
+ if (walletEventDebounceRef.current) {
335
+ clearTimeout(walletEventDebounceRef.current);
336
+ }
337
+
338
+ walletEventDebounceRef.current = setTimeout(() => {
339
+ console.log(`[useTokenBalances] Wallet event (${event.type}), refreshing balances...`);
340
+ loadBalances();
341
+ walletEventDebounceRef.current = null;
342
+ }, walletEventDebounceMs);
343
+ }
344
+ }, { domains: ['wallet'] });
345
+
346
+ return () => {
347
+ unsubscribe();
348
+ if (walletEventDebounceRef.current) {
349
+ clearTimeout(walletEventDebounceRef.current);
350
+ }
351
+ };
352
+ }, [sdk, refreshOnWalletEvents, walletEventDebounceMs, isAvailable, loadBalances]);
353
+
296
354
  return {
297
355
  tokenBalances,
298
356
  isLoading,