@explorins/pers-sdk-react-native 2.1.12 → 2.1.14-beta.0
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 +108 -14
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useCampaigns.d.ts +2 -0
- package/dist/hooks/useCampaigns.d.ts.map +1 -1
- package/dist/hooks/useCampaigns.js +58 -1
- package/dist/hooks/useRedemptions.d.ts +28 -7
- package/dist/hooks/useRedemptions.d.ts.map +1 -1
- package/dist/hooks/useRedemptions.js +91 -10
- package/dist/hooks/useTokenBalances.d.ts +8 -1
- package/dist/hooks/useTokenBalances.d.ts.map +1 -1
- package/dist/hooks/useTokenBalances.js +8 -1
- package/dist/hooks/useTransactionSigner.d.ts.map +1 -1
- package/dist/hooks/useTransactionSigner.js +38 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +461 -116
- package/dist/index.js.map +1 -1
- package/dist/providers/rn-dpop-provider.d.ts.map +1 -1
- package/dist/providers/rn-dpop-provider.js +48 -12
- package/package.json +3 -3
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useCampaigns.ts +62 -2
- package/src/hooks/useRedemptions.ts +119 -13
- package/src/hooks/useTokenBalances.ts +8 -1
- package/src/hooks/useTransactionSigner.ts +41 -10
- package/src/index.ts +1 -1
- package/src/providers/rn-dpop-provider.ts +57 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rn-dpop-provider.d.ts","sourceRoot":"","sources":["../../src/providers/rn-dpop-provider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"rn-dpop-provider.d.ts","sourceRoot":"","sources":["../../src/providers/rn-dpop-provider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAgD3E,qBAAa,uBAAwB,YAAW,kBAAkB;IAChE;;;;;OAKG;IACG,eAAe,CAAC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,CAAC;IA0C1E,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;IA0D9E,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAqB3D,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,eAAe;CAOxB"}
|
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
2
|
import { Buffer } from 'buffer';
|
|
3
|
-
//
|
|
4
|
-
let
|
|
5
|
-
|
|
3
|
+
// Lazy-loaded crypto module - initialized on first use to avoid native module timing issues
|
|
4
|
+
let _crypto = null;
|
|
5
|
+
let _cryptoInitialized = false;
|
|
6
|
+
/**
|
|
7
|
+
* Lazily load the crypto module.
|
|
8
|
+
*
|
|
9
|
+
* Supports customer pre-initialization via global.__QuickCrypto for environments
|
|
10
|
+
* where native module timing is unpredictable (e.g., certain React Native setups).
|
|
11
|
+
*
|
|
12
|
+
* @returns The crypto module or null if unavailable
|
|
13
|
+
*/
|
|
14
|
+
function getCrypto() {
|
|
15
|
+
if (_cryptoInitialized) {
|
|
16
|
+
return _crypto;
|
|
17
|
+
}
|
|
18
|
+
_cryptoInitialized = true;
|
|
19
|
+
if (Platform.OS === 'web') {
|
|
20
|
+
// On Web, we shouldn't be using this provider (Core SDK has WebDPoPProvider)
|
|
21
|
+
_crypto = {
|
|
22
|
+
generateKeyPair: () => { throw new Error('ReactNativeDPoPProvider not supported on Web'); },
|
|
23
|
+
randomUUID: () => globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`
|
|
24
|
+
};
|
|
25
|
+
return _crypto;
|
|
26
|
+
}
|
|
27
|
+
// Check for customer pre-initialized global (for native module timing issues)
|
|
28
|
+
if (typeof global !== 'undefined' && global.__QuickCrypto) {
|
|
29
|
+
_crypto = global.__QuickCrypto;
|
|
30
|
+
return _crypto;
|
|
31
|
+
}
|
|
32
|
+
// Standard lazy load
|
|
6
33
|
try {
|
|
7
|
-
|
|
34
|
+
_crypto = require('react-native-quick-crypto').default || require('react-native-quick-crypto');
|
|
8
35
|
}
|
|
9
36
|
catch (e) {
|
|
10
|
-
console.warn('ReactNativeDPoPProvider: Failed to load react-native-quick-crypto', e);
|
|
37
|
+
console.warn('ReactNativeDPoPProvider: Failed to load react-native-quick-crypto.', e);
|
|
38
|
+
console.warn('Tip: If you encounter timing issues, pre-initialize in index.js: global.__QuickCrypto = require("react-native-quick-crypto")');
|
|
39
|
+
_crypto = null;
|
|
11
40
|
}
|
|
12
|
-
|
|
13
|
-
else {
|
|
14
|
-
// on Web, we shouldn't be using this provider anyway (Core SDK has WebDPoPProvider)
|
|
15
|
-
crypto = {
|
|
16
|
-
generateKeyPair: () => { throw new Error('ReactNativeDPoPProvider not supported on Web'); }
|
|
17
|
-
};
|
|
41
|
+
return _crypto;
|
|
18
42
|
}
|
|
19
43
|
export class ReactNativeDPoPProvider {
|
|
20
44
|
/**
|
|
@@ -24,6 +48,10 @@ export class ReactNativeDPoPProvider {
|
|
|
24
48
|
* Falls back to Node.js crypto API on iOS if needed.
|
|
25
49
|
*/
|
|
26
50
|
async generateKeyPair(options) {
|
|
51
|
+
const crypto = getCrypto();
|
|
52
|
+
if (!crypto) {
|
|
53
|
+
throw new Error('Crypto module not available. Install react-native-quick-crypto or pre-initialize global.__QuickCrypto');
|
|
54
|
+
}
|
|
27
55
|
// Try WebCrypto API first (works on both iOS and Android)
|
|
28
56
|
if (crypto.subtle) {
|
|
29
57
|
try {
|
|
@@ -57,6 +85,10 @@ export class ReactNativeDPoPProvider {
|
|
|
57
85
|
});
|
|
58
86
|
}
|
|
59
87
|
async signProof(payload, keyPair) {
|
|
88
|
+
const crypto = getCrypto();
|
|
89
|
+
if (!crypto) {
|
|
90
|
+
throw new Error('Crypto module not available for signing');
|
|
91
|
+
}
|
|
60
92
|
// 1. Construct Header
|
|
61
93
|
const header = {
|
|
62
94
|
typ: 'dpop+jwt',
|
|
@@ -67,7 +99,7 @@ export class ReactNativeDPoPProvider {
|
|
|
67
99
|
const finalPayload = {
|
|
68
100
|
...payload,
|
|
69
101
|
iat: payload.iat || Math.floor(Date.now() / 1000),
|
|
70
|
-
jti: payload.jti || crypto.randomUUID()
|
|
102
|
+
jti: payload.jti || (crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`)
|
|
71
103
|
};
|
|
72
104
|
// 3. Encode
|
|
73
105
|
const b64Header = this.base64Url(JSON.stringify(header));
|
|
@@ -93,6 +125,10 @@ export class ReactNativeDPoPProvider {
|
|
|
93
125
|
return `${signingInput}.${this.base64UrlBuffer(signature)}`;
|
|
94
126
|
}
|
|
95
127
|
async hashAccessToken(accessToken) {
|
|
128
|
+
const crypto = getCrypto();
|
|
129
|
+
if (!crypto) {
|
|
130
|
+
throw new Error('Crypto module not available for hashing');
|
|
131
|
+
}
|
|
96
132
|
// Try WebCrypto first
|
|
97
133
|
if (crypto.subtle) {
|
|
98
134
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@explorins/pers-sdk-react-native",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.14-beta.0",
|
|
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,7 +37,7 @@
|
|
|
37
37
|
"author": "eXplorins",
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@explorins/pers-sdk": "^2.1.
|
|
40
|
+
"@explorins/pers-sdk": "^2.1.21",
|
|
41
41
|
"@explorins/pers-signer": "^1.0.33",
|
|
42
42
|
"buffer": "^6.0.3",
|
|
43
43
|
"ethers": "^6.15.0",
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"@ethersproject/scrypt": false
|
|
156
156
|
},
|
|
157
157
|
"devDependencies": {
|
|
158
|
-
"@explorins/pers-shared": "^2.1.
|
|
158
|
+
"@explorins/pers-shared": "^2.1.123",
|
|
159
159
|
"@react-native-async-storage/async-storage": "^3.0.2",
|
|
160
160
|
"@rollup/plugin-commonjs": "^25.0.7",
|
|
161
161
|
"@rollup/plugin-json": "^6.1.0",
|
package/src/hooks/index.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type {
|
|
|
38
38
|
} from './useTokenBalances';
|
|
39
39
|
|
|
40
40
|
// Export campaign-related types
|
|
41
|
-
export type { CampaignClaimFilters, CampaignHook } from './useCampaigns';
|
|
41
|
+
export type { CampaignClaimFilters, CampaignHook, GetCampaignsOptions } from './useCampaigns';
|
|
42
42
|
|
|
43
43
|
// Export redemption-related types
|
|
44
44
|
export type { RedemptionRedeemFilters, RedemptionHook } from './useRedemptions';
|
|
@@ -8,7 +8,8 @@ import type {
|
|
|
8
8
|
PaginatedResponseDTO,
|
|
9
9
|
CampaignClaimIncludeRelation,
|
|
10
10
|
CampaignIncludeRelation,
|
|
11
|
-
SortOrder
|
|
11
|
+
SortOrder,
|
|
12
|
+
// DynamicContext
|
|
12
13
|
} from '@explorins/pers-shared';
|
|
13
14
|
import type { CampaignClaimFilters } from '@explorins/pers-sdk/campaign';
|
|
14
15
|
|
|
@@ -21,6 +22,8 @@ export type { CampaignClaimFilters } from '@explorins/pers-sdk/campaign';
|
|
|
21
22
|
export interface GetCampaignsOptions {
|
|
22
23
|
/** Filter by tag */
|
|
23
24
|
tag?: string;
|
|
25
|
+
/** Free text search across name and description (case-insensitive) */
|
|
26
|
+
search?: string;
|
|
24
27
|
/** Filter by business ID */
|
|
25
28
|
businessId?: string;
|
|
26
29
|
/** Page number (1-indexed) */
|
|
@@ -113,6 +116,63 @@ export const useCampaigns = () => {
|
|
|
113
116
|
}
|
|
114
117
|
}, [sdk, isInitialized]);
|
|
115
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Claim a campaign reward with optional dynamic context
|
|
121
|
+
*
|
|
122
|
+
* Triggers a campaign claim for the authenticated user. The campaign must be active
|
|
123
|
+
* and the user must meet all eligibility requirements. Supports dynamic context for
|
|
124
|
+
* personalized NFT generation when the campaign issues ERC721 tokens.
|
|
125
|
+
*
|
|
126
|
+
* @param request - Campaign claim request with optional context
|
|
127
|
+
* @param request.campaignId - ID of the campaign to claim (optional if using triggerSourceId)
|
|
128
|
+
* @param request.triggerSourceId - ID of trigger source (QR, NFC, etc.)
|
|
129
|
+
* @param request.context - Dynamic context for NFT personalization
|
|
130
|
+
* @param request.metadata - Additional metadata for the claim
|
|
131
|
+
* @param request.latitude - User's latitude for geo-validated claims
|
|
132
|
+
* @param request.longitude - User's longitude for geo-validated claims
|
|
133
|
+
* @returns Promise resolving to campaign claim result
|
|
134
|
+
*
|
|
135
|
+
* @example Basic campaign claim
|
|
136
|
+
* ```typescript
|
|
137
|
+
* const claim = await claimCampaign({
|
|
138
|
+
* triggerSourceId: 'qr-code-123',
|
|
139
|
+
* });
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* @example Campaign claim with dynamic context (personalized NFT)
|
|
143
|
+
* ```typescript
|
|
144
|
+
* import { DynamicContext } from '@explorins/pers-sdk-react-native';
|
|
145
|
+
*
|
|
146
|
+
* const context: DynamicContext = {
|
|
147
|
+
* // Validity - token valid for 30 days
|
|
148
|
+
* validityDate: new Date().toISOString(),
|
|
149
|
+
* validityDuration: 30,
|
|
150
|
+
*
|
|
151
|
+
* // AI prompt context - available as {{context.xxx}} in prompts
|
|
152
|
+
* visitorName: 'John Doe',
|
|
153
|
+
* visitLocation: 'Grand Hotel Lobby',
|
|
154
|
+
* membershipTier: 'Gold',
|
|
155
|
+
* };
|
|
156
|
+
*
|
|
157
|
+
* const claim = await claimCampaign({
|
|
158
|
+
* triggerSourceId: 'hotel-checkin-qr',
|
|
159
|
+
* context,
|
|
160
|
+
* });
|
|
161
|
+
* ```
|
|
162
|
+
*
|
|
163
|
+
* @example Geo-validated claim with context
|
|
164
|
+
* ```typescript
|
|
165
|
+
* const claim = await claimCampaign({
|
|
166
|
+
* triggerSourceId: 'museum-entrance-nfc',
|
|
167
|
+
* latitude: 48.8606,
|
|
168
|
+
* longitude: 2.3376,
|
|
169
|
+
* context: {
|
|
170
|
+
* visitorType: 'Adult',
|
|
171
|
+
* ticketCategory: 'General Admission',
|
|
172
|
+
* }
|
|
173
|
+
* });
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
116
176
|
const claimCampaign = useCallback(async (request: CampaignClaimRequestDTO): Promise<CampaignClaimDTO | null> => {
|
|
117
177
|
if (!isInitialized || !sdk) {
|
|
118
178
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
@@ -141,7 +201,7 @@ export const useCampaigns = () => {
|
|
|
141
201
|
* // Get user's claims with campaign details
|
|
142
202
|
* const { data: claims } = await getUserClaims({ include: ['campaign', 'business'] });
|
|
143
203
|
* claims.forEach(claim => {
|
|
144
|
-
* console.log('Campaign:', claim.included?.campaign?.
|
|
204
|
+
* console.log('Campaign:', claim.included?.campaign?.name);
|
|
145
205
|
* });
|
|
146
206
|
*
|
|
147
207
|
* // With pagination
|
|
@@ -8,13 +8,34 @@ import type {
|
|
|
8
8
|
RedemptionRedeemRequestResponseDTO,
|
|
9
9
|
RedemptionTypeDTO,
|
|
10
10
|
PaginatedResponseDTO,
|
|
11
|
-
RedemptionRedeemIncludeRelation
|
|
11
|
+
RedemptionRedeemIncludeRelation,
|
|
12
|
+
ProcessRecordStatus,
|
|
13
|
+
DynamicContext,
|
|
14
|
+
SortOrder
|
|
12
15
|
} from '@explorins/pers-shared';
|
|
13
|
-
import type { RedemptionRedeemFilters } from '@explorins/pers-sdk/redemption';
|
|
16
|
+
import type { RedemptionRedeemFilters, RedemptionFilterOptions } from '@explorins/pers-sdk/redemption';
|
|
14
17
|
import type { PaginationOptions } from '@explorins/pers-sdk';
|
|
15
18
|
|
|
16
19
|
// Re-export for consumers
|
|
17
|
-
export type { RedemptionRedeemFilters } from '@explorins/pers-sdk/redemption';
|
|
20
|
+
export type { RedemptionRedeemFilters, RedemptionFilterOptions } from '@explorins/pers-sdk/redemption';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options for fetching redemptions with filtering and pagination
|
|
24
|
+
*/
|
|
25
|
+
export interface GetRedemptionsOptions {
|
|
26
|
+
/** Filter by active status (Admin only - regular users always see active offers) */
|
|
27
|
+
active?: boolean;
|
|
28
|
+
/** Free text search across name, description, tags (case-insensitive) */
|
|
29
|
+
search?: string;
|
|
30
|
+
/** Page number (1-indexed) */
|
|
31
|
+
page?: number;
|
|
32
|
+
/** Results per page */
|
|
33
|
+
limit?: number;
|
|
34
|
+
/** Field to sort by */
|
|
35
|
+
sortBy?: 'name' | 'createdAt';
|
|
36
|
+
/** Sort direction */
|
|
37
|
+
sortOrder?: SortOrder;
|
|
38
|
+
}
|
|
18
39
|
|
|
19
40
|
export const useRedemptions = () => {
|
|
20
41
|
const { sdk, isInitialized, isAuthenticated } = usePersSDK();
|
|
@@ -27,8 +48,37 @@ export const useRedemptions = () => {
|
|
|
27
48
|
* **Administrators:** Can see all redemptions and filter by active/inactive status
|
|
28
49
|
*
|
|
29
50
|
* The backend enforces permission-based filtering automatically.
|
|
51
|
+
*
|
|
52
|
+
* @param options - Filter and pagination options
|
|
53
|
+
* @param options.active - Filter by active status (Admin only)
|
|
54
|
+
* @param options.search - Free text search across name, description, tags
|
|
55
|
+
* @param options.page - Page number (1-indexed)
|
|
56
|
+
* @param options.limit - Results per page
|
|
57
|
+
* @param options.sortBy - Field to sort by ('name' | 'createdAt')
|
|
58
|
+
* @param options.sortOrder - Sort direction ('ASC' | 'DESC')
|
|
59
|
+
*
|
|
60
|
+
* @example Basic usage
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const { data: offers } = await getRedemptions();
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @example Search redemptions
|
|
66
|
+
* ```typescript
|
|
67
|
+
* const { data: coffeeOffers } = await getRedemptions({ search: 'coffee' });
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @example With pagination and sorting
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const { data, pagination } = await getRedemptions({
|
|
73
|
+
* search: 'discount',
|
|
74
|
+
* page: 1,
|
|
75
|
+
* limit: 10,
|
|
76
|
+
* sortBy: 'name',
|
|
77
|
+
* sortOrder: 'ASC'
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
30
80
|
*/
|
|
31
|
-
const getRedemptions = useCallback(async (options?:
|
|
81
|
+
const getRedemptions = useCallback(async (options?: GetRedemptionsOptions): Promise<PaginatedResponseDTO<RedemptionDTO>> => {
|
|
32
82
|
if (!isInitialized || !sdk) {
|
|
33
83
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
34
84
|
}
|
|
@@ -43,27 +93,33 @@ export const useRedemptions = () => {
|
|
|
43
93
|
}, [sdk, isInitialized]);
|
|
44
94
|
|
|
45
95
|
/**
|
|
46
|
-
* Get user's redemption history with optional pagination and include relations
|
|
96
|
+
* Get user's redemption history with optional filters, pagination and include relations
|
|
47
97
|
*
|
|
98
|
+
* @param filters - Optional filters (redemptionId, status)
|
|
48
99
|
* @param options - Pagination options (page, limit, sortBy, sortOrder)
|
|
49
|
-
* @param include - Relations to include: 'redemption', 'user', 'business'
|
|
100
|
+
* @param include - Relations to include: 'redemption', 'user', 'business', 'transactions'
|
|
50
101
|
* @returns Promise resolving to paginated redemption history
|
|
51
102
|
*
|
|
52
103
|
* @example
|
|
53
104
|
* ```typescript
|
|
54
105
|
* // Get first page of user redemptions
|
|
55
|
-
* const { data: redeems, pagination } = await getUserRedemptions({ page: 1, limit: 10 });
|
|
106
|
+
* const { data: redeems, pagination } = await getUserRedemptions({}, { page: 1, limit: 10 });
|
|
56
107
|
* console.log(`Page ${pagination.page} of ${pagination.pages}`);
|
|
57
108
|
*
|
|
109
|
+
* // Get only completed redemptions
|
|
110
|
+
* import { ProcessRecordStatus } from '@explorins/pers-shared';
|
|
111
|
+
* const { data: completed } = await getUserRedemptions({ status: ProcessRecordStatus.COMPLETED });
|
|
112
|
+
*
|
|
58
113
|
* // Get user redemptions with full redemption details
|
|
59
|
-
* const { data: redeems } = await getUserRedemptions({}, ['redemption', 'business']);
|
|
114
|
+
* const { data: redeems } = await getUserRedemptions({}, {}, ['redemption', 'business']);
|
|
60
115
|
* redeems.forEach(redeem => {
|
|
61
|
-
* console.log('Redemption:', redeem.included?.redemption?.
|
|
116
|
+
* console.log('Redemption:', redeem.included?.redemption?.name);
|
|
62
117
|
* console.log('Business:', redeem.included?.business?.displayName);
|
|
63
118
|
* });
|
|
64
119
|
* ```
|
|
65
120
|
*/
|
|
66
121
|
const getUserRedemptions = useCallback(async (
|
|
122
|
+
filters?: { redemptionId?: string; status?: ProcessRecordStatus },
|
|
67
123
|
options?: PaginationOptions,
|
|
68
124
|
include?: RedemptionRedeemIncludeRelation[]
|
|
69
125
|
): Promise<PaginatedResponseDTO<RedemptionRedeemDTO>> => {
|
|
@@ -76,7 +132,7 @@ export const useRedemptions = () => {
|
|
|
76
132
|
}
|
|
77
133
|
|
|
78
134
|
try {
|
|
79
|
-
const result = await sdk.redemptions.getUserRedemptions(options, include);
|
|
135
|
+
const result = await sdk.redemptions.getUserRedemptions(filters, options, include);
|
|
80
136
|
return result;
|
|
81
137
|
} catch (error) {
|
|
82
138
|
console.error('Failed to fetch user redemptions:', error);
|
|
@@ -84,7 +140,57 @@ export const useRedemptions = () => {
|
|
|
84
140
|
}
|
|
85
141
|
}, [sdk, isInitialized, isAuthenticated]);
|
|
86
142
|
|
|
87
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Redeem a redemption offer with optional dynamic context
|
|
145
|
+
*
|
|
146
|
+
* Supports dynamic context for personalized NFT generation when the redemption
|
|
147
|
+
* issues ERC721 tokens. Context can include validity dates, AI prompt placeholders,
|
|
148
|
+
* and custom metadata.
|
|
149
|
+
*
|
|
150
|
+
* @param redemptionId - ID of the redemption to redeem
|
|
151
|
+
* @param options - Optional settings
|
|
152
|
+
* @param options.context - Dynamic context for NFT personalization (validity dates, AI prompts)
|
|
153
|
+
* @param options.onStatusUpdate - Callback for transaction signing status updates
|
|
154
|
+
* @returns Promise resolving to redemption result
|
|
155
|
+
*
|
|
156
|
+
* @example Basic redemption
|
|
157
|
+
* ```typescript
|
|
158
|
+
* const result = await redeem('redemption-123');
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* @example Redemption with dynamic context (personalized NFT)
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const result = await redeem('event-ticket-123', {
|
|
164
|
+
* context: {
|
|
165
|
+
* // Validity - token valid for event duration
|
|
166
|
+
* validityDate: new Date().toISOString(),
|
|
167
|
+
* validityDuration: 8, // 8 hours
|
|
168
|
+
*
|
|
169
|
+
* // AI prompt context - available as {{context.xxx}} in prompts
|
|
170
|
+
* attendeeName: 'John Doe',
|
|
171
|
+
* ticketTier: 'VIP',
|
|
172
|
+
* seatSection: 'A12',
|
|
173
|
+
* }
|
|
174
|
+
* });
|
|
175
|
+
* ```
|
|
176
|
+
*
|
|
177
|
+
* @example Hotel stay NFT with check-in/check-out dates
|
|
178
|
+
* ```typescript
|
|
179
|
+
* const result = await redeem('hotel-nft-456', {
|
|
180
|
+
* context: {
|
|
181
|
+
* validityDate: '2026-04-15T14:00:00Z', // Check-in
|
|
182
|
+
* validityEndDate: '2026-04-20T11:00:00Z', // Check-out
|
|
183
|
+
* guestName: 'Jane Smith',
|
|
184
|
+
* roomNumber: '305',
|
|
185
|
+
* roomType: 'Suite',
|
|
186
|
+
* }
|
|
187
|
+
* });
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
const redeem = useCallback(async (
|
|
191
|
+
redemptionId: string,
|
|
192
|
+
options?: { context?: DynamicContext; onStatusUpdate?: OnStatusUpdateFn }
|
|
193
|
+
): Promise<RedemptionRedeemRequestResponseDTO> => {
|
|
88
194
|
if (!isInitialized || !sdk) {
|
|
89
195
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
90
196
|
}
|
|
@@ -93,12 +199,12 @@ export const useRedemptions = () => {
|
|
|
93
199
|
}
|
|
94
200
|
|
|
95
201
|
try {
|
|
96
|
-
const result = await sdk.redemptions.redeem(redemptionId);
|
|
202
|
+
const result = await sdk.redemptions.redeem(redemptionId, options?.context);
|
|
97
203
|
|
|
98
204
|
const txToken = result.senderTransaction?.actionable?.authToken;
|
|
99
205
|
if (txToken && isSignerAvailable) {
|
|
100
206
|
try {
|
|
101
|
-
const signingResult: SubmissionResult = await signAndSubmitTransactionWithJWT(txToken, onStatusUpdate);
|
|
207
|
+
const signingResult: SubmissionResult = await signAndSubmitTransactionWithJWT(txToken, options?.onStatusUpdate);
|
|
102
208
|
if (signingResult.success) {
|
|
103
209
|
// Return enhanced result with signing information
|
|
104
210
|
return {
|
|
@@ -95,10 +95,17 @@ export interface UseTokenBalancesResult {
|
|
|
95
95
|
* **Basic Usage:**
|
|
96
96
|
* ```typescript
|
|
97
97
|
* import { useTokenBalances, useTokens, usePersSDK } from '@explorins/pers-sdk-react-native';
|
|
98
|
+
* import { useState, useEffect } from 'react';
|
|
98
99
|
*
|
|
99
100
|
* function TokenBalancesScreen() {
|
|
100
101
|
* const { user } = usePersSDK();
|
|
101
|
-
* const {
|
|
102
|
+
* const { getTokens } = useTokens();
|
|
103
|
+
* const [availableTokens, setAvailableTokens] = useState([]);
|
|
104
|
+
*
|
|
105
|
+
* // Load tokens on mount
|
|
106
|
+
* useEffect(() => {
|
|
107
|
+
* getTokens().then(result => setAvailableTokens(result.data));
|
|
108
|
+
* }, []);
|
|
102
109
|
*
|
|
103
110
|
* // App explicitly selects which wallet to use
|
|
104
111
|
* const walletAddress = user?.wallets?.[0]?.address;
|
|
@@ -19,14 +19,44 @@ export type { StatusUpdateData };
|
|
|
19
19
|
export type OnStatusUpdateFn = (status: SigningStatus, message: string, data?: StatusUpdateData) => void;
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
let
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
// Lazy-loaded signer SDK factory - initialized on first use to avoid native module timing issues
|
|
23
|
+
let _createPersSignerSDK: ((config: PersSignerConfig) => Promise<PersSignerSDK>) | null = null;
|
|
24
|
+
let _signerInitAttempted = false;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Lazily load the PERS Signer SDK.
|
|
28
|
+
*
|
|
29
|
+
* Supports customer pre-initialization via global.__PersSigner for environments
|
|
30
|
+
* where native module timing is unpredictable (e.g., certain React Native setups).
|
|
31
|
+
*
|
|
32
|
+
* @returns The createPersSignerSDK function or null if unavailable
|
|
33
|
+
*/
|
|
34
|
+
function getSignerSDKFactory(): ((config: PersSignerConfig) => Promise<PersSignerSDK>) | null {
|
|
35
|
+
if (_signerInitAttempted) {
|
|
36
|
+
// Already tried - return cached result (could be null if failed)
|
|
37
|
+
return _createPersSignerSDK;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_signerInitAttempted = true;
|
|
41
|
+
|
|
42
|
+
// Check for customer pre-initialized global (for native module timing issues)
|
|
43
|
+
if (typeof global !== 'undefined' && (global as any).__PersSigner?.createPersSignerSDK) {
|
|
44
|
+
_createPersSignerSDK = (global as any).__PersSigner.createPersSignerSDK;
|
|
45
|
+
return _createPersSignerSDK;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Standard lazy load
|
|
49
|
+
try {
|
|
50
|
+
const signerModule = require('@explorins/pers-signer/react-native');
|
|
51
|
+
_createPersSignerSDK = signerModule.createPersSignerSDK;
|
|
52
|
+
return _createPersSignerSDK;
|
|
53
|
+
} catch (error: unknown) {
|
|
54
|
+
console.warn('[useTransactionSigner] PERS Signer SDK not available:', (error as Error).message);
|
|
55
|
+
console.warn('[useTransactionSigner] Real blockchain signing will not be available');
|
|
56
|
+
console.warn('[useTransactionSigner] Tip: If timing issues, pre-initialize in index.js: global.__PersSigner = require("@explorins/pers-signer/react-native")');
|
|
57
|
+
_createPersSignerSDK = null; // Mark as unavailable
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
30
60
|
}
|
|
31
61
|
|
|
32
62
|
|
|
@@ -243,6 +273,7 @@ export const useTransactionSigner = (): TransactionSignerHook => {
|
|
|
243
273
|
// Auto-initialize signer SDK when PERS SDK is ready
|
|
244
274
|
useEffect(() => {
|
|
245
275
|
const initializeSignerSDK = async () => {
|
|
276
|
+
const createPersSignerSDK = getSignerSDKFactory();
|
|
246
277
|
if (!createPersSignerSDK) {
|
|
247
278
|
console.warn('[useTransactionSigner] Signer SDK not available');
|
|
248
279
|
return;
|
|
@@ -361,7 +392,7 @@ export const useTransactionSigner = (): TransactionSignerHook => {
|
|
|
361
392
|
if (!isSignerInitialized || !signerSDKRef.current) {
|
|
362
393
|
throw new Error('Transaction signer not initialized');
|
|
363
394
|
}
|
|
364
|
-
if (!
|
|
395
|
+
if (!getSignerSDKFactory()) {
|
|
365
396
|
throw new Error('PERS Signer SDK not available. Blockchain signing is not supported.');
|
|
366
397
|
}
|
|
367
398
|
|
|
@@ -514,7 +545,7 @@ export const useTransactionSigner = (): TransactionSignerHook => {
|
|
|
514
545
|
* );
|
|
515
546
|
* ```
|
|
516
547
|
*/
|
|
517
|
-
isSignerAvailable: isSignerInitialized && !!
|
|
548
|
+
isSignerAvailable: isSignerInitialized && !!getSignerSDKFactory(),
|
|
518
549
|
|
|
519
550
|
/**
|
|
520
551
|
* Current signing status for UI feedback
|
package/src/index.ts
CHANGED
|
@@ -284,7 +284,7 @@ export type { EventsHook, PersEvent, EventHandler, EventFilter, Unsubscribe } fr
|
|
|
284
284
|
export type { TokenBalanceWithToken, UseTokenBalancesOptions, UseTokenBalancesResult } from './hooks';
|
|
285
285
|
|
|
286
286
|
// Re-export campaign types for convenience
|
|
287
|
-
export type { CampaignClaimFilters, CampaignHook } from './hooks';
|
|
287
|
+
export type { CampaignClaimFilters, CampaignHook, GetCampaignsOptions } from './hooks';
|
|
288
288
|
|
|
289
289
|
// Re-export redemption types for convenience
|
|
290
290
|
export type { RedemptionRedeemFilters, RedemptionHook } from './hooks';
|
|
@@ -2,19 +2,50 @@ import { Platform } from 'react-native';
|
|
|
2
2
|
import { Buffer } from 'buffer';
|
|
3
3
|
import { DPoPCryptoProvider, DPoPKeyPair } from '@explorins/pers-sdk/core';
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
let
|
|
7
|
-
|
|
5
|
+
// Lazy-loaded crypto module - initialized on first use to avoid native module timing issues
|
|
6
|
+
let _crypto: any = null;
|
|
7
|
+
let _cryptoInitialized = false;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Lazily load the crypto module.
|
|
11
|
+
*
|
|
12
|
+
* Supports customer pre-initialization via global.__QuickCrypto for environments
|
|
13
|
+
* where native module timing is unpredictable (e.g., certain React Native setups).
|
|
14
|
+
*
|
|
15
|
+
* @returns The crypto module or null if unavailable
|
|
16
|
+
*/
|
|
17
|
+
function getCrypto(): any {
|
|
18
|
+
if (_cryptoInitialized) {
|
|
19
|
+
return _crypto;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_cryptoInitialized = true;
|
|
23
|
+
|
|
24
|
+
if (Platform.OS === 'web') {
|
|
25
|
+
// On Web, we shouldn't be using this provider (Core SDK has WebDPoPProvider)
|
|
26
|
+
_crypto = {
|
|
27
|
+
generateKeyPair: () => { throw new Error('ReactNativeDPoPProvider not supported on Web'); },
|
|
28
|
+
randomUUID: () => globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`
|
|
29
|
+
};
|
|
30
|
+
return _crypto;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check for customer pre-initialized global (for native module timing issues)
|
|
34
|
+
if (typeof global !== 'undefined' && (global as any).__QuickCrypto) {
|
|
35
|
+
_crypto = (global as any).__QuickCrypto;
|
|
36
|
+
return _crypto;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Standard lazy load
|
|
8
40
|
try {
|
|
9
|
-
|
|
41
|
+
_crypto = require('react-native-quick-crypto').default || require('react-native-quick-crypto');
|
|
10
42
|
} catch (e) {
|
|
11
|
-
console.warn('ReactNativeDPoPProvider: Failed to load react-native-quick-crypto', e);
|
|
43
|
+
console.warn('ReactNativeDPoPProvider: Failed to load react-native-quick-crypto.', e);
|
|
44
|
+
console.warn('Tip: If you encounter timing issues, pre-initialize in index.js: global.__QuickCrypto = require("react-native-quick-crypto")');
|
|
45
|
+
_crypto = null;
|
|
12
46
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
crypto = {
|
|
16
|
-
generateKeyPair: () => { throw new Error('ReactNativeDPoPProvider not supported on Web'); }
|
|
17
|
-
};
|
|
47
|
+
|
|
48
|
+
return _crypto;
|
|
18
49
|
}
|
|
19
50
|
|
|
20
51
|
export class ReactNativeDPoPProvider implements DPoPCryptoProvider {
|
|
@@ -25,6 +56,11 @@ export class ReactNativeDPoPProvider implements DPoPCryptoProvider {
|
|
|
25
56
|
* Falls back to Node.js crypto API on iOS if needed.
|
|
26
57
|
*/
|
|
27
58
|
async generateKeyPair(options?: { extractable?: boolean }): Promise<DPoPKeyPair> {
|
|
59
|
+
const crypto = getCrypto();
|
|
60
|
+
if (!crypto) {
|
|
61
|
+
throw new Error('Crypto module not available. Install react-native-quick-crypto or pre-initialize global.__QuickCrypto');
|
|
62
|
+
}
|
|
63
|
+
|
|
28
64
|
// Try WebCrypto API first (works on both iOS and Android)
|
|
29
65
|
if (crypto.subtle) {
|
|
30
66
|
try {
|
|
@@ -62,6 +98,11 @@ export class ReactNativeDPoPProvider implements DPoPCryptoProvider {
|
|
|
62
98
|
}
|
|
63
99
|
|
|
64
100
|
async signProof(payload: Record<string, any>, keyPair: DPoPKeyPair): Promise<string> {
|
|
101
|
+
const crypto = getCrypto();
|
|
102
|
+
if (!crypto) {
|
|
103
|
+
throw new Error('Crypto module not available for signing');
|
|
104
|
+
}
|
|
105
|
+
|
|
65
106
|
// 1. Construct Header
|
|
66
107
|
const header = {
|
|
67
108
|
typ: 'dpop+jwt',
|
|
@@ -73,7 +114,7 @@ export class ReactNativeDPoPProvider implements DPoPCryptoProvider {
|
|
|
73
114
|
const finalPayload = {
|
|
74
115
|
...payload,
|
|
75
116
|
iat: payload.iat || Math.floor(Date.now() / 1000),
|
|
76
|
-
jti: payload.jti || crypto.randomUUID()
|
|
117
|
+
jti: payload.jti || (crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`)
|
|
77
118
|
};
|
|
78
119
|
|
|
79
120
|
// 3. Encode
|
|
@@ -115,6 +156,11 @@ export class ReactNativeDPoPProvider implements DPoPCryptoProvider {
|
|
|
115
156
|
}
|
|
116
157
|
|
|
117
158
|
async hashAccessToken(accessToken: string): Promise<string> {
|
|
159
|
+
const crypto = getCrypto();
|
|
160
|
+
if (!crypto) {
|
|
161
|
+
throw new Error('Crypto module not available for hashing');
|
|
162
|
+
}
|
|
163
|
+
|
|
118
164
|
// Try WebCrypto first
|
|
119
165
|
if (crypto.subtle) {
|
|
120
166
|
try {
|