@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,468 +1,468 @@
1
- import { useCallback } from 'react';
2
- import { usePersSDK } from '../providers/PersSDKProvider';
3
- import { SubmissionResult, useTransactionSigner, OnStatusUpdateFn } from './useTransactionSigner';
4
- import type {
5
- RedemptionCreateRequestDTO,
6
- RedemptionDTO,
7
- RedemptionRedeemDTO,
8
- RedemptionRedeemRequestResponseDTO,
9
- RedemptionTypeDTO,
10
- PaginatedResponseDTO,
11
- RedemptionRedeemIncludeRelation,
12
- RedemptionIncludeRelation,
13
- ProcessRecordStatus,
14
- DynamicContext,
15
- SortOrder
16
- } from '@explorins/pers-sdk';
17
- import type { RedemptionRedeemQueryParams, RedemptionQueryParams } from '@explorins/pers-sdk/redemption';
18
- import type { PaginationOptions } from '@explorins/pers-sdk';
19
-
20
- // Re-export for consumers (new names + deprecated aliases)
21
- export type { RedemptionRedeemQueryParams, RedemptionQueryParams } from '@explorins/pers-sdk/redemption';
22
- /** @deprecated Use RedemptionRedeemQueryParams instead */
23
- export type RedemptionRedeemFilters = RedemptionRedeemQueryParams;
24
- /** @deprecated Use RedemptionQueryParams instead */
25
- export type RedemptionFilterOptions = RedemptionQueryParams;
26
-
27
- /**
28
- * Options for fetching redemptions with filtering and pagination
29
- */
30
- export interface GetRedemptionsOptions {
31
- /** Filter by active status (Admin only - regular users always see active offers) */
32
- active?: boolean;
33
- /** Free text search across name, description, tags (case-insensitive) */
34
- search?: string;
35
- /** Filter by tag(s) - single tag or array for OR match */
36
- tags?: string | string[];
37
- /** Filter redemptions starting on or after this date */
38
- startDate?: Date | string;
39
- /** Filter redemptions ending on or before this date */
40
- endDate?: Date | string;
41
- /** Relations to include: 'redeemCount' */
42
- include?: RedemptionIncludeRelation[];
43
- /** Page number (1-indexed) */
44
- page?: number;
45
- /** Results per page */
46
- limit?: number;
47
- /** Field to sort by */
48
- sortBy?: 'name' | 'createdAt' | 'startDate';
49
- /** Sort direction */
50
- sortOrder?: SortOrder;
51
- }
52
-
53
- export const useRedemptions = () => {
54
- const { sdk, isInitialized, isAuthenticated } = usePersSDK();
55
- const { signAndSubmitTransactionWithJWT, isSignerAvailable, currentStatus, statusMessage } = useTransactionSigner();
56
-
57
- /**
58
- * Get redemption offers based on user permissions
59
- *
60
- * **Regular Users:** Automatically see only active redemptions available for purchase
61
- * **Administrators:** Can see all redemptions and filter by active/inactive status
62
- *
63
- * The backend enforces permission-based filtering automatically.
64
- *
65
- * @param options - Filter and pagination options
66
- * @param options.active - Filter by active status (Admin only)
67
- * @param options.search - Free text search across name, description, tags
68
- * @param options.startDate - Filter redemptions starting on or after this date
69
- * @param options.endDate - Filter redemptions ending on or before this date
70
- * @param options.include - Relations to include: 'redeemCount'
71
- * @param options.page - Page number (1-indexed)
72
- * @param options.limit - Results per page
73
- * @param options.sortBy - Field to sort by ('name' | 'createdAt')
74
- * @param options.sortOrder - Sort direction ('ASC' | 'DESC')
75
- *
76
- * @example Basic usage
77
- * ```typescript
78
- * const { data: offers } = await getRedemptions();
79
- * ```
80
- *
81
- * @example Filter by date range
82
- * ```typescript
83
- * const { data: dateFiltered } = await getRedemptions({
84
- * startDate: '2026-05-01',
85
- * endDate: '2026-06-30'
86
- * });
87
- * ```
88
- *
89
- * @example Get redemptions with redeem count
90
- * ```typescript
91
- * const { data: offers } = await getRedemptions({ include: ['redeemCount'] });
92
- * offers.forEach(o => console.log(`${o.name}: ${o.redeemCount} redeems`));
93
- * ```
94
- *
95
- * @example Search with pagination
96
- * ```typescript
97
- * const { data, pagination } = await getRedemptions({
98
- * search: 'discount',
99
- * page: 1,
100
- * limit: 10,
101
- * sortBy: 'name',
102
- * sortOrder: 'ASC'
103
- * });
104
- * ```
105
- */
106
- const getRedemptions = useCallback(async (options?: GetRedemptionsOptions): Promise<PaginatedResponseDTO<RedemptionDTO>> => {
107
- if (!isInitialized || !sdk) {
108
- throw new Error('SDK not initialized. Call initialize() first.');
109
- }
110
-
111
- try {
112
- // Convert Date to ISO string for API compatibility
113
- const apiOptions = options ? {
114
- ...options,
115
- startDate: options.startDate instanceof Date ? options.startDate.toISOString() : options.startDate,
116
- endDate: options.endDate instanceof Date ? options.endDate.toISOString() : options.endDate,
117
- } : undefined;
118
-
119
- const result = await sdk.redemptions.getRedemptions(apiOptions);
120
- return result;
121
- } catch (error) {
122
- console.error('Failed to fetch redemptions:', error);
123
- throw error;
124
- }
125
- }, [sdk, isInitialized]);
126
-
127
- /**
128
- * Get user's redemption history with optional filters, pagination and include relations
129
- *
130
- * @param filters - Optional filters (redemptionId, status)
131
- * @param options - Pagination options (page, limit, sortBy, sortOrder)
132
- * @param include - Relations to include: 'redemption', 'user', 'business', 'transactions'
133
- * @returns Promise resolving to paginated redemption history
134
- *
135
- * @example
136
- * ```typescript
137
- * // Get first page of user redemptions
138
- * const { data: redeems, pagination } = await getUserRedemptions({}, { page: 1, limit: 10 });
139
- * console.log(`Page ${pagination.page} of ${pagination.pages}`);
140
- *
141
- * // Get only completed redemptions
142
- * import { ProcessRecordStatus } from '@explorins/pers-shared';
143
- * const { data: completed } = await getUserRedemptions({ status: ProcessRecordStatus.COMPLETED });
144
- *
145
- * // Get user redemptions with full redemption details
146
- * const { data: redeems } = await getUserRedemptions({}, {}, ['redemption', 'business']);
147
- * redeems.forEach(redeem => {
148
- * console.log('Redemption:', redeem.included?.redemption?.name);
149
- * console.log('Business:', redeem.included?.business?.displayName);
150
- * });
151
- * ```
152
- */
153
- const getUserRedemptions = useCallback(async (
154
- filters?: { redemptionId?: string; status?: ProcessRecordStatus },
155
- options?: PaginationOptions,
156
- include?: RedemptionRedeemIncludeRelation[]
157
- ): Promise<PaginatedResponseDTO<RedemptionRedeemDTO>> => {
158
- if (!isInitialized || !sdk) {
159
- throw new Error('SDK not initialized. Call initialize() first.');
160
- }
161
- if (!isAuthenticated) {
162
- console.warn('SDK not authenticated. getUserRedemptions requires authentication.');
163
- return { data: [], pagination: { page: 1, limit: 0, total: 0, pages: 0, hasNext: false, hasPrev: false } };
164
- }
165
-
166
- try {
167
- const result = await sdk.redemptions.getUserRedemptions(filters, options, include);
168
- return result;
169
- } catch (error) {
170
- console.error('Failed to fetch user redemptions:', error);
171
- throw error;
172
- }
173
- }, [sdk, isInitialized, isAuthenticated]);
174
-
175
- /**
176
- * Redeem a redemption offer with optional dynamic context
177
- *
178
- * ## When to use `redeem()` vs `createTransaction()`
179
- *
180
- * | Use Case | Method | Auto-Signs |
181
- * |----------|--------|------------|
182
- * | **Voucher redemption** | `redeem()` | ✅ Yes |
183
- * | **Reward redemption** | `redeem()` | ✅ Yes |
184
- * | **Direct token transfer** | `createTransaction()` | ⚠️ Depends |
185
- * | **POS burn with QR** | `buildPOSBurnRequest()` | ❌ Manual |
186
- *
187
- * **Use `redeem()` for voucher/reward redemptions.** It automatically:
188
- * 1. Creates redemption record
189
- * 2. Triggers passkey/biometric prompt
190
- * 3. Signs the transaction
191
- * 4. Submits to blockchain
192
- * 5. Returns result with `transactionHash`
193
- *
194
- * **No admin approval required** - The "PENDING" state is brief (seconds).
195
- * The user's passkey signature IS the approval.
196
- *
197
- * Supports dynamic context for personalized NFT generation when the redemption
198
- * issues ERC721 tokens. Context can include validity dates, AI prompt placeholders,
199
- * and custom metadata.
200
- *
201
- * @param redemptionId - ID of the redemption to redeem
202
- * @param options - Optional settings
203
- * @param options.context - Dynamic context for NFT personalization (validity dates, AI prompts)
204
- * @param options.onStatusUpdate - Callback for transaction signing status updates
205
- * @returns Promise resolving to redemption result with `transactionHash` on success
206
- *
207
- * @example Basic redemption (most common)
208
- * ```typescript
209
- * const { redeem } = useRedemptions();
210
- *
211
- * const result = await redeem('redemption-123', {
212
- * onStatusUpdate: (status, message) => {
213
- * console.log(`Status: ${status} - ${message}`);
214
- * }
215
- * });
216
- *
217
- * if (result.transactionHash) {
218
- * console.log('Redemption complete!', result.transactionHash);
219
- * }
220
- * ```
221
- *
222
- * @example Redemption with dynamic context (personalized NFT)
223
- * ```typescript
224
- * const result = await redeem('event-ticket-123', {
225
- * context: {
226
- * // Validity - token valid for event duration
227
- * validityDate: new Date().toISOString(),
228
- * validityDuration: 8, // 8 hours
229
- *
230
- * // AI prompt context - available as {{context.xxx}} in prompts
231
- * attendeeName: 'John Doe',
232
- * ticketTier: 'VIP',
233
- * seatSection: 'A12',
234
- * }
235
- * });
236
- * ```
237
- *
238
- * @example Hotel stay NFT with check-in/check-out dates
239
- * ```typescript
240
- * const result = await redeem('hotel-nft-456', {
241
- * context: {
242
- * validityDate: '2026-04-15T14:00:00Z', // Check-in
243
- * validityEndDate: '2026-04-20T11:00:00Z', // Check-out
244
- * guestName: 'Jane Smith',
245
- * roomNumber: '305',
246
- * roomType: 'Suite',
247
- * }
248
- * });
249
- * ```
250
- *
251
- * @see {@link useTransactions.createTransaction | createTransaction()} for direct transfers without redemption flow
252
- */
253
- const redeem = useCallback(async (
254
- redemptionId: string,
255
- options?: { context?: DynamicContext; onStatusUpdate?: OnStatusUpdateFn }
256
- ): Promise<RedemptionRedeemRequestResponseDTO> => {
257
- if (!isInitialized || !sdk) {
258
- throw new Error('SDK not initialized. Call initialize() first.');
259
- }
260
- if (!isAuthenticated) {
261
- throw new Error('SDK not authenticated. redeem requires authentication.');
262
- }
263
-
264
- try {
265
- const result = await sdk.redemptions.redeem(redemptionId, options?.context);
266
-
267
- const txToken = result.senderTransaction?.actionable?.authToken;
268
- if (txToken && isSignerAvailable) {
269
- try {
270
- const signingResult: SubmissionResult = await signAndSubmitTransactionWithJWT(txToken, options?.onStatusUpdate);
271
- if (signingResult.success) {
272
- // Return enhanced result with signing information
273
- return {
274
- ...result,
275
- transactionHash: signingResult.transactionHash,
276
- // signature: signingResult.signature,
277
- isSigned: true,
278
- signedAt: new Date().toISOString()
279
- } as RedemptionRedeemRequestResponseDTO;
280
- } else {
281
- console.error('Transaction signing failed:', signingResult.error);
282
- throw new Error(signingResult.error || 'Transaction signing failed');
283
- }
284
- } catch (signingError) {
285
- console.error('Blockchain signing error:', signingError);
286
- throw new Error(`Transaction signing failed: ${signingError}`);
287
- }
288
- } else if (txToken && !isSignerAvailable) {
289
- console.warn('Transaction requires signature but signer is not available');
290
- throw new Error('Transaction requires signature but blockchain signer is not initialized');
291
- }
292
-
293
- // Return original result if no signing was required
294
- return result;
295
- } catch (error) {
296
- console.error('Failed to redeem redemption:', error);
297
- throw error;
298
- }
299
- }, [sdk, isInitialized, isAuthenticated, signAndSubmitTransactionWithJWT, isSignerAvailable]);
300
-
301
- // Admin methods
302
-
303
- /**
304
- * Admin: Get all redemption redeems with filters and include relations
305
- *
306
- * Retrieves all redemption redeems across the platform with filtering.
307
- *
308
- * @param filters - Optional filters for user, redemption, date range, and pagination
309
- * @param filters.userId - Filter by user ID
310
- * @param filters.redemptionId - Filter by redemption offer ID
311
- * @param filters.businessId - Filter by business ID (admin only)
312
- * @param filters.status - Filter by processing status (PENDING, PROCESSING, COMPLETED, FAILED)
313
- * @param filters.dateFrom - Filter redeems created on or after this date (ISO 8601 format)
314
- * @param filters.dateTo - Filter redeems created on or before this date (ISO 8601 format)
315
- * @param filters.page - Page number (1-indexed)
316
- * @param filters.limit - Items per page
317
- * @param filters.sortBy - Field to sort by
318
- * @param filters.sortOrder - Sort direction
319
- * @param include - Relations to include: 'redemption', 'user', 'business'
320
- * @returns Promise resolving to paginated redemption redeems
321
- *
322
- * @example
323
- * ```typescript
324
- * // Get all redeems with user details
325
- * const { data: redeems } = await getRedemptionRedeems({}, ['user', 'redemption']);
326
- *
327
- * // Filter by specific user
328
- * const { data: userRedeems } = await getRedemptionRedeems(
329
- * { userId: 'user-123' },
330
- * ['redemption']
331
- * );
332
- *
333
- * // Filter by date range
334
- * const { data } = await getRedemptionRedeems({
335
- * dateFrom: '2026-01-01',
336
- * dateTo: '2026-03-31'
337
- * }, ['user', 'redemption']);
338
- * ```
339
- */
340
- const getRedemptionRedeems = useCallback(async (
341
- filters?: RedemptionRedeemQueryParams,
342
- include?: RedemptionRedeemIncludeRelation[]
343
- ): Promise<PaginatedResponseDTO<RedemptionRedeemDTO>> => {
344
- if (!isInitialized || !sdk) {
345
- throw new Error('SDK not initialized. Call initialize() first.');
346
- }
347
-
348
- try {
349
- const result = await sdk.redemptions.getRedemptionRedeems(filters, include);
350
- return result;
351
- } catch (error) {
352
- console.error('Failed to fetch redemption redeems:', error);
353
- throw error;
354
- }
355
- }, [sdk, isInitialized]);
356
-
357
- const createRedemption = useCallback(async (redemptionData: RedemptionCreateRequestDTO): Promise<RedemptionDTO> => {
358
- if (!isInitialized || !sdk) {
359
- throw new Error('SDK not initialized. Call initialize() first.');
360
- }
361
-
362
- try {
363
- const result = await sdk.redemptions.createRedemption(redemptionData);
364
- return result;
365
- } catch (error) {
366
- console.error('Failed to create redemption offer:', error);
367
- throw error;
368
- }
369
- }, [sdk, isInitialized]);
370
-
371
- const getRedemptionTypes = useCallback(async (): Promise<PaginatedResponseDTO<RedemptionTypeDTO>> => {
372
- if (!isInitialized || !sdk) {
373
- throw new Error('SDK not initialized. Call initialize() first.');
374
- }
375
-
376
- try {
377
- const result = await sdk.redemptions.getRedemptionTypes();
378
- return result;
379
- } catch (error) {
380
- console.error('Failed to fetch redemption types:', error);
381
- throw error;
382
- }
383
- }, [sdk, isInitialized]);
384
-
385
- const updateRedemption = useCallback(async (redemptionId: string, redemptionData: RedemptionCreateRequestDTO): Promise<RedemptionDTO> => {
386
- if (!isInitialized || !sdk) {
387
- throw new Error('SDK not initialized. Call initialize() first.');
388
- }
389
-
390
- try {
391
- const result = await sdk.redemptions.updateRedemption(redemptionId, redemptionData);
392
- return result;
393
- } catch (error) {
394
- console.error('Failed to update redemption:', error);
395
- throw error;
396
- }
397
- }, [sdk, isInitialized]);
398
-
399
- const toggleRedemptionStatus = useCallback(async (redemptionId: string): Promise<RedemptionDTO> => {
400
- if (!isInitialized || !sdk) {
401
- throw new Error('SDK not initialized. Call initialize() first.');
402
- }
403
-
404
- try {
405
- const result = await sdk.redemptions.toggleRedemptionStatus(redemptionId);
406
- return result;
407
- } catch (error) {
408
- console.error('Failed to toggle redemption status:', error);
409
- throw error;
410
- }
411
- }, [sdk, isInitialized]);
412
-
413
- /**
414
- * Get a specific redemption by ID
415
- *
416
- * Retrieves detailed information about a specific redemption offer.
417
- *
418
- * @param id - Redemption UUID
419
- * @param include - Optional relations to include: 'redeemCount'
420
- * @returns Promise resolving to the redemption
421
- *
422
- * @example Basic usage
423
- * ```typescript
424
- * const redemption = await getRedemptionById('7baf4d39-0bd6-45ca-9c66-8c0d655767c3');
425
- * console.log(redemption.name);
426
- * ```
427
- *
428
- * @example With redeem count
429
- * ```typescript
430
- * const redemption = await getRedemptionById('redemption-123', ['redeemCount']);
431
- * console.log(`${redemption.name} has been redeemed ${redemption.redeemCount} times`);
432
- * ```
433
- */
434
- const getRedemptionById = useCallback(async (
435
- id: string,
436
- include?: RedemptionIncludeRelation[]
437
- ): Promise<RedemptionDTO> => {
438
- if (!isInitialized || !sdk) {
439
- throw new Error('SDK not initialized. Call initialize() first.');
440
- }
441
-
442
- try {
443
- const result = await sdk.redemptions.getRedemptionById(id, include);
444
- return result;
445
- } catch (error) {
446
- console.error('Failed to fetch redemption by ID:', error);
447
- throw error;
448
- }
449
- }, [sdk, isInitialized]);
450
-
451
- return {
452
- getRedemptions,
453
- getRedemptionById,
454
- getUserRedemptions,
455
- redeem,
456
- getRedemptionTypes,
457
- getRedemptionRedeems,
458
- createRedemption,
459
- updateRedemption,
460
- toggleRedemptionStatus,
461
- isAvailable: isInitialized && !!sdk?.redemptions,
462
- // Expose signing status for UI feedback
463
- signingStatus: currentStatus,
464
- signingStatusMessage: statusMessage,
465
- };
466
- };
467
-
1
+ import { useCallback } from 'react';
2
+ import { usePersSDK } from '../providers/PersSDKProvider';
3
+ import { SubmissionResult, useTransactionSigner, OnStatusUpdateFn } from './useTransactionSigner';
4
+ import type {
5
+ RedemptionCreateRequestDTO,
6
+ RedemptionDTO,
7
+ RedemptionRedeemDTO,
8
+ RedemptionRedeemRequestResponseDTO,
9
+ RedemptionTypeDTO,
10
+ PaginatedResponseDTO,
11
+ RedemptionRedeemIncludeRelation,
12
+ RedemptionIncludeRelation,
13
+ ProcessRecordStatus,
14
+ DynamicContext,
15
+ SortOrder
16
+ } from '@explorins/pers-sdk';
17
+ import type { RedemptionRedeemQueryParams, RedemptionQueryParams } from '@explorins/pers-sdk/redemption';
18
+ import type { PaginationOptions } from '@explorins/pers-sdk';
19
+
20
+ // Re-export for consumers (new names + deprecated aliases)
21
+ export type { RedemptionRedeemQueryParams, RedemptionQueryParams } from '@explorins/pers-sdk/redemption';
22
+ /** @deprecated Use RedemptionRedeemQueryParams instead */
23
+ export type RedemptionRedeemFilters = RedemptionRedeemQueryParams;
24
+ /** @deprecated Use RedemptionQueryParams instead */
25
+ export type RedemptionFilterOptions = RedemptionQueryParams;
26
+
27
+ /**
28
+ * Options for fetching redemptions with filtering and pagination
29
+ */
30
+ export interface GetRedemptionsOptions {
31
+ /** Filter by active status (Admin only - regular users always see active offers) */
32
+ active?: boolean;
33
+ /** Free text search across name, description, tags (case-insensitive) */
34
+ search?: string;
35
+ /** Filter by tag(s) - single tag or array for OR match */
36
+ tags?: string | string[];
37
+ /** Filter redemptions starting on or after this date */
38
+ startDate?: Date | string;
39
+ /** Filter redemptions ending on or before this date */
40
+ endDate?: Date | string;
41
+ /** Relations to include: 'redeemCount' */
42
+ include?: RedemptionIncludeRelation[];
43
+ /** Page number (1-indexed) */
44
+ page?: number;
45
+ /** Results per page */
46
+ limit?: number;
47
+ /** Field to sort by */
48
+ sortBy?: 'name' | 'createdAt' | 'startDate';
49
+ /** Sort direction */
50
+ sortOrder?: SortOrder;
51
+ }
52
+
53
+ export const useRedemptions = () => {
54
+ const { sdk, isInitialized, isAuthenticated } = usePersSDK();
55
+ const { signAndSubmitTransactionWithJWT, isSignerAvailable, currentStatus, statusMessage } = useTransactionSigner();
56
+
57
+ /**
58
+ * Get redemption offers based on user permissions
59
+ *
60
+ * **Regular Users:** Automatically see only active redemptions available for purchase
61
+ * **Administrators:** Can see all redemptions and filter by active/inactive status
62
+ *
63
+ * The backend enforces permission-based filtering automatically.
64
+ *
65
+ * @param options - Filter and pagination options
66
+ * @param options.active - Filter by active status (Admin only)
67
+ * @param options.search - Free text search across name, description, tags
68
+ * @param options.startDate - Filter redemptions starting on or after this date
69
+ * @param options.endDate - Filter redemptions ending on or before this date
70
+ * @param options.include - Relations to include: 'redeemCount'
71
+ * @param options.page - Page number (1-indexed)
72
+ * @param options.limit - Results per page
73
+ * @param options.sortBy - Field to sort by ('name' | 'createdAt')
74
+ * @param options.sortOrder - Sort direction ('ASC' | 'DESC')
75
+ *
76
+ * @example Basic usage
77
+ * ```typescript
78
+ * const { data: offers } = await getRedemptions();
79
+ * ```
80
+ *
81
+ * @example Filter by date range
82
+ * ```typescript
83
+ * const { data: dateFiltered } = await getRedemptions({
84
+ * startDate: '2026-05-01',
85
+ * endDate: '2026-06-30'
86
+ * });
87
+ * ```
88
+ *
89
+ * @example Get redemptions with redeem count
90
+ * ```typescript
91
+ * const { data: offers } = await getRedemptions({ include: ['redeemCount'] });
92
+ * offers.forEach(o => console.log(`${o.name}: ${o.redeemCount} redeems`));
93
+ * ```
94
+ *
95
+ * @example Search with pagination
96
+ * ```typescript
97
+ * const { data, pagination } = await getRedemptions({
98
+ * search: 'discount',
99
+ * page: 1,
100
+ * limit: 10,
101
+ * sortBy: 'name',
102
+ * sortOrder: 'ASC'
103
+ * });
104
+ * ```
105
+ */
106
+ const getRedemptions = useCallback(async (options?: GetRedemptionsOptions): Promise<PaginatedResponseDTO<RedemptionDTO>> => {
107
+ if (!isInitialized || !sdk) {
108
+ throw new Error('SDK not initialized. Call initialize() first.');
109
+ }
110
+
111
+ try {
112
+ // Convert Date to ISO string for API compatibility
113
+ const apiOptions = options ? {
114
+ ...options,
115
+ startDate: options.startDate instanceof Date ? options.startDate.toISOString() : options.startDate,
116
+ endDate: options.endDate instanceof Date ? options.endDate.toISOString() : options.endDate,
117
+ } : undefined;
118
+
119
+ const result = await sdk.redemptions.getRedemptions(apiOptions);
120
+ return result;
121
+ } catch (error) {
122
+ console.error('Failed to fetch redemptions:', error);
123
+ throw error;
124
+ }
125
+ }, [sdk, isInitialized]);
126
+
127
+ /**
128
+ * Get user's redemption history with optional filters, pagination and include relations
129
+ *
130
+ * @param filters - Optional filters (redemptionId, status)
131
+ * @param options - Pagination options (page, limit, sortBy, sortOrder)
132
+ * @param include - Relations to include: 'redemption', 'user', 'business', 'transactions'
133
+ * @returns Promise resolving to paginated redemption history
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * // Get first page of user redemptions
138
+ * const { data: redeems, pagination } = await getUserRedemptions({}, { page: 1, limit: 10 });
139
+ * console.log(`Page ${pagination.page} of ${pagination.pages}`);
140
+ *
141
+ * // Get only completed redemptions
142
+ * import { ProcessRecordStatus } from '@explorins/pers-shared';
143
+ * const { data: completed } = await getUserRedemptions({ status: ProcessRecordStatus.COMPLETED });
144
+ *
145
+ * // Get user redemptions with full redemption details
146
+ * const { data: redeems } = await getUserRedemptions({}, {}, ['redemption', 'business']);
147
+ * redeems.forEach(redeem => {
148
+ * console.log('Redemption:', redeem.included?.redemption?.name);
149
+ * console.log('Business:', redeem.included?.business?.displayName);
150
+ * });
151
+ * ```
152
+ */
153
+ const getUserRedemptions = useCallback(async (
154
+ filters?: { redemptionId?: string; status?: ProcessRecordStatus },
155
+ options?: PaginationOptions,
156
+ include?: RedemptionRedeemIncludeRelation[]
157
+ ): Promise<PaginatedResponseDTO<RedemptionRedeemDTO>> => {
158
+ if (!isInitialized || !sdk) {
159
+ throw new Error('SDK not initialized. Call initialize() first.');
160
+ }
161
+ if (!isAuthenticated) {
162
+ console.warn('SDK not authenticated. getUserRedemptions requires authentication.');
163
+ return { data: [], pagination: { page: 1, limit: 0, total: 0, pages: 0, hasNext: false, hasPrev: false } };
164
+ }
165
+
166
+ try {
167
+ const result = await sdk.redemptions.getUserRedemptions(filters, options, include);
168
+ return result;
169
+ } catch (error) {
170
+ console.error('Failed to fetch user redemptions:', error);
171
+ throw error;
172
+ }
173
+ }, [sdk, isInitialized, isAuthenticated]);
174
+
175
+ /**
176
+ * Redeem a redemption offer with optional dynamic context
177
+ *
178
+ * ## When to use `redeem()` vs `createTransaction()`
179
+ *
180
+ * | Use Case | Method | Auto-Signs |
181
+ * |----------|--------|------------|
182
+ * | **Voucher redemption** | `redeem()` | ✅ Yes |
183
+ * | **Reward redemption** | `redeem()` | ✅ Yes |
184
+ * | **Direct token transfer** | `createTransaction()` | ⚠️ Depends |
185
+ * | **POS burn with QR** | `buildPOSBurnRequest()` | ❌ Manual |
186
+ *
187
+ * **Use `redeem()` for voucher/reward redemptions.** It automatically:
188
+ * 1. Creates redemption record
189
+ * 2. Triggers passkey/biometric prompt
190
+ * 3. Signs the transaction
191
+ * 4. Submits to blockchain
192
+ * 5. Returns result with `transactionHash`
193
+ *
194
+ * **No admin approval required** - The "PENDING" state is brief (seconds).
195
+ * The user's passkey signature IS the approval.
196
+ *
197
+ * Supports dynamic context for personalized NFT generation when the redemption
198
+ * issues ERC721 tokens. Context can include validity dates, AI prompt placeholders,
199
+ * and custom metadata.
200
+ *
201
+ * @param redemptionId - ID of the redemption to redeem
202
+ * @param options - Optional settings
203
+ * @param options.context - Dynamic context for NFT personalization (validity dates, AI prompts)
204
+ * @param options.onStatusUpdate - Callback for transaction signing status updates
205
+ * @returns Promise resolving to redemption result with `transactionHash` on success
206
+ *
207
+ * @example Basic redemption (most common)
208
+ * ```typescript
209
+ * const { redeem } = useRedemptions();
210
+ *
211
+ * const result = await redeem('redemption-123', {
212
+ * onStatusUpdate: (status, message) => {
213
+ * console.log(`Status: ${status} - ${message}`);
214
+ * }
215
+ * });
216
+ *
217
+ * if (result.transactionHash) {
218
+ * console.log('Redemption complete!', result.transactionHash);
219
+ * }
220
+ * ```
221
+ *
222
+ * @example Redemption with dynamic context (personalized NFT)
223
+ * ```typescript
224
+ * const result = await redeem('event-ticket-123', {
225
+ * context: {
226
+ * // Validity - token valid for event duration
227
+ * validityDate: new Date().toISOString(),
228
+ * validityDuration: 8, // 8 hours
229
+ *
230
+ * // AI prompt context - available as {{context.xxx}} in prompts
231
+ * attendeeName: 'John Doe',
232
+ * ticketTier: 'VIP',
233
+ * seatSection: 'A12',
234
+ * }
235
+ * });
236
+ * ```
237
+ *
238
+ * @example Hotel stay NFT with check-in/check-out dates
239
+ * ```typescript
240
+ * const result = await redeem('hotel-nft-456', {
241
+ * context: {
242
+ * validityDate: '2026-04-15T14:00:00Z', // Check-in
243
+ * validityEndDate: '2026-04-20T11:00:00Z', // Check-out
244
+ * guestName: 'Jane Smith',
245
+ * roomNumber: '305',
246
+ * roomType: 'Suite',
247
+ * }
248
+ * });
249
+ * ```
250
+ *
251
+ * @see {@link useTransactions.createTransaction | createTransaction()} for direct transfers without redemption flow
252
+ */
253
+ const redeem = useCallback(async (
254
+ redemptionId: string,
255
+ options?: { context?: DynamicContext; onStatusUpdate?: OnStatusUpdateFn }
256
+ ): Promise<RedemptionRedeemRequestResponseDTO> => {
257
+ if (!isInitialized || !sdk) {
258
+ throw new Error('SDK not initialized. Call initialize() first.');
259
+ }
260
+ if (!isAuthenticated) {
261
+ throw new Error('SDK not authenticated. redeem requires authentication.');
262
+ }
263
+
264
+ try {
265
+ const result = await sdk.redemptions.redeem(redemptionId, options?.context);
266
+
267
+ const txToken = result.senderTransaction?.actionable?.authToken;
268
+ if (txToken && isSignerAvailable) {
269
+ try {
270
+ const signingResult: SubmissionResult = await signAndSubmitTransactionWithJWT(txToken, options?.onStatusUpdate);
271
+ if (signingResult.success) {
272
+ // Return enhanced result with signing information
273
+ return {
274
+ ...result,
275
+ transactionHash: signingResult.transactionHash,
276
+ // signature: signingResult.signature,
277
+ isSigned: true,
278
+ signedAt: new Date().toISOString()
279
+ } as RedemptionRedeemRequestResponseDTO;
280
+ } else {
281
+ console.error('Transaction signing failed:', signingResult.error);
282
+ throw new Error(signingResult.error || 'Transaction signing failed');
283
+ }
284
+ } catch (signingError) {
285
+ console.error('Blockchain signing error:', signingError);
286
+ throw new Error(`Transaction signing failed: ${signingError}`);
287
+ }
288
+ } else if (txToken && !isSignerAvailable) {
289
+ console.warn('Transaction requires signature but signer is not available');
290
+ throw new Error('Transaction requires signature but blockchain signer is not initialized');
291
+ }
292
+
293
+ // Return original result if no signing was required
294
+ return result;
295
+ } catch (error) {
296
+ console.error('Failed to redeem redemption:', error);
297
+ throw error;
298
+ }
299
+ }, [sdk, isInitialized, isAuthenticated, signAndSubmitTransactionWithJWT, isSignerAvailable]);
300
+
301
+ // Admin methods
302
+
303
+ /**
304
+ * Admin: Get all redemption redeems with filters and include relations
305
+ *
306
+ * Retrieves all redemption redeems across the platform with filtering.
307
+ *
308
+ * @param filters - Optional filters for user, redemption, date range, and pagination
309
+ * @param filters.userId - Filter by user ID
310
+ * @param filters.redemptionId - Filter by redemption offer ID
311
+ * @param filters.businessId - Filter by business ID (admin only)
312
+ * @param filters.status - Filter by processing status (PENDING, PROCESSING, COMPLETED, FAILED)
313
+ * @param filters.dateFrom - Filter redeems created on or after this date (ISO 8601 format)
314
+ * @param filters.dateTo - Filter redeems created on or before this date (ISO 8601 format)
315
+ * @param filters.page - Page number (1-indexed)
316
+ * @param filters.limit - Items per page
317
+ * @param filters.sortBy - Field to sort by
318
+ * @param filters.sortOrder - Sort direction
319
+ * @param include - Relations to include: 'redemption', 'user', 'business'
320
+ * @returns Promise resolving to paginated redemption redeems
321
+ *
322
+ * @example
323
+ * ```typescript
324
+ * // Get all redeems with user details
325
+ * const { data: redeems } = await getRedemptionRedeems({}, ['user', 'redemption']);
326
+ *
327
+ * // Filter by specific user
328
+ * const { data: userRedeems } = await getRedemptionRedeems(
329
+ * { userId: 'user-123' },
330
+ * ['redemption']
331
+ * );
332
+ *
333
+ * // Filter by date range
334
+ * const { data } = await getRedemptionRedeems({
335
+ * dateFrom: '2026-01-01',
336
+ * dateTo: '2026-03-31'
337
+ * }, ['user', 'redemption']);
338
+ * ```
339
+ */
340
+ const getRedemptionRedeems = useCallback(async (
341
+ filters?: RedemptionRedeemQueryParams,
342
+ include?: RedemptionRedeemIncludeRelation[]
343
+ ): Promise<PaginatedResponseDTO<RedemptionRedeemDTO>> => {
344
+ if (!isInitialized || !sdk) {
345
+ throw new Error('SDK not initialized. Call initialize() first.');
346
+ }
347
+
348
+ try {
349
+ const result = await sdk.redemptions.getRedemptionRedeems(filters, include);
350
+ return result;
351
+ } catch (error) {
352
+ console.error('Failed to fetch redemption redeems:', error);
353
+ throw error;
354
+ }
355
+ }, [sdk, isInitialized]);
356
+
357
+ const createRedemption = useCallback(async (redemptionData: RedemptionCreateRequestDTO): Promise<RedemptionDTO> => {
358
+ if (!isInitialized || !sdk) {
359
+ throw new Error('SDK not initialized. Call initialize() first.');
360
+ }
361
+
362
+ try {
363
+ const result = await sdk.redemptions.createRedemption(redemptionData);
364
+ return result;
365
+ } catch (error) {
366
+ console.error('Failed to create redemption offer:', error);
367
+ throw error;
368
+ }
369
+ }, [sdk, isInitialized]);
370
+
371
+ const getRedemptionTypes = useCallback(async (): Promise<PaginatedResponseDTO<RedemptionTypeDTO>> => {
372
+ if (!isInitialized || !sdk) {
373
+ throw new Error('SDK not initialized. Call initialize() first.');
374
+ }
375
+
376
+ try {
377
+ const result = await sdk.redemptions.getRedemptionTypes();
378
+ return result;
379
+ } catch (error) {
380
+ console.error('Failed to fetch redemption types:', error);
381
+ throw error;
382
+ }
383
+ }, [sdk, isInitialized]);
384
+
385
+ const updateRedemption = useCallback(async (redemptionId: string, redemptionData: RedemptionCreateRequestDTO): Promise<RedemptionDTO> => {
386
+ if (!isInitialized || !sdk) {
387
+ throw new Error('SDK not initialized. Call initialize() first.');
388
+ }
389
+
390
+ try {
391
+ const result = await sdk.redemptions.updateRedemption(redemptionId, redemptionData);
392
+ return result;
393
+ } catch (error) {
394
+ console.error('Failed to update redemption:', error);
395
+ throw error;
396
+ }
397
+ }, [sdk, isInitialized]);
398
+
399
+ const toggleRedemptionStatus = useCallback(async (redemptionId: string): Promise<RedemptionDTO> => {
400
+ if (!isInitialized || !sdk) {
401
+ throw new Error('SDK not initialized. Call initialize() first.');
402
+ }
403
+
404
+ try {
405
+ const result = await sdk.redemptions.toggleRedemptionStatus(redemptionId);
406
+ return result;
407
+ } catch (error) {
408
+ console.error('Failed to toggle redemption status:', error);
409
+ throw error;
410
+ }
411
+ }, [sdk, isInitialized]);
412
+
413
+ /**
414
+ * Get a specific redemption by ID
415
+ *
416
+ * Retrieves detailed information about a specific redemption offer.
417
+ *
418
+ * @param id - Redemption UUID
419
+ * @param include - Optional relations to include: 'redeemCount'
420
+ * @returns Promise resolving to the redemption
421
+ *
422
+ * @example Basic usage
423
+ * ```typescript
424
+ * const redemption = await getRedemptionById('7baf4d39-0bd6-45ca-9c66-8c0d655767c3');
425
+ * console.log(redemption.name);
426
+ * ```
427
+ *
428
+ * @example With redeem count
429
+ * ```typescript
430
+ * const redemption = await getRedemptionById('redemption-123', ['redeemCount']);
431
+ * console.log(`${redemption.name} has been redeemed ${redemption.redeemCount} times`);
432
+ * ```
433
+ */
434
+ const getRedemptionById = useCallback(async (
435
+ id: string,
436
+ include?: RedemptionIncludeRelation[]
437
+ ): Promise<RedemptionDTO> => {
438
+ if (!isInitialized || !sdk) {
439
+ throw new Error('SDK not initialized. Call initialize() first.');
440
+ }
441
+
442
+ try {
443
+ const result = await sdk.redemptions.getRedemptionById(id, include);
444
+ return result;
445
+ } catch (error) {
446
+ console.error('Failed to fetch redemption by ID:', error);
447
+ throw error;
448
+ }
449
+ }, [sdk, isInitialized]);
450
+
451
+ return {
452
+ getRedemptions,
453
+ getRedemptionById,
454
+ getUserRedemptions,
455
+ redeem,
456
+ getRedemptionTypes,
457
+ getRedemptionRedeems,
458
+ createRedemption,
459
+ updateRedemption,
460
+ toggleRedemptionStatus,
461
+ isAvailable: isInitialized && !!sdk?.redemptions,
462
+ // Expose signing status for UI feedback
463
+ signingStatus: currentStatus,
464
+ signingStatusMessage: statusMessage,
465
+ };
466
+ };
467
+
468
468
  export type RedemptionHook = ReturnType<typeof useRedemptions>;