@explorins/pers-sdk-react-native 2.3.6 → 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.
- package/LICENSE +21 -0
- package/README.md +1266 -1244
- package/dist/__tests__/PersSDKProvider.test.js +33 -0
- package/dist/hooks/useBusiness.d.ts +2 -2
- package/dist/hooks/useBusiness.d.ts.map +1 -1
- package/dist/hooks/useBusiness.js +2 -2
- package/dist/hooks/useIPFSUrl.d.ts +3 -3
- package/dist/hooks/useIPFSUrl.d.ts.map +1 -1
- package/dist/hooks/useIPFSUrl.js +7 -7
- package/dist/hooks/useTransactions.d.ts.map +1 -1
- package/dist/hooks/useTransactions.js +13 -5
- package/dist/hooks/useWeb3.js +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2412 -492
- package/dist/index.js.map +1 -1
- package/dist/providers/PersSDKProvider.d.ts +4 -0
- package/dist/providers/PersSDKProvider.d.ts.map +1 -1
- package/dist/providers/PersSDKProvider.js +14 -0
- package/dist/providers/react-native-http-client.d.ts +2 -0
- package/dist/providers/react-native-http-client.d.ts.map +1 -1
- package/dist/providers/react-native-http-client.js +5 -2
- package/dist/storage/rn-secure-storage.d.ts.map +1 -1
- package/dist/storage/rn-secure-storage.js +21 -8
- package/package.json +3 -3
- package/src/__tests__/PersSDKProvider.test.tsx +484 -420
- package/src/__tests__/setup.ts +40 -40
- package/src/hooks/index.ts +60 -60
- package/src/hooks/useAnalytics.ts +327 -327
- package/src/hooks/useAuth.ts +265 -265
- package/src/hooks/useBookings.ts +191 -191
- package/src/hooks/useBusiness.ts +251 -251
- package/src/hooks/useCampaigns.ts +500 -500
- package/src/hooks/useDonations.ts +66 -66
- package/src/hooks/useEvents.ts +372 -372
- package/src/hooks/useFiles.ts +155 -155
- package/src/hooks/useIPFSUrl.ts +192 -192
- package/src/hooks/usePurchases.ts +65 -65
- package/src/hooks/useRedemptions.ts +467 -467
- package/src/hooks/useTenants.ts +72 -72
- package/src/hooks/useTokenBalances.ts +385 -385
- package/src/hooks/useTokens.ts +313 -313
- package/src/hooks/useTransactionSigner.ts +611 -611
- package/src/hooks/useTransactions.ts +299 -290
- package/src/hooks/useTriggerSources.ts +301 -301
- package/src/hooks/useUserStatus.ts +61 -61
- package/src/hooks/useUsers.ts +167 -167
- package/src/hooks/useWeb3.ts +355 -355
- package/src/index.ts +777 -774
- package/src/polyfills/index.ts +160 -160
- package/src/polyfills/web3-polyfills.ts +97 -97
- package/src/providers/PersSDKProvider.tsx +393 -374
- package/src/providers/react-native-auth-provider.ts +110 -110
- package/src/providers/react-native-http-client.ts +150 -145
- package/src/providers/rn-dpop-provider.ts +190 -190
- package/src/storage/async-storage-token-storage.ts +185 -185
- package/src/storage/rn-secure-storage.ts +182 -170
- package/src/types/react-native-globals.d.ts +45 -45
- package/src/utils/index.ts +8 -8
- package/src/utils/platform-detection.ts +86 -86
- package/dist/index-CzLxqePZ.js +0 -7717
- package/dist/index-CzLxqePZ.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,1245 +1,1267 @@
|
|
|
1
|
-
# PERS SDK React Native
|
|
2
|
-
|
|
3
|
-
A comprehensive React Native SDK for the PERS (Phygital Experience Rewards System) platform, designed specifically for tourism loyalty applications with blockchain transaction signing capabilities.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- **Secure Authentication**: WebAuthn-based authentication with device biometrics
|
|
8
|
-
- **Blockchain Integration**: Complete transaction signing with PERS Signer SDK supporting EVM-compatible chains (Ethereum, Polygon, Camino, etc.)
|
|
9
|
-
- **Token Operations**: Balance checking, earning, redeeming, and transfers
|
|
10
|
-
- **Business Management**: Business profiles, campaigns, and operations
|
|
11
|
-
- **Campaign System**: Marketing campaigns, participation, and rewards
|
|
12
|
-
- **Redemption Engine**: Comprehensive reward redemption and fulfillment
|
|
13
|
-
- **Analytics**: Real-time analytics and transaction monitoring
|
|
14
|
-
- **Donations**: Charitable giving and donation management
|
|
15
|
-
- **User Management**: Profile management and user operations
|
|
16
|
-
- **Web3 Support**: EVM-compatible chain wallet operations and blockchain interactions
|
|
17
|
-
- **Secure Storage**: Hybrid storage strategy using **Keychain** (High Security) for secrets and **AsyncStorage** for public data.
|
|
18
|
-
- **DPoP Integration**: Native C++ bridge for high-performance ECDSA signing via `react-native-quick-crypto`.
|
|
19
|
-
- **React Native Optimized**: Full platform support with automatic polyfills.
|
|
20
|
-
|
|
21
|
-
## Installation
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
npm install @explorins/pers-sdk-react-native
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
### Required Peer Dependencies
|
|
28
|
-
|
|
29
|
-
You must install `async-storage` manually as it is a peer dependency:
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
npm install @react-native-async-storage/async-storage
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### Included Native Dependencies
|
|
36
|
-
|
|
37
|
-
The SDK automatically installs the following native libraries. **You must rebuild your native app** (e.g., `npx expo run:android` or `cd ios && pod install`) after installing the SDK to link these native modules:
|
|
38
|
-
|
|
39
|
-
- `react-native-quick-crypto` (High-performance Crypto)
|
|
40
|
-
- `react-native-keychain` (Secure Storage)
|
|
41
|
-
- `react-native-passkey` (WebAuthn/Passkeys)
|
|
42
|
-
- `react-native-get-random-values` (Crypto Primitives)
|
|
43
|
-
|
|
44
|
-
### Optional Dependencies
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
# For URL Polyfills (Recommended if not already included in your app)
|
|
48
|
-
npm install react-native-url-polyfill
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Metro Configuration (Required)
|
|
52
|
-
|
|
53
|
-
The PERS SDK uses subpath exports (`@explorins/pers-sdk/core`, etc.) which require Metro to be configured. Add to your `metro.config.js`:
|
|
54
|
-
|
|
55
|
-
```javascript
|
|
56
|
-
const { getDefaultConfig } = require('expo/metro-config');
|
|
57
|
-
// or for bare RN: const { getDefaultConfig } = require('@react-native/metro-config');
|
|
58
|
-
|
|
59
|
-
const config = getDefaultConfig(__dirname);
|
|
60
|
-
|
|
61
|
-
// REQUIRED: Enable package exports for PERS SDK subpath imports
|
|
62
|
-
config.resolver.unstable_enablePackageExports = true;
|
|
63
|
-
|
|
64
|
-
module.exports = config;
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## ⚠️ Android Architecture Requirements
|
|
70
|
-
|
|
71
|
-
**The SDK requires React Native New Architecture** starting from v2.1.x.
|
|
72
|
-
|
|
73
|
-
### Why?
|
|
74
|
-
|
|
75
|
-
`react-native-quick-crypto` v1.0+ (used for high-performance cryptographic operations) uses [Nitro Modules](https://github.com/margelo/nitro), which require New Architecture.
|
|
76
|
-
|
|
77
|
-
### Quick Check
|
|
78
|
-
|
|
79
|
-
If you see errors like:
|
|
80
|
-
```
|
|
81
|
-
NitroModulesProxy.createHybridObject(...) threw an unknown std::runtime_error
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Your app is running with **Old Architecture** (`newArchEnabled=false`).
|
|
85
|
-
|
|
86
|
-
### Solution 1: Enable New Architecture (Recommended)
|
|
87
|
-
|
|
88
|
-
React Native 0.76+ has stable New Architecture support. Enable it in `android/gradle.properties`:
|
|
89
|
-
|
|
90
|
-
```properties
|
|
91
|
-
# android/gradle.properties
|
|
92
|
-
|
|
93
|
-
# Use this property to enable support to the new architecture.
|
|
94
|
-
newArchEnabled=true
|
|
95
|
-
|
|
96
|
-
# Use this property to enable or disable the Hermes JS engine.
|
|
97
|
-
hermesEnabled=true
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Then clean and rebuild:
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
cd android && ./gradlew clean && cd ..
|
|
104
|
-
npx react-native start --reset-cache
|
|
105
|
-
npx react-native run-android
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
> **Reference**: See the [VQ Demo App's gradle.properties](https://github.com/explorins/vq-pers-integration-demo/blob/main/android/gradle.properties) for a working configuration with Expo SDK 54 and React Native 0.81.5.
|
|
109
|
-
|
|
110
|
-
### Solution 2: Downgrade quick-crypto (For Old Architecture)
|
|
111
|
-
|
|
112
|
-
If you must stay on Old Architecture, pin to v0.7.x:
|
|
113
|
-
|
|
114
|
-
```json
|
|
115
|
-
{
|
|
116
|
-
"resolutions": {
|
|
117
|
-
"react-native-quick-crypto": "0.7.17"
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Or for npm (in `package.json`):
|
|
123
|
-
|
|
124
|
-
```json
|
|
125
|
-
{
|
|
126
|
-
"overrides": {
|
|
127
|
-
"react-native-quick-crypto": "0.7.17"
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Compatibility Matrix
|
|
133
|
-
|
|
134
|
-
| RN Version | Architecture | quick-crypto | Status |
|
|
135
|
-
|------------|--------------|--------------|--------|
|
|
136
|
-
| 0.76+ | New (`newArchEnabled=true`) | 1.0.6+ | ✅ Recommended |
|
|
137
|
-
| 0.76+ | Old (`newArchEnabled=false`) | 0.7.17 | ⚠️ Requires downgrade |
|
|
138
|
-
| < 0.76 | Old | 0.7.x | ⚠️ Use older quick-crypto |
|
|
139
|
-
|
|
140
|
-
> **See [Android Compatibility FAQ](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/docs/ANDROID_COMPATIBILITY_FAQ.md)** for detailed troubleshooting.
|
|
141
|
-
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
## Critical Setup Requirement: Passkeys
|
|
145
|
-
|
|
146
|
-
To enable Passkey authentication (WebAuthn) on iOS and Android, you **must**:
|
|
147
|
-
|
|
148
|
-
1. **Register your app with PERS backend** - Contact **support@explorins.com** with:
|
|
149
|
-
- iOS: Bundle ID + Apple Team ID
|
|
150
|
-
- Android: Package Name + SHA-256 fingerprint
|
|
151
|
-
|
|
152
|
-
2. **Configure native files** - See [Passkey Setup Guide](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/REACT_NATIVE_PASSKEY_SETUP.md)
|
|
153
|
-
|
|
154
|
-
3. **For Expo**: Use development builds (`npx expo run:android`), not Expo Go
|
|
155
|
-
|
|
156
|
-
## Quick Start
|
|
157
|
-
|
|
158
|
-
### 1. Setup the Provider
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
import React from 'react';
|
|
162
|
-
import { PersSDKProvider } from '@explorins/pers-sdk-react-native';
|
|
163
|
-
|
|
164
|
-
export default function App() {
|
|
165
|
-
return (
|
|
166
|
-
<PersSDKProvider config={{
|
|
167
|
-
apiUrl: 'https://api.pers.ninja',
|
|
168
|
-
tenantId: 'your-tenant-id' // Optional
|
|
169
|
-
}}>
|
|
170
|
-
<NavigationContainer>
|
|
171
|
-
<YourAppContent />
|
|
172
|
-
</NavigationContainer>
|
|
173
|
-
</PersSDKProvider>
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### 2. Authentication & Token Operations
|
|
179
|
-
|
|
180
|
-
```typescript
|
|
181
|
-
import {
|
|
182
|
-
useAuth,
|
|
183
|
-
useTokens,
|
|
184
|
-
useRedemptions,
|
|
185
|
-
useTransactionSigner
|
|
186
|
-
} from '@explorins/pers-sdk-react-native';
|
|
187
|
-
|
|
188
|
-
function RewardScreen() {
|
|
189
|
-
const { user, isAuthenticated, login } = useAuth();
|
|
190
|
-
const { getTokens } = useTokens();
|
|
191
|
-
const { redeem } = useRedemptions();
|
|
192
|
-
const { signAndSubmitTransactionWithJWT, isSignerAvailable } = useTransactionSigner();
|
|
193
|
-
|
|
194
|
-
const handleLogin = async () => {
|
|
195
|
-
try {
|
|
196
|
-
await login('your-jwt-token');
|
|
197
|
-
console.log('Authenticated:', user?.identifier);
|
|
198
|
-
} catch (error) {
|
|
199
|
-
console.error('Login failed:', error);
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const handleLoadTokens = async () => {
|
|
204
|
-
try {
|
|
205
|
-
const tokens = await getTokens();
|
|
206
|
-
console.log('User tokens:', tokens);
|
|
207
|
-
} catch (error) {
|
|
208
|
-
console.error('Failed to load tokens:', error);
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const handleRedemption = async () => {
|
|
213
|
-
try {
|
|
214
|
-
// Step 1: Create redemption
|
|
215
|
-
const redemptionId = 'redemption-id-from-ui';
|
|
216
|
-
const result = await redeem(redemptionId);
|
|
217
|
-
|
|
218
|
-
// Note: The redeem method automatically handles blockchain signing if required
|
|
219
|
-
// and if the signer is available.
|
|
220
|
-
|
|
221
|
-
if (result.isSigned) {
|
|
222
|
-
console.log('Redemption completed with blockchain signature!');
|
|
223
|
-
console.log('Transaction Hash:', result.transactionHash);
|
|
224
|
-
}
|
|
225
|
-
} catch (error) {
|
|
226
|
-
console.error('Redemption failed:', error);
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
return (
|
|
231
|
-
<ScrollView style={styles.container}>
|
|
232
|
-
{!isAuthenticated ? (
|
|
233
|
-
<TouchableOpacity onPress={handleLogin} style={styles.loginButton}>
|
|
234
|
-
<Text style={styles.buttonText}>Login with PERS</Text>
|
|
235
|
-
</TouchableOpacity>
|
|
236
|
-
) : (
|
|
237
|
-
<View>
|
|
238
|
-
<Text style={styles.welcome}>Welcome, {user?.identifier}!</Text>
|
|
239
|
-
|
|
240
|
-
<TouchableOpacity onPress={handleLoadTokens} style={styles.button}>
|
|
241
|
-
<Text style={styles.buttonText}>Load Tokens</Text>
|
|
242
|
-
</TouchableOpacity>
|
|
243
|
-
|
|
244
|
-
<TouchableOpacity
|
|
245
|
-
onPress={handleRedemption}
|
|
246
|
-
disabled={!isSignerAvailable}
|
|
247
|
-
style={[styles.button, !isSignerAvailable && styles.disabled]}
|
|
248
|
-
>
|
|
249
|
-
<Text style={styles.buttonText}>
|
|
250
|
-
{isSignerAvailable ? 'Redeem Rewards' : 'Signer Loading...'}
|
|
251
|
-
</Text>
|
|
252
|
-
</TouchableOpacity>
|
|
253
|
-
</View>
|
|
254
|
-
)}
|
|
255
|
-
</ScrollView>
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
## Core Hooks
|
|
261
|
-
|
|
262
|
-
### Authentication & Users
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
// Authentication management
|
|
266
|
-
const {
|
|
267
|
-
isInitialized,
|
|
268
|
-
isAuthenticated,
|
|
269
|
-
user,
|
|
270
|
-
login,
|
|
271
|
-
loginWithRawData,
|
|
272
|
-
logout,
|
|
273
|
-
refreshUserData,
|
|
274
|
-
getCurrentUser,
|
|
275
|
-
checkIsAuthenticated,
|
|
276
|
-
refreshTokens,
|
|
277
|
-
clearAuth,
|
|
278
|
-
hasValidAuth
|
|
279
|
-
} = useAuth();
|
|
280
|
-
|
|
281
|
-
// User profile operations
|
|
282
|
-
const {
|
|
283
|
-
getCurrentUser,
|
|
284
|
-
updateCurrentUser,
|
|
285
|
-
getUserById,
|
|
286
|
-
getAllUsersPublic,
|
|
287
|
-
getAllUsers, // Admin
|
|
288
|
-
updateUser, // Admin
|
|
289
|
-
toggleUserStatus // Admin
|
|
290
|
-
} = useUsers();
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
### Token & Financial Operations
|
|
294
|
-
|
|
295
|
-
```typescript
|
|
296
|
-
// Token management
|
|
297
|
-
const {
|
|
298
|
-
getTokens,
|
|
299
|
-
getActiveCreditToken,
|
|
300
|
-
getRewardTokens,
|
|
301
|
-
getTokenTypes,
|
|
302
|
-
getStatusTokens,
|
|
303
|
-
getTokenByContract
|
|
304
|
-
} = useTokens();
|
|
305
|
-
|
|
306
|
-
// Token balance loading (NEW in v2.1.1)
|
|
307
|
-
const {
|
|
308
|
-
tokenBalances, // Array of balances with token metadata
|
|
309
|
-
isLoading, // Loading state
|
|
310
|
-
error, // Error state
|
|
311
|
-
refresh // Manual refresh function
|
|
312
|
-
} = useTokenBalances({
|
|
313
|
-
availableTokens, // From useTokens()
|
|
314
|
-
autoLoad: true, // Auto-load on mount
|
|
315
|
-
refreshInterval: 30000 // Optional: refresh every 30s
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Transaction history
|
|
319
|
-
const {
|
|
320
|
-
createTransaction,
|
|
321
|
-
getTransactionById,
|
|
322
|
-
getUserTransactionHistory, // With TransactionQueryOptions (includes pagination + filters + include)
|
|
323
|
-
getPaginatedTransactions, // Admin - single options object with include
|
|
324
|
-
exportTransactionsCSV, // Admin
|
|
325
|
-
signingStatus, // UI feedback during blockchain signing
|
|
326
|
-
signingStatusMessage
|
|
327
|
-
} = useTransactions();
|
|
328
|
-
|
|
329
|
-
// Blockchain transaction signing
|
|
330
|
-
const {
|
|
331
|
-
signAndSubmitTransactionWithJWT,
|
|
332
|
-
isSignerAvailable,
|
|
333
|
-
isSignerInitialized,
|
|
334
|
-
currentStatus,
|
|
335
|
-
statusMessage
|
|
336
|
-
} = useTransactionSigner();
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
### Business & Campaign Management
|
|
340
|
-
|
|
341
|
-
```typescript
|
|
342
|
-
// Business operations
|
|
343
|
-
const {
|
|
344
|
-
getActiveBusinesses,
|
|
345
|
-
getBusinessTypes,
|
|
346
|
-
getBusinesses,
|
|
347
|
-
getBusinessById,
|
|
348
|
-
getBusinessByAccount,
|
|
349
|
-
getBusinessesByType,
|
|
350
|
-
createBusiness, // Admin
|
|
351
|
-
updateBusiness, // Admin
|
|
352
|
-
toggleBusinessStatus // Admin
|
|
353
|
-
} = useBusiness();
|
|
354
|
-
|
|
355
|
-
// Campaign management
|
|
356
|
-
const {
|
|
357
|
-
getActiveCampaigns,
|
|
358
|
-
getCampaigns, // With options: { active?, businessId?, sortBy?, include?, page?, limit? }
|
|
359
|
-
getCampaignById,
|
|
360
|
-
claimCampaign,
|
|
361
|
-
getUserClaims, // With options: { page?, limit?, include? }
|
|
362
|
-
getCampaignTriggers,
|
|
363
|
-
getAllCampaigns, // Admin
|
|
364
|
-
getCampaignClaims, // Admin - with options object
|
|
365
|
-
getCampaignClaimsByUserId, // Admin
|
|
366
|
-
getCampaignClaimsByBusinessId // Admin
|
|
367
|
-
} = useCampaigns();
|
|
368
|
-
|
|
369
|
-
// Redemption system
|
|
370
|
-
const {
|
|
371
|
-
getActiveRedemptions,
|
|
372
|
-
getUserRedemptions,
|
|
373
|
-
redeem,
|
|
374
|
-
getRedemptionTypes,
|
|
375
|
-
signingStatus, // UI feedback during blockchain signing
|
|
376
|
-
signingStatusMessage,
|
|
377
|
-
createRedemption, // Admin
|
|
378
|
-
getAllRedemptions, // Admin
|
|
379
|
-
updateRedemption, // Admin
|
|
380
|
-
toggleRedemptionStatus // Admin
|
|
381
|
-
} = useRedemptions();
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
### Platform & Analytics
|
|
385
|
-
|
|
386
|
-
```typescript
|
|
387
|
-
// Purchase processing
|
|
388
|
-
const {
|
|
389
|
-
createPaymentIntent,
|
|
390
|
-
getActivePurchaseTokens,
|
|
391
|
-
getAllUserPurchases
|
|
392
|
-
} = usePurchases();
|
|
393
|
-
|
|
394
|
-
// Multi-tenant support
|
|
395
|
-
const {
|
|
396
|
-
getTenantInfo,
|
|
397
|
-
getClientConfig,
|
|
398
|
-
getLoginToken,
|
|
399
|
-
getAdmins
|
|
400
|
-
} = useTenants();
|
|
401
|
-
|
|
402
|
-
// Analytics & reporting
|
|
403
|
-
const {
|
|
404
|
-
getTransactionAnalytics
|
|
405
|
-
} = useAnalytics();
|
|
406
|
-
|
|
407
|
-
// Web3 & blockchain (wallet addresses from user.wallets)
|
|
408
|
-
const {
|
|
409
|
-
getTokenBalance,
|
|
410
|
-
getTokenMetadata,
|
|
411
|
-
getTokenCollection,
|
|
412
|
-
resolveIPFSUrl,
|
|
413
|
-
fetchAndProcessMetadata,
|
|
414
|
-
getChainDataById,
|
|
415
|
-
getExplorerUrl,
|
|
416
|
-
// Helper methods for token collections
|
|
417
|
-
extractTokenIds, // Extract tokenIds from TokenDTO metadata
|
|
418
|
-
getAccountOwnedTokensFromContract, // Recommended: Get owned tokens automatically
|
|
419
|
-
buildCollectionRequest // Build request for getTokenCollection
|
|
420
|
-
} = useWeb3();
|
|
421
|
-
|
|
422
|
-
// User status & achievements
|
|
423
|
-
const {
|
|
424
|
-
getUserStatusTypes,
|
|
425
|
-
getEarnedUserStatus,
|
|
426
|
-
createUserStatusType // Admin
|
|
427
|
-
} = useUserStatus();
|
|
428
|
-
|
|
429
|
-
// File management
|
|
430
|
-
const {
|
|
431
|
-
getSignedPutUrl,
|
|
432
|
-
getSignedGetUrl,
|
|
433
|
-
getSignedUrl,
|
|
434
|
-
optimizeMedia
|
|
435
|
-
} = useFiles();
|
|
436
|
-
|
|
437
|
-
// Donations
|
|
438
|
-
const {
|
|
439
|
-
getDonationTypes
|
|
440
|
-
} = useDonations();
|
|
441
|
-
|
|
442
|
-
// Event subscriptions (notifications, logging)
|
|
443
|
-
const {
|
|
444
|
-
subscribe, // Subscribe to SDK events
|
|
445
|
-
once, // One-time event listener
|
|
446
|
-
clear, // Clear all subscriptions
|
|
447
|
-
isAvailable, // Event system available
|
|
448
|
-
subscriberCount // Active subscriber count
|
|
449
|
-
} = useEvents();
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
##
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
###
|
|
479
|
-
|
|
480
|
-
```typescript
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
);
|
|
489
|
-
|
|
490
|
-
//
|
|
491
|
-
subscribe(
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
domain:
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
//
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
//
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
);
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
###
|
|
983
|
-
|
|
984
|
-
The
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
```
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
```
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
- **
|
|
1223
|
-
- **
|
|
1224
|
-
- **
|
|
1225
|
-
- **
|
|
1226
|
-
|
|
1227
|
-
---
|
|
1228
|
-
|
|
1229
|
-
##
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1
|
+
# PERS SDK React Native
|
|
2
|
+
|
|
3
|
+
A comprehensive React Native SDK for the PERS (Phygital Experience Rewards System) platform, designed specifically for tourism loyalty applications with blockchain transaction signing capabilities.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Secure Authentication**: WebAuthn-based authentication with device biometrics
|
|
8
|
+
- **Blockchain Integration**: Complete transaction signing with PERS Signer SDK supporting EVM-compatible chains (Ethereum, Polygon, Camino, etc.)
|
|
9
|
+
- **Token Operations**: Balance checking, earning, redeeming, and transfers
|
|
10
|
+
- **Business Management**: Business profiles, campaigns, and operations
|
|
11
|
+
- **Campaign System**: Marketing campaigns, participation, and rewards
|
|
12
|
+
- **Redemption Engine**: Comprehensive reward redemption and fulfillment
|
|
13
|
+
- **Analytics**: Real-time analytics and transaction monitoring
|
|
14
|
+
- **Donations**: Charitable giving and donation management
|
|
15
|
+
- **User Management**: Profile management and user operations
|
|
16
|
+
- **Web3 Support**: EVM-compatible chain wallet operations and blockchain interactions
|
|
17
|
+
- **Secure Storage**: Hybrid storage strategy using **Keychain** (High Security) for secrets and **AsyncStorage** for public data.
|
|
18
|
+
- **DPoP Integration**: Native C++ bridge for high-performance ECDSA signing via `react-native-quick-crypto`.
|
|
19
|
+
- **React Native Optimized**: Full platform support with automatic polyfills.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @explorins/pers-sdk-react-native
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Required Peer Dependencies
|
|
28
|
+
|
|
29
|
+
You must install `async-storage` manually as it is a peer dependency:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @react-native-async-storage/async-storage
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Included Native Dependencies
|
|
36
|
+
|
|
37
|
+
The SDK automatically installs the following native libraries. **You must rebuild your native app** (e.g., `npx expo run:android` or `cd ios && pod install`) after installing the SDK to link these native modules:
|
|
38
|
+
|
|
39
|
+
- `react-native-quick-crypto` (High-performance Crypto)
|
|
40
|
+
- `react-native-keychain` (Secure Storage)
|
|
41
|
+
- `react-native-passkey` (WebAuthn/Passkeys)
|
|
42
|
+
- `react-native-get-random-values` (Crypto Primitives)
|
|
43
|
+
|
|
44
|
+
### Optional Dependencies
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# For URL Polyfills (Recommended if not already included in your app)
|
|
48
|
+
npm install react-native-url-polyfill
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Metro Configuration (Required)
|
|
52
|
+
|
|
53
|
+
The PERS SDK uses subpath exports (`@explorins/pers-sdk/core`, etc.) which require Metro to be configured. Add to your `metro.config.js`:
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
const { getDefaultConfig } = require('expo/metro-config');
|
|
57
|
+
// or for bare RN: const { getDefaultConfig } = require('@react-native/metro-config');
|
|
58
|
+
|
|
59
|
+
const config = getDefaultConfig(__dirname);
|
|
60
|
+
|
|
61
|
+
// REQUIRED: Enable package exports for PERS SDK subpath imports
|
|
62
|
+
config.resolver.unstable_enablePackageExports = true;
|
|
63
|
+
|
|
64
|
+
module.exports = config;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## ⚠️ Android Architecture Requirements
|
|
70
|
+
|
|
71
|
+
**The SDK requires React Native New Architecture** starting from v2.1.x.
|
|
72
|
+
|
|
73
|
+
### Why?
|
|
74
|
+
|
|
75
|
+
`react-native-quick-crypto` v1.0+ (used for high-performance cryptographic operations) uses [Nitro Modules](https://github.com/margelo/nitro), which require New Architecture.
|
|
76
|
+
|
|
77
|
+
### Quick Check
|
|
78
|
+
|
|
79
|
+
If you see errors like:
|
|
80
|
+
```
|
|
81
|
+
NitroModulesProxy.createHybridObject(...) threw an unknown std::runtime_error
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Your app is running with **Old Architecture** (`newArchEnabled=false`).
|
|
85
|
+
|
|
86
|
+
### Solution 1: Enable New Architecture (Recommended)
|
|
87
|
+
|
|
88
|
+
React Native 0.76+ has stable New Architecture support. Enable it in `android/gradle.properties`:
|
|
89
|
+
|
|
90
|
+
```properties
|
|
91
|
+
# android/gradle.properties
|
|
92
|
+
|
|
93
|
+
# Use this property to enable support to the new architecture.
|
|
94
|
+
newArchEnabled=true
|
|
95
|
+
|
|
96
|
+
# Use this property to enable or disable the Hermes JS engine.
|
|
97
|
+
hermesEnabled=true
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Then clean and rebuild:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
cd android && ./gradlew clean && cd ..
|
|
104
|
+
npx react-native start --reset-cache
|
|
105
|
+
npx react-native run-android
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> **Reference**: See the [VQ Demo App's gradle.properties](https://github.com/explorins/vq-pers-integration-demo/blob/main/android/gradle.properties) for a working configuration with Expo SDK 54 and React Native 0.81.5.
|
|
109
|
+
|
|
110
|
+
### Solution 2: Downgrade quick-crypto (For Old Architecture)
|
|
111
|
+
|
|
112
|
+
If you must stay on Old Architecture, pin to v0.7.x:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"resolutions": {
|
|
117
|
+
"react-native-quick-crypto": "0.7.17"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Or for npm (in `package.json`):
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"overrides": {
|
|
127
|
+
"react-native-quick-crypto": "0.7.17"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Compatibility Matrix
|
|
133
|
+
|
|
134
|
+
| RN Version | Architecture | quick-crypto | Status |
|
|
135
|
+
|------------|--------------|--------------|--------|
|
|
136
|
+
| 0.76+ | New (`newArchEnabled=true`) | 1.0.6+ | ✅ Recommended |
|
|
137
|
+
| 0.76+ | Old (`newArchEnabled=false`) | 0.7.17 | ⚠️ Requires downgrade |
|
|
138
|
+
| < 0.76 | Old | 0.7.x | ⚠️ Use older quick-crypto |
|
|
139
|
+
|
|
140
|
+
> **See [Android Compatibility FAQ](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/docs/ANDROID_COMPATIBILITY_FAQ.md)** for detailed troubleshooting.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Critical Setup Requirement: Passkeys
|
|
145
|
+
|
|
146
|
+
To enable Passkey authentication (WebAuthn) on iOS and Android, you **must**:
|
|
147
|
+
|
|
148
|
+
1. **Register your app with PERS backend** - Contact **support@explorins.com** with:
|
|
149
|
+
- iOS: Bundle ID + Apple Team ID
|
|
150
|
+
- Android: Package Name + SHA-256 fingerprint
|
|
151
|
+
|
|
152
|
+
2. **Configure native files** - See [Passkey Setup Guide](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/REACT_NATIVE_PASSKEY_SETUP.md)
|
|
153
|
+
|
|
154
|
+
3. **For Expo**: Use development builds (`npx expo run:android`), not Expo Go
|
|
155
|
+
|
|
156
|
+
## Quick Start
|
|
157
|
+
|
|
158
|
+
### 1. Setup the Provider
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import React from 'react';
|
|
162
|
+
import { PersSDKProvider } from '@explorins/pers-sdk-react-native';
|
|
163
|
+
|
|
164
|
+
export default function App() {
|
|
165
|
+
return (
|
|
166
|
+
<PersSDKProvider config={{
|
|
167
|
+
apiUrl: 'https://api.pers.ninja',
|
|
168
|
+
tenantId: 'your-tenant-id' // Optional
|
|
169
|
+
}}>
|
|
170
|
+
<NavigationContainer>
|
|
171
|
+
<YourAppContent />
|
|
172
|
+
</NavigationContainer>
|
|
173
|
+
</PersSDKProvider>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 2. Authentication & Token Operations
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import {
|
|
182
|
+
useAuth,
|
|
183
|
+
useTokens,
|
|
184
|
+
useRedemptions,
|
|
185
|
+
useTransactionSigner
|
|
186
|
+
} from '@explorins/pers-sdk-react-native';
|
|
187
|
+
|
|
188
|
+
function RewardScreen() {
|
|
189
|
+
const { user, isAuthenticated, login } = useAuth();
|
|
190
|
+
const { getTokens } = useTokens();
|
|
191
|
+
const { redeem } = useRedemptions();
|
|
192
|
+
const { signAndSubmitTransactionWithJWT, isSignerAvailable } = useTransactionSigner();
|
|
193
|
+
|
|
194
|
+
const handleLogin = async () => {
|
|
195
|
+
try {
|
|
196
|
+
await login('your-jwt-token');
|
|
197
|
+
console.log('Authenticated:', user?.identifier);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('Login failed:', error);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const handleLoadTokens = async () => {
|
|
204
|
+
try {
|
|
205
|
+
const tokens = await getTokens();
|
|
206
|
+
console.log('User tokens:', tokens);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error('Failed to load tokens:', error);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const handleRedemption = async () => {
|
|
213
|
+
try {
|
|
214
|
+
// Step 1: Create redemption
|
|
215
|
+
const redemptionId = 'redemption-id-from-ui';
|
|
216
|
+
const result = await redeem(redemptionId);
|
|
217
|
+
|
|
218
|
+
// Note: The redeem method automatically handles blockchain signing if required
|
|
219
|
+
// and if the signer is available.
|
|
220
|
+
|
|
221
|
+
if (result.isSigned) {
|
|
222
|
+
console.log('Redemption completed with blockchain signature!');
|
|
223
|
+
console.log('Transaction Hash:', result.transactionHash);
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error('Redemption failed:', error);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<ScrollView style={styles.container}>
|
|
232
|
+
{!isAuthenticated ? (
|
|
233
|
+
<TouchableOpacity onPress={handleLogin} style={styles.loginButton}>
|
|
234
|
+
<Text style={styles.buttonText}>Login with PERS</Text>
|
|
235
|
+
</TouchableOpacity>
|
|
236
|
+
) : (
|
|
237
|
+
<View>
|
|
238
|
+
<Text style={styles.welcome}>Welcome, {user?.identifier}!</Text>
|
|
239
|
+
|
|
240
|
+
<TouchableOpacity onPress={handleLoadTokens} style={styles.button}>
|
|
241
|
+
<Text style={styles.buttonText}>Load Tokens</Text>
|
|
242
|
+
</TouchableOpacity>
|
|
243
|
+
|
|
244
|
+
<TouchableOpacity
|
|
245
|
+
onPress={handleRedemption}
|
|
246
|
+
disabled={!isSignerAvailable}
|
|
247
|
+
style={[styles.button, !isSignerAvailable && styles.disabled]}
|
|
248
|
+
>
|
|
249
|
+
<Text style={styles.buttonText}>
|
|
250
|
+
{isSignerAvailable ? 'Redeem Rewards' : 'Signer Loading...'}
|
|
251
|
+
</Text>
|
|
252
|
+
</TouchableOpacity>
|
|
253
|
+
</View>
|
|
254
|
+
)}
|
|
255
|
+
</ScrollView>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Core Hooks
|
|
261
|
+
|
|
262
|
+
### Authentication & Users
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// Authentication management
|
|
266
|
+
const {
|
|
267
|
+
isInitialized,
|
|
268
|
+
isAuthenticated,
|
|
269
|
+
user,
|
|
270
|
+
login,
|
|
271
|
+
loginWithRawData,
|
|
272
|
+
logout,
|
|
273
|
+
refreshUserData,
|
|
274
|
+
getCurrentUser,
|
|
275
|
+
checkIsAuthenticated,
|
|
276
|
+
refreshTokens,
|
|
277
|
+
clearAuth,
|
|
278
|
+
hasValidAuth
|
|
279
|
+
} = useAuth();
|
|
280
|
+
|
|
281
|
+
// User profile operations
|
|
282
|
+
const {
|
|
283
|
+
getCurrentUser,
|
|
284
|
+
updateCurrentUser,
|
|
285
|
+
getUserById,
|
|
286
|
+
getAllUsersPublic,
|
|
287
|
+
getAllUsers, // Admin
|
|
288
|
+
updateUser, // Admin
|
|
289
|
+
toggleUserStatus // Admin
|
|
290
|
+
} = useUsers();
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Token & Financial Operations
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// Token management
|
|
297
|
+
const {
|
|
298
|
+
getTokens,
|
|
299
|
+
getActiveCreditToken,
|
|
300
|
+
getRewardTokens,
|
|
301
|
+
getTokenTypes,
|
|
302
|
+
getStatusTokens,
|
|
303
|
+
getTokenByContract
|
|
304
|
+
} = useTokens();
|
|
305
|
+
|
|
306
|
+
// Token balance loading (NEW in v2.1.1)
|
|
307
|
+
const {
|
|
308
|
+
tokenBalances, // Array of balances with token metadata
|
|
309
|
+
isLoading, // Loading state
|
|
310
|
+
error, // Error state
|
|
311
|
+
refresh // Manual refresh function
|
|
312
|
+
} = useTokenBalances({
|
|
313
|
+
availableTokens, // From useTokens()
|
|
314
|
+
autoLoad: true, // Auto-load on mount
|
|
315
|
+
refreshInterval: 30000 // Optional: refresh every 30s
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Transaction history
|
|
319
|
+
const {
|
|
320
|
+
createTransaction,
|
|
321
|
+
getTransactionById,
|
|
322
|
+
getUserTransactionHistory, // With TransactionQueryOptions (includes pagination + filters + include)
|
|
323
|
+
getPaginatedTransactions, // Admin - single options object with include
|
|
324
|
+
exportTransactionsCSV, // Admin
|
|
325
|
+
signingStatus, // UI feedback during blockchain signing
|
|
326
|
+
signingStatusMessage
|
|
327
|
+
} = useTransactions();
|
|
328
|
+
|
|
329
|
+
// Blockchain transaction signing
|
|
330
|
+
const {
|
|
331
|
+
signAndSubmitTransactionWithJWT,
|
|
332
|
+
isSignerAvailable,
|
|
333
|
+
isSignerInitialized,
|
|
334
|
+
currentStatus,
|
|
335
|
+
statusMessage
|
|
336
|
+
} = useTransactionSigner();
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Business & Campaign Management
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
// Business operations
|
|
343
|
+
const {
|
|
344
|
+
getActiveBusinesses,
|
|
345
|
+
getBusinessTypes,
|
|
346
|
+
getBusinesses,
|
|
347
|
+
getBusinessById,
|
|
348
|
+
getBusinessByAccount,
|
|
349
|
+
getBusinessesByType,
|
|
350
|
+
createBusiness, // Admin
|
|
351
|
+
updateBusiness, // Admin
|
|
352
|
+
toggleBusinessStatus // Admin
|
|
353
|
+
} = useBusiness();
|
|
354
|
+
|
|
355
|
+
// Campaign management
|
|
356
|
+
const {
|
|
357
|
+
getActiveCampaigns,
|
|
358
|
+
getCampaigns, // With options: { active?, businessId?, sortBy?, include?, page?, limit? }
|
|
359
|
+
getCampaignById,
|
|
360
|
+
claimCampaign,
|
|
361
|
+
getUserClaims, // With options: { page?, limit?, include? }
|
|
362
|
+
getCampaignTriggers,
|
|
363
|
+
getAllCampaigns, // Admin
|
|
364
|
+
getCampaignClaims, // Admin - with options object
|
|
365
|
+
getCampaignClaimsByUserId, // Admin
|
|
366
|
+
getCampaignClaimsByBusinessId // Admin
|
|
367
|
+
} = useCampaigns();
|
|
368
|
+
|
|
369
|
+
// Redemption system
|
|
370
|
+
const {
|
|
371
|
+
getActiveRedemptions,
|
|
372
|
+
getUserRedemptions,
|
|
373
|
+
redeem,
|
|
374
|
+
getRedemptionTypes,
|
|
375
|
+
signingStatus, // UI feedback during blockchain signing
|
|
376
|
+
signingStatusMessage,
|
|
377
|
+
createRedemption, // Admin
|
|
378
|
+
getAllRedemptions, // Admin
|
|
379
|
+
updateRedemption, // Admin
|
|
380
|
+
toggleRedemptionStatus // Admin
|
|
381
|
+
} = useRedemptions();
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Platform & Analytics
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
// Purchase processing
|
|
388
|
+
const {
|
|
389
|
+
createPaymentIntent,
|
|
390
|
+
getActivePurchaseTokens,
|
|
391
|
+
getAllUserPurchases
|
|
392
|
+
} = usePurchases();
|
|
393
|
+
|
|
394
|
+
// Multi-tenant support
|
|
395
|
+
const {
|
|
396
|
+
getTenantInfo,
|
|
397
|
+
getClientConfig,
|
|
398
|
+
getLoginToken,
|
|
399
|
+
getAdmins
|
|
400
|
+
} = useTenants();
|
|
401
|
+
|
|
402
|
+
// Analytics & reporting
|
|
403
|
+
const {
|
|
404
|
+
getTransactionAnalytics
|
|
405
|
+
} = useAnalytics();
|
|
406
|
+
|
|
407
|
+
// Web3 & blockchain (wallet addresses from user.wallets)
|
|
408
|
+
const {
|
|
409
|
+
getTokenBalance,
|
|
410
|
+
getTokenMetadata,
|
|
411
|
+
getTokenCollection,
|
|
412
|
+
resolveIPFSUrl,
|
|
413
|
+
fetchAndProcessMetadata,
|
|
414
|
+
getChainDataById,
|
|
415
|
+
getExplorerUrl,
|
|
416
|
+
// Helper methods for token collections
|
|
417
|
+
extractTokenIds, // Extract tokenIds from TokenDTO metadata
|
|
418
|
+
getAccountOwnedTokensFromContract, // Recommended: Get owned tokens automatically
|
|
419
|
+
buildCollectionRequest // Build request for getTokenCollection
|
|
420
|
+
} = useWeb3();
|
|
421
|
+
|
|
422
|
+
// User status & achievements
|
|
423
|
+
const {
|
|
424
|
+
getUserStatusTypes,
|
|
425
|
+
getEarnedUserStatus,
|
|
426
|
+
createUserStatusType // Admin
|
|
427
|
+
} = useUserStatus();
|
|
428
|
+
|
|
429
|
+
// File management
|
|
430
|
+
const {
|
|
431
|
+
getSignedPutUrl,
|
|
432
|
+
getSignedGetUrl,
|
|
433
|
+
getSignedUrl,
|
|
434
|
+
optimizeMedia
|
|
435
|
+
} = useFiles();
|
|
436
|
+
|
|
437
|
+
// Donations
|
|
438
|
+
const {
|
|
439
|
+
getDonationTypes
|
|
440
|
+
} = useDonations();
|
|
441
|
+
|
|
442
|
+
// Event subscriptions (notifications, logging)
|
|
443
|
+
const {
|
|
444
|
+
subscribe, // Subscribe to SDK events
|
|
445
|
+
once, // One-time event listener
|
|
446
|
+
clear, // Clear all subscriptions
|
|
447
|
+
isAvailable, // Event system available
|
|
448
|
+
subscriberCount // Active subscriber count
|
|
449
|
+
} = useEvents();
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## Localization
|
|
453
|
+
|
|
454
|
+
Campaign, redemption, business, token metadata and other content can be returned in the user's language. It is
|
|
455
|
+
opt-in: without a locale the API returns the base text, exactly as before.
|
|
456
|
+
|
|
457
|
+
```tsx
|
|
458
|
+
// Device language, or a BCP 47 tag such as 'de' or 'pt-BR'; omit or pass null to keep base text
|
|
459
|
+
<PersSDKProvider config={{ apiProjectKey: 'my-project', locale: 'auto' }}>
|
|
460
|
+
<App />
|
|
461
|
+
</PersSDKProvider>
|
|
462
|
+
|
|
463
|
+
const { locale, setLocale } = usePersSDK();
|
|
464
|
+
setLocale('de'); // then refetch: keep app caches (e.g. React Query keys) per locale
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
- Each field falls back per field: exact tag (`ar-SA`), then language (`ar`), then the base text. Fields without a
|
|
468
|
+
translation stay in the base language.
|
|
469
|
+
- Token metadata read with `useMetadata` is resolved on the device and re-resolves when the locale changes.
|
|
470
|
+
- Tokens minted before localization was enabled, and AI-generated per-mint text, stay in the base language.
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Event System
|
|
475
|
+
|
|
476
|
+
The `useEvents` hook provides access to SDK-wide events for showing notifications, logging, and reacting to SDK operations. All events include a `userMessage` field ready for UI display.
|
|
477
|
+
|
|
478
|
+
### Basic Usage
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
import { useEvents } from '@explorins/pers-sdk-react-native';
|
|
482
|
+
import { useEffect } from 'react';
|
|
483
|
+
|
|
484
|
+
function NotificationHandler() {
|
|
485
|
+
const { subscribe, isAvailable } = useEvents();
|
|
486
|
+
|
|
487
|
+
useEffect(() => {
|
|
488
|
+
if (!isAvailable) return;
|
|
489
|
+
|
|
490
|
+
// Subscribe to all events
|
|
491
|
+
const unsubscribe = subscribe((event) => {
|
|
492
|
+
showNotification(event.userMessage, event.level);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
return () => unsubscribe(); // Cleanup on unmount
|
|
496
|
+
}, [subscribe, isAvailable]);
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Filtering Events
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
// Only transaction successes
|
|
504
|
+
subscribe(
|
|
505
|
+
(event) => {
|
|
506
|
+
playSuccessSound();
|
|
507
|
+
showConfetti();
|
|
508
|
+
},
|
|
509
|
+
{ domain: 'transaction', level: 'success' }
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
// Only errors (for logging)
|
|
513
|
+
subscribe(
|
|
514
|
+
(event) => {
|
|
515
|
+
logToSentry(event);
|
|
516
|
+
},
|
|
517
|
+
{ level: 'error' }
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
// One-time event (auto-unsubscribes)
|
|
521
|
+
once(
|
|
522
|
+
(event) => {
|
|
523
|
+
console.log('First transaction completed!');
|
|
524
|
+
},
|
|
525
|
+
{ domain: 'transaction', level: 'success' }
|
|
526
|
+
);
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Event Domains
|
|
530
|
+
|
|
531
|
+
| Domain | Events |
|
|
532
|
+
|--------|--------|
|
|
533
|
+
| `auth` | Login, logout, token refresh |
|
|
534
|
+
| `user` | Profile updates |
|
|
535
|
+
| `transaction` | Created, completed, failed |
|
|
536
|
+
| `campaign` | Claimed, activated |
|
|
537
|
+
| `redemption` | Redeemed, expired |
|
|
538
|
+
| `business` | Created, updated, membership |
|
|
539
|
+
| `api` | Network errors, validation errors |
|
|
540
|
+
|
|
541
|
+
### Event Structure
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
interface PersEvent {
|
|
545
|
+
id: string; // Unique event ID
|
|
546
|
+
timestamp: number; // Unix timestamp (ms)
|
|
547
|
+
domain: string; // Event domain (transaction, auth, etc.)
|
|
548
|
+
type: string; // Event type within domain
|
|
549
|
+
level: 'success' | 'error';
|
|
550
|
+
userMessage: string; // Ready for UI display
|
|
551
|
+
action?: string; // Suggested action
|
|
552
|
+
details?: object; // Additional data
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## POS Transaction Flow
|
|
559
|
+
|
|
560
|
+
For Point-of-Sale scenarios where a **business submits a transaction on behalf of a user**, use the `buildPOSTransferRequest` helper:
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
import {
|
|
564
|
+
useTransactions,
|
|
565
|
+
buildPOSTransferRequest,
|
|
566
|
+
useEvents
|
|
567
|
+
} from '@explorins/pers-sdk-react-native';
|
|
568
|
+
|
|
569
|
+
function POSScreen() {
|
|
570
|
+
const { createTransaction, signingStatus } = useTransactions();
|
|
571
|
+
const { subscribe, isAvailable } = useEvents();
|
|
572
|
+
|
|
573
|
+
// Listen for transaction events
|
|
574
|
+
useEffect(() => {
|
|
575
|
+
if (!isAvailable) return;
|
|
576
|
+
|
|
577
|
+
const unsubscribe = subscribe(
|
|
578
|
+
(event) => {
|
|
579
|
+
if (event.level === 'success') {
|
|
580
|
+
Alert.alert('Success', event.userMessage);
|
|
581
|
+
}
|
|
582
|
+
},
|
|
583
|
+
{ domain: 'transaction' }
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
return () => unsubscribe();
|
|
587
|
+
}, [subscribe, isAvailable]);
|
|
588
|
+
|
|
589
|
+
const handlePOSTransaction = async (
|
|
590
|
+
userId: string,
|
|
591
|
+
businessId: string,
|
|
592
|
+
amount: number,
|
|
593
|
+
token: TokenDTO
|
|
594
|
+
) => {
|
|
595
|
+
// Build POS transfer request
|
|
596
|
+
const request = buildPOSTransferRequest({
|
|
597
|
+
amount,
|
|
598
|
+
contractAddress: token.contractAddress,
|
|
599
|
+
chainId: token.chainId,
|
|
600
|
+
userId, // User sending tokens
|
|
601
|
+
businessId // Business receiving & authorized to submit
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Create and sign transaction
|
|
605
|
+
const result = await createTransaction(request, (status, message) => {
|
|
606
|
+
console.log(`Signing: ${status} - ${message}`);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
console.log('Transaction created:', result.transaction?.id);
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### POS Authorization Fields
|
|
615
|
+
|
|
616
|
+
The `buildPOSTransferRequest` helper automatically sets:
|
|
617
|
+
|
|
618
|
+
| Field | Value | Purpose |
|
|
619
|
+
|-------|-------|---------|
|
|
620
|
+
| `engagedBusinessId` | Business ID | Business commercially involved (for reporting) |
|
|
621
|
+
| `authorizedSubmitterId` | Business ID | Entity authorized to submit the signed tx |
|
|
622
|
+
| `authorizedSubmitterType` | `BUSINESS` | Type of authorized submitter |
|
|
623
|
+
|
|
624
|
+
For custom scenarios, use `buildTransferRequest` with manual POS fields:
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
import { buildTransferRequest, AccountOwnerType } from '@explorins/pers-sdk-react-native';
|
|
628
|
+
|
|
629
|
+
const request = buildTransferRequest({
|
|
630
|
+
amount: 100,
|
|
631
|
+
contractAddress: '0x...',
|
|
632
|
+
chainId: 137,
|
|
633
|
+
senderAccountId: 'user-123',
|
|
634
|
+
senderAccountType: AccountOwnerType.USER,
|
|
635
|
+
recipientAccountId: 'business-456',
|
|
636
|
+
recipientAccountType: AccountOwnerType.BUSINESS,
|
|
637
|
+
// POS authorization
|
|
638
|
+
engagedBusinessId: 'business-456',
|
|
639
|
+
authorizedSubmitterId: 'business-456',
|
|
640
|
+
authorizedSubmitterType: AccountOwnerType.BUSINESS
|
|
641
|
+
});
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## Token Collection Helper Methods
|
|
647
|
+
|
|
648
|
+
The `useWeb3` hook includes helper methods for querying token balances from any blockchain address. These work with **all token standards** (ERC-20, ERC-721, ERC-1155).
|
|
649
|
+
|
|
650
|
+
### Recommended: getAccountOwnedTokensFromContract
|
|
651
|
+
|
|
652
|
+
A unified API to get all tokens owned by any blockchain address:
|
|
653
|
+
|
|
654
|
+
```typescript
|
|
655
|
+
import { useWeb3, useTokens, usePersSDK } from '@explorins/pers-sdk-react-native';
|
|
656
|
+
|
|
657
|
+
function RewardTokensScreen() {
|
|
658
|
+
const { user } = usePersSDK();
|
|
659
|
+
const { getRewardTokens } = useTokens();
|
|
660
|
+
const { getAccountOwnedTokensFromContract } = useWeb3();
|
|
661
|
+
|
|
662
|
+
const loadUserRewards = async () => {
|
|
663
|
+
const walletAddress = user?.wallets?.[0]?.address;
|
|
664
|
+
if (!walletAddress) return;
|
|
665
|
+
|
|
666
|
+
const rewardTokens = await getRewardTokens();
|
|
667
|
+
|
|
668
|
+
for (const token of rewardTokens) {
|
|
669
|
+
// Works with ERC-20, ERC-721, and ERC-1155 automatically
|
|
670
|
+
const result = await getAccountOwnedTokensFromContract(walletAddress, token);
|
|
671
|
+
|
|
672
|
+
console.log(`Token: ${token.symbol}`);
|
|
673
|
+
console.log(`Owned: ${result.totalOwned}`);
|
|
674
|
+
|
|
675
|
+
result.ownedTokens.forEach(owned => {
|
|
676
|
+
console.log(` - ${owned.metadata?.name}: ${owned.balance}`);
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
return (
|
|
682
|
+
<TouchableOpacity onPress={loadUserRewards}>
|
|
683
|
+
<Text>Load My Rewards</Text>
|
|
684
|
+
</TouchableOpacity>
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### How It Handles Each Token Standard
|
|
690
|
+
|
|
691
|
+
| Token Type | What It Does |
|
|
692
|
+
|------------|--------------|
|
|
693
|
+
| **ERC-20** | Returns balance for fungible tokens |
|
|
694
|
+
| **ERC-721** | Enumerates all owned NFTs |
|
|
695
|
+
| **ERC-1155** | Extracts tokenIds from metadata, queries balances |
|
|
696
|
+
|
|
697
|
+
The helper abstracts away the complexity - especially for ERC-1155 which requires specific `tokenIds` that the helper extracts from `token.metadata[].tokenMetadataIncrementalId`.
|
|
698
|
+
|
|
699
|
+
### Return Type
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
interface AccountOwnedTokensResult {
|
|
703
|
+
token: TokenDTO; // The token definition
|
|
704
|
+
ownedTokens: TokenBalance[]; // Tokens with balance > 0
|
|
705
|
+
totalOwned: number; // Count of owned tokens
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
## EVM Blockchain Transaction Signing
|
|
710
|
+
|
|
711
|
+
The `useTransactionSigner` hook provides secure EVM blockchain transaction signing:
|
|
712
|
+
|
|
713
|
+
```typescript
|
|
714
|
+
import { useTransactionSigner } from '@explorins/pers-sdk-react-native';
|
|
715
|
+
|
|
716
|
+
function BlockchainRedemption() {
|
|
717
|
+
const {
|
|
718
|
+
signAndSubmitTransactionWithJWT,
|
|
719
|
+
isSignerAvailable,
|
|
720
|
+
isSignerInitialized
|
|
721
|
+
} = useTransactionSigner();
|
|
722
|
+
|
|
723
|
+
const handleBlockchainRedemption = async (jwtToken: string) => {
|
|
724
|
+
if (!isSignerAvailable) {
|
|
725
|
+
console.error('EVM blockchain signer not available');
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
try {
|
|
730
|
+
// This handles the complete flow:
|
|
731
|
+
// 1. User authentication via WebAuthn
|
|
732
|
+
// 2. Transaction data fetching from PERS backend
|
|
733
|
+
// 3. Secure EVM transaction signing using device biometrics
|
|
734
|
+
// 4. EVM blockchain submission
|
|
735
|
+
const result = await signAndSubmitTransactionWithJWT(jwtToken);
|
|
736
|
+
|
|
737
|
+
if (result.success) {
|
|
738
|
+
console.log('Transaction completed!');
|
|
739
|
+
console.log('Hash:', result.transactionHash);
|
|
740
|
+
console.log('View on blockchain explorer: [Chain-specific URL]');
|
|
741
|
+
|
|
742
|
+
// Handle post-transaction flow
|
|
743
|
+
if (result.shouldRedirect && result.redirectUrl) {
|
|
744
|
+
// Navigate to success page or external URL
|
|
745
|
+
Linking.openURL(result.redirectUrl);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
} catch (error) {
|
|
749
|
+
if (error.message.includes('expired')) {
|
|
750
|
+
// JWT token expired - redirect to login
|
|
751
|
+
navigation.navigate('Login');
|
|
752
|
+
} else if (error.message.includes('cancelled')) {
|
|
753
|
+
// User cancelled WebAuthn authentication
|
|
754
|
+
console.log('User cancelled transaction');
|
|
755
|
+
} else {
|
|
756
|
+
// Other errors
|
|
757
|
+
Alert.alert('Transaction Failed', error.message);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
// Show loading state while signer initializes
|
|
763
|
+
if (!isSignerInitialized) {
|
|
764
|
+
return (
|
|
765
|
+
<View style={styles.loadingContainer}>
|
|
766
|
+
<ActivityIndicator size="large" />
|
|
767
|
+
<Text>Initializing EVM blockchain signer...</Text>
|
|
768
|
+
</View>
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return (
|
|
773
|
+
<TouchableOpacity
|
|
774
|
+
onPress={() => handleBlockchainRedemption(redemptionJWT)}
|
|
775
|
+
disabled={!isSignerAvailable}
|
|
776
|
+
style={[styles.button, !isSignerAvailable && styles.disabled]}
|
|
777
|
+
>
|
|
778
|
+
<Text style={styles.buttonText}>
|
|
779
|
+
Sign & Submit Transaction
|
|
780
|
+
</Text>
|
|
781
|
+
</TouchableOpacity>
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
```
|
|
785
|
+
### Error Handling
|
|
786
|
+
|
|
787
|
+
The SDK provides comprehensive structured error handling with utilities for consistent error management:
|
|
788
|
+
|
|
789
|
+
```typescript
|
|
790
|
+
import {
|
|
791
|
+
PersApiError,
|
|
792
|
+
ErrorUtils
|
|
793
|
+
} from '@explorins/pers-sdk-react-native';
|
|
794
|
+
import { Alert } from 'react-native';
|
|
795
|
+
|
|
796
|
+
// Structured error handling
|
|
797
|
+
try {
|
|
798
|
+
await sdk.campaigns.claimCampaign({ campaignId });
|
|
799
|
+
} catch (error) {
|
|
800
|
+
if (error instanceof PersApiError) {
|
|
801
|
+
// Structured error with backend details
|
|
802
|
+
console.log('Error code:', error.code); // 'CAMPAIGN_BUSINESS_REQUIRED'
|
|
803
|
+
console.log('Status:', error.status); // 400
|
|
804
|
+
console.log('User message:', error.userMessage); // User-friendly message
|
|
805
|
+
|
|
806
|
+
// Show user-friendly error
|
|
807
|
+
Alert.alert('Error', error.userMessage || error.message);
|
|
808
|
+
|
|
809
|
+
// Check if retryable
|
|
810
|
+
if (error.retryable) {
|
|
811
|
+
Alert.alert('Error', error.message, [
|
|
812
|
+
{ text: 'Cancel' },
|
|
813
|
+
{ text: 'Retry', onPress: () => retry() }
|
|
814
|
+
]);
|
|
815
|
+
}
|
|
816
|
+
} else {
|
|
817
|
+
// Generic error fallback with ErrorUtils
|
|
818
|
+
const message = ErrorUtils.getMessage(error);
|
|
819
|
+
Alert.alert('Error', message);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Error utilities for any error type
|
|
824
|
+
const status = ErrorUtils.getStatus(error); // Extract HTTP status
|
|
825
|
+
const message = ErrorUtils.getMessage(error); // Extract user message
|
|
826
|
+
const retryable = ErrorUtils.isRetryable(error); // Check if retryable
|
|
827
|
+
const tokenExpired = ErrorUtils.isTokenExpiredError(error); // Detect token expiration
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
### Transaction Helpers
|
|
831
|
+
|
|
832
|
+
**NEW in v2.1.1:** Factory functions for client-side transaction workflows:
|
|
833
|
+
|
|
834
|
+
```typescript
|
|
835
|
+
import {
|
|
836
|
+
ClientTransactionType,
|
|
837
|
+
buildPendingTransactionData,
|
|
838
|
+
extractDeadlineFromSigningData,
|
|
839
|
+
type PendingTransactionParams
|
|
840
|
+
} from '@explorins/pers-sdk-react-native';
|
|
841
|
+
|
|
842
|
+
// Build pending transaction data for QR codes, NFC, deep links, etc.
|
|
843
|
+
const qrData = buildPendingTransactionData(
|
|
844
|
+
signingResult.transactionId,
|
|
845
|
+
signingResult.signature,
|
|
846
|
+
'EIP-712' // Default format
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
console.log(qrData);
|
|
850
|
+
// {
|
|
851
|
+
// transactionId: '...',
|
|
852
|
+
// signature: '0x...',
|
|
853
|
+
// transactionFormat: 'EIP-712',
|
|
854
|
+
// txType: 'PENDING_SUBMISSION'
|
|
855
|
+
// }
|
|
856
|
+
|
|
857
|
+
// Serialize for transfer (QR code, NFC, etc.)
|
|
858
|
+
const qrCodeValue = JSON.stringify(qrData);
|
|
859
|
+
|
|
860
|
+
// Extract deadline from EIP-712 signing data
|
|
861
|
+
const deadline = extractDeadlineFromSigningData(signingResult.signingData);
|
|
862
|
+
if (deadline) {
|
|
863
|
+
const expiryTime = new Date(deadline * 1000);
|
|
864
|
+
console.log('Transaction expires at:', expiryTime);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Client transaction types
|
|
868
|
+
if (transaction.txType === ClientTransactionType.PENDING_SUBMISSION) {
|
|
869
|
+
// Handle pending submission flow
|
|
870
|
+
}
|
|
871
|
+
```
|
|
872
|
+
## Security Features
|
|
873
|
+
|
|
874
|
+
### WebAuthn Authentication
|
|
875
|
+
- Device biometric authentication (fingerprint, face ID, PIN)
|
|
876
|
+
- No passwords or private keys stored locally
|
|
877
|
+
- Secure hardware-backed authentication
|
|
878
|
+
|
|
879
|
+
### Token Storage
|
|
880
|
+
- React Native Keychain integration for sensitive data
|
|
881
|
+
- Automatic token refresh and expiration handling
|
|
882
|
+
- Multiple storage strategies (AsyncStorage, Keychain, Memory)
|
|
883
|
+
|
|
884
|
+
### Transaction Security
|
|
885
|
+
- JWT token validation and expiration checking
|
|
886
|
+
- Secure transaction signing without exposing private keys
|
|
887
|
+
- Automatic session management with secure caching
|
|
888
|
+
|
|
889
|
+
## Platform Support
|
|
890
|
+
|
|
891
|
+
- **iOS**: Full support with Keychain integration
|
|
892
|
+
- **Android**: Full support with Keystore integration
|
|
893
|
+
- **Expo**: Compatible with Expo managed workflow
|
|
894
|
+
- **Web**: React Native Web compatibility
|
|
895
|
+
|
|
896
|
+
---
|
|
897
|
+
|
|
898
|
+
## Analytics & Attribution Tracking
|
|
899
|
+
|
|
900
|
+
The SDK provides built-in analytics tracking for user acquisition and engagement. The `PersSDKProvider` **automatically detects the platform** (iOS/Android) and sends tracking headers with all API requests.
|
|
901
|
+
|
|
902
|
+
### Automatic Platform Detection
|
|
903
|
+
|
|
904
|
+
When you wrap your app with `PersSDKProvider`, platform info is auto-detected:
|
|
905
|
+
|
|
906
|
+
```typescript
|
|
907
|
+
// Platform is automatically detected and set:
|
|
908
|
+
// { os: 'iOS', osVersion: '17.4', deviceType: 'phone', app: 'MyApp' }
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
### Manual Data Source Tracking
|
|
912
|
+
|
|
913
|
+
For campaign attribution, use `usePersSDK()` to set custom tracking data:
|
|
914
|
+
|
|
915
|
+
```typescript
|
|
916
|
+
import { usePersSDK } from '@explorins/pers-sdk-react-native';
|
|
917
|
+
import { Linking } from 'react-native';
|
|
918
|
+
|
|
919
|
+
function DeepLinkHandler() {
|
|
920
|
+
const { sdk } = usePersSDK();
|
|
921
|
+
|
|
922
|
+
useEffect(() => {
|
|
923
|
+
// Handle deep links with UTM params
|
|
924
|
+
Linking.getInitialURL().then(url => {
|
|
925
|
+
if (url) {
|
|
926
|
+
const params = new URL(url).searchParams;
|
|
927
|
+
|
|
928
|
+
sdk.setDataSource({
|
|
929
|
+
channel: 'mobile',
|
|
930
|
+
medium: params.get('utm_medium') || undefined,
|
|
931
|
+
campaign: params.get('utm_campaign') || undefined,
|
|
932
|
+
source: params.get('utm_source') || undefined,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}, [sdk]);
|
|
937
|
+
}
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
### HTTP Headers Sent
|
|
941
|
+
|
|
942
|
+
The SDK automatically sends tracking data as HTTP headers:
|
|
943
|
+
|
|
944
|
+
| Header | Source | Description |
|
|
945
|
+
|--------|--------|-------------|
|
|
946
|
+
| `x-source-channel` | `dataSource.channel` | `'mobile'` for native apps |
|
|
947
|
+
| `x-source-medium` | `dataSource.medium` | Traffic medium (push, email, etc.) |
|
|
948
|
+
| `x-source-campaign` | `dataSource.campaign` | Campaign identifier |
|
|
949
|
+
| `x-source` | `dataSource.source` | Traffic source |
|
|
950
|
+
| `x-platform` | Auto-detected | JSON: `{ os, osVersion, deviceType, app }` |
|
|
951
|
+
|
|
952
|
+
### Push Notification Attribution
|
|
953
|
+
|
|
954
|
+
Track users coming from push notifications:
|
|
955
|
+
|
|
956
|
+
```typescript
|
|
957
|
+
import { usePersSDK } from '@explorins/pers-sdk-react-native';
|
|
958
|
+
|
|
959
|
+
function usePushNotificationTracking() {
|
|
960
|
+
const { sdk } = usePersSDK();
|
|
961
|
+
|
|
962
|
+
const handlePushNotification = (notification: any) => {
|
|
963
|
+
// Extract campaign from notification data
|
|
964
|
+
const campaignId = notification.data?.campaignId;
|
|
965
|
+
|
|
966
|
+
if (campaignId) {
|
|
967
|
+
sdk.updateDataSource({
|
|
968
|
+
medium: 'push',
|
|
969
|
+
campaign: campaignId,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
return { handlePushNotification };
|
|
975
|
+
}
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
---
|
|
979
|
+
|
|
980
|
+
## Advanced Configuration
|
|
981
|
+
|
|
982
|
+
### SDK Initialization & DPoP
|
|
983
|
+
|
|
984
|
+
The `PersSDKProvider` accepts a `config` object allowing you to customize behavior, including **DPoP (Distributed Proof of Possession)**.
|
|
985
|
+
|
|
986
|
+
**DPoP Behavior:**
|
|
987
|
+
- **Enabled by Default**: Automatically active on iOS/Android using a high-performance C++ crypto bridge (`react-native-quick-crypto`).
|
|
988
|
+
- **Web Support**: Uses standard WebCrypto API.
|
|
989
|
+
- **Security**: Binds access tokens to the device, preventing replay attacks.
|
|
990
|
+
|
|
991
|
+
**Customizing Initialization:**
|
|
992
|
+
```typescript
|
|
993
|
+
<PersSDKProvider config={{
|
|
994
|
+
apiProjectKey: 'your-project-key',
|
|
995
|
+
// DPoP is enabled by default. To disable (not recommended):
|
|
996
|
+
dpop: {
|
|
997
|
+
enabled: false
|
|
998
|
+
}
|
|
999
|
+
}}>
|
|
1000
|
+
<YourApp />
|
|
1001
|
+
</PersSDKProvider>
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
### Storage Fallback Strategy
|
|
1005
|
+
|
|
1006
|
+
The SDK implements a robust **Hybrid Storage Strategy**:
|
|
1007
|
+
1. **Primary**: Attempts to use **Android Keystore / iOS Keychain** (via `react-native-keychain`) for maximum security.
|
|
1008
|
+
2. **Fallback**: If hardware storage is unavailable or fails, it automatically falls back to **AsyncStorage**.
|
|
1009
|
+
3. **Web**: Uses `localStorage` automatically.
|
|
1010
|
+
|
|
1011
|
+
This ensures your app works reliably across all devices while prioritizing security where available.
|
|
1012
|
+
|
|
1013
|
+
### Custom Authentication Provider
|
|
1014
|
+
|
|
1015
|
+
The SDK automatically uses `ReactNativeSecureStorage` for React Native (iOS/Android) and `LocalStorageTokenStorage` for Web. You can customize this if needed:
|
|
1016
|
+
|
|
1017
|
+
```typescript
|
|
1018
|
+
import { createReactNativeAuthProvider, ReactNativeSecureStorage } from '@explorins/pers-sdk-react-native';
|
|
1019
|
+
|
|
1020
|
+
const authProvider = createReactNativeAuthProvider('your-project-key', {
|
|
1021
|
+
keyPrefix: 'my_app_tokens_',
|
|
1022
|
+
debug: true,
|
|
1023
|
+
// Optional: Provide custom storage implementation
|
|
1024
|
+
// customStorage: new ReactNativeSecureStorage('custom_prefix_')
|
|
1025
|
+
});
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
### Error Handling Patterns
|
|
1029
|
+
|
|
1030
|
+
```typescript
|
|
1031
|
+
import { useTokens } from '@explorins/pers-sdk-react-native';
|
|
1032
|
+
|
|
1033
|
+
function ErrorHandlingExample() {
|
|
1034
|
+
const { getTokens } = useTokens();
|
|
1035
|
+
const [error, setError] = useState<string | null>(null);
|
|
1036
|
+
|
|
1037
|
+
const handleLoadTokensWithErrorHandling = async () => {
|
|
1038
|
+
try {
|
|
1039
|
+
setError(null);
|
|
1040
|
+
await getTokens();
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
// Handle specific error types
|
|
1043
|
+
if (err.code === 'NETWORK_ERROR') {
|
|
1044
|
+
setError('Network connection required');
|
|
1045
|
+
} else if (err.code === 'INVALID_TOKEN') {
|
|
1046
|
+
setError('Please log in again');
|
|
1047
|
+
} else {
|
|
1048
|
+
setError(err.message || 'An unexpected error occurred');
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
## Analytics Integration
|
|
1056
|
+
|
|
1057
|
+
```typescript
|
|
1058
|
+
import { useAnalytics } from '@explorins/pers-sdk-react-native';
|
|
1059
|
+
|
|
1060
|
+
function AnalyticsExample() {
|
|
1061
|
+
const { getTransactionAnalytics } = useAnalytics();
|
|
1062
|
+
|
|
1063
|
+
const loadAnalytics = async () => {
|
|
1064
|
+
try {
|
|
1065
|
+
const txAnalytics = await getTransactionAnalytics({
|
|
1066
|
+
groupBy: ['day'],
|
|
1067
|
+
metrics: ['count', 'sum'],
|
|
1068
|
+
startDate: '2023-01-01',
|
|
1069
|
+
endDate: '2023-12-31'
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
console.log('Transaction analytics:', txAnalytics.results);
|
|
1073
|
+
console.log('Execution time:', txAnalytics.metadata?.executionTime);
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
console.error('Failed to load analytics:', error);
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
## Migration Guide
|
|
1082
|
+
|
|
1083
|
+
### v2.0.0 Breaking Changes - Pagination
|
|
1084
|
+
|
|
1085
|
+
**Version 2.0.0 introduces standardized pagination** across all hooks that return lists. Previously, hooks returned raw arrays. Now they return `PaginatedResponseDTO<T>` with pagination metadata.
|
|
1086
|
+
|
|
1087
|
+
#### What Changed
|
|
1088
|
+
|
|
1089
|
+
All hooks returning lists now return paginated responses:
|
|
1090
|
+
|
|
1091
|
+
```typescript
|
|
1092
|
+
// ❌ OLD (v1.x) - Direct array
|
|
1093
|
+
const businesses: BusinessDTO[] = await getActiveBusinesses();
|
|
1094
|
+
|
|
1095
|
+
// ✅ NEW (v2.x) - Paginated response
|
|
1096
|
+
const response: PaginatedResponseDTO<BusinessDTO> = await getActiveBusinesses();
|
|
1097
|
+
const businesses: BusinessDTO[] = response.data;
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
#### Affected Hooks
|
|
1101
|
+
|
|
1102
|
+
| Hook | Method | Return Type |
|
|
1103
|
+
|------|--------|-------------|
|
|
1104
|
+
| `useBusiness` | `getActiveBusinesses()` | `PaginatedResponseDTO<BusinessDTO>` |
|
|
1105
|
+
| `useBusiness` | `getBusinessTypes()` | `PaginatedResponseDTO<BusinessTypeDTO>` |
|
|
1106
|
+
| `useCampaigns` | `getActiveCampaigns()` | `PaginatedResponseDTO<CampaignDTO>` |
|
|
1107
|
+
| `useCampaigns` | `getUserClaims()` | `PaginatedResponseDTO<CampaignClaimDTO>` |
|
|
1108
|
+
| `useTokens` | `getTokens()` | `PaginatedResponseDTO<TokenDTO>` |
|
|
1109
|
+
| `useTokens` | `getRewardTokens()` | `PaginatedResponseDTO<TokenDTO>` |
|
|
1110
|
+
| `useTokens` | `getStatusTokens()` | `PaginatedResponseDTO<TokenDTO>` |
|
|
1111
|
+
| `useRedemptions` | `getActiveRedemptions()` | `PaginatedResponseDTO<RedemptionDTO>` |
|
|
1112
|
+
| `useRedemptions` | `getUserRedemptions()` | `PaginatedResponseDTO<RedemptionDTO>` |
|
|
1113
|
+
| `useTransactions` | `getUserTransactionHistory()` | `PaginatedResponseDTO<TransactionDTO>` |
|
|
1114
|
+
| `usePurchases` | `getAllUserPurchases()` | `PaginatedResponseDTO<PurchaseDTO>` |
|
|
1115
|
+
| `usePurchases` | `getActivePurchaseTokens()` | `PaginatedResponseDTO<PurchaseTokenDTO>` |
|
|
1116
|
+
| `useDonations` | `getDonationTypes()` | `PaginatedResponseDTO<DonationTypeDTO>` |
|
|
1117
|
+
|
|
1118
|
+
#### PaginatedResponseDTO Structure
|
|
1119
|
+
|
|
1120
|
+
```typescript
|
|
1121
|
+
import type { PaginatedResponseDTO } from '@explorins/pers-shared';
|
|
1122
|
+
|
|
1123
|
+
interface PaginatedResponseDTO<T> {
|
|
1124
|
+
data: T[]; // Array of results
|
|
1125
|
+
pagination: {
|
|
1126
|
+
currentPage: number; // Current page number (1-indexed)
|
|
1127
|
+
pageSize: number; // Items per page
|
|
1128
|
+
totalItems: number; // Total number of items across all pages
|
|
1129
|
+
totalPages: number; // Total number of pages
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
#### Migration Examples
|
|
1135
|
+
|
|
1136
|
+
**Before (v1.x):**
|
|
1137
|
+
```typescript
|
|
1138
|
+
const businesses = await getActiveBusinesses();
|
|
1139
|
+
console.log('Business count:', businesses.length);
|
|
1140
|
+
businesses.forEach(b => console.log(b.name));
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
**After (v2.x):**
|
|
1144
|
+
```typescript
|
|
1145
|
+
const response = await getActiveBusinesses();
|
|
1146
|
+
console.log('Business count:', response.data.length);
|
|
1147
|
+
console.log('Total businesses:', response.pagination.totalItems);
|
|
1148
|
+
response.data.forEach(b => console.log(b.name));
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
**With Pagination Parameters:**
|
|
1152
|
+
```typescript
|
|
1153
|
+
// Fetch page 2 with 20 items per page
|
|
1154
|
+
const response = await getActiveBusinesses({ page: 2, pageSize: 20 });
|
|
1155
|
+
|
|
1156
|
+
console.log(`Page ${response.pagination.currentPage} of ${response.pagination.totalPages}`);
|
|
1157
|
+
console.log(`Showing ${response.data.length} businesses`);
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
#### React Native Component Example
|
|
1161
|
+
|
|
1162
|
+
```typescript
|
|
1163
|
+
import { useState, useEffect } from 'react';
|
|
1164
|
+
import { useBusiness } from '@explorins/pers-sdk-react-native';
|
|
1165
|
+
import { View, Text, FlatList, ActivityIndicator } from 'react-native';
|
|
1166
|
+
|
|
1167
|
+
function BusinessListScreen() {
|
|
1168
|
+
const { getActiveBusinesses } = useBusiness();
|
|
1169
|
+
const [businesses, setBusinesses] = useState<BusinessDTO[]>([]);
|
|
1170
|
+
const [pagination, setPagination] = useState(null);
|
|
1171
|
+
const [loading, setLoading] = useState(true);
|
|
1172
|
+
|
|
1173
|
+
useEffect(() => {
|
|
1174
|
+
loadBusinesses();
|
|
1175
|
+
}, []);
|
|
1176
|
+
|
|
1177
|
+
const loadBusinesses = async (page = 1) => {
|
|
1178
|
+
try {
|
|
1179
|
+
setLoading(true);
|
|
1180
|
+
const response = await getActiveBusinesses({ page, pageSize: 20 });
|
|
1181
|
+
setBusinesses(response.data);
|
|
1182
|
+
setPagination(response.pagination);
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
console.error('Failed to load businesses:', error);
|
|
1185
|
+
} finally {
|
|
1186
|
+
setLoading(false);
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
if (loading) return <ActivityIndicator />;
|
|
1191
|
+
|
|
1192
|
+
return (
|
|
1193
|
+
<View>
|
|
1194
|
+
<Text>
|
|
1195
|
+
Showing {businesses.length} of {pagination?.totalItems} businesses
|
|
1196
|
+
</Text>
|
|
1197
|
+
<FlatList
|
|
1198
|
+
data={businesses}
|
|
1199
|
+
keyExtractor={(item) => item.id}
|
|
1200
|
+
renderItem={({ item }) => <BusinessCard business={item} />}
|
|
1201
|
+
/>
|
|
1202
|
+
<Text>Page {pagination?.currentPage} of {pagination?.totalPages}</Text>
|
|
1203
|
+
</View>
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
#### Quick Fix for Existing Code
|
|
1209
|
+
|
|
1210
|
+
If you want minimal code changes, extract `.data` immediately:
|
|
1211
|
+
|
|
1212
|
+
```typescript
|
|
1213
|
+
// Quick adaptation in your hooks
|
|
1214
|
+
const businesses = (await getActiveBusinesses()).data;
|
|
1215
|
+
const campaigns = (await getActiveCampaigns()).data;
|
|
1216
|
+
const tokens = (await getTokens()).data;
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
#### Benefits of Pagination
|
|
1220
|
+
|
|
1221
|
+
- **Performance**: Load only what you need, not entire datasets
|
|
1222
|
+
- **Consistency**: All list endpoints follow the same pattern
|
|
1223
|
+
- **Metadata**: Access total counts without loading all items
|
|
1224
|
+
- **Better UX**: Build proper pagination UI components
|
|
1225
|
+
- **Memory Efficiency**: Reduced memory footprint for large datasets
|
|
1226
|
+
|
|
1227
|
+
---
|
|
1228
|
+
|
|
1229
|
+
## Documentation
|
|
1230
|
+
|
|
1231
|
+
All documentation is available on GitHub:
|
|
1232
|
+
|
|
1233
|
+
| Guide | Description |
|
|
1234
|
+
|-------|-------------|
|
|
1235
|
+
| [Quick Setup Guide](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/docs/QUICK_SETUP_GUIDE.md) | Android + Passkey setup in one page |
|
|
1236
|
+
| [**Troubleshooting**](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/docs/TROUBLESHOOTING.md) | **Common issues & solutions** |
|
|
1237
|
+
| [Passkey Setup](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/REACT_NATIVE_PASSKEY_SETUP.md) | WebAuthn/Passkey configuration |
|
|
1238
|
+
| [Android Compatibility](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/docs/ANDROID_COMPATIBILITY_FAQ.md) | New Architecture troubleshooting |
|
|
1239
|
+
| [Auth State Handling](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/docs/AUTH_STATE_HANDLING.md) | Session management patterns |
|
|
1240
|
+
| [Changelog](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/CHANGELOG.md) | Version history |
|
|
1241
|
+
|
|
1242
|
+
### Quick Reference: Common Issues
|
|
1243
|
+
|
|
1244
|
+
- **User is null after restart?** → Session restoration is async. [See solution](./docs/TROUBLESHOOTING.md#1-sdkuser-is-null-after-app-restart)
|
|
1245
|
+
- **Stamp transaction fails?** → Use `tokenBalance.tokenId`, not metadata ID. [See solution](./docs/TROUBLESHOOTING.md#3-stamp-erc721-voucher-transaction-fails)
|
|
1246
|
+
- **Which wallet to use?** → Match by `token.chainId`, never use `wallets[0]`. [See solution](./docs/TROUBLESHOOTING.md#6-which-wallet-to-use-multiple-wallets)
|
|
1247
|
+
- **Passkey "NotConfigured"?** → Device needs Google Play Services. [See solution](./docs/TROUBLESHOOTING.md#2-passkey-notconfigured-error-on-android)
|
|
1248
|
+
|
|
1249
|
+
---
|
|
1250
|
+
|
|
1251
|
+
## Contributing
|
|
1252
|
+
|
|
1253
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
1254
|
+
|
|
1255
|
+
## License
|
|
1256
|
+
|
|
1257
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
1258
|
+
|
|
1259
|
+
## Support
|
|
1260
|
+
|
|
1261
|
+
- Email: support@explorins.com
|
|
1262
|
+
- Documentation: [https://docs.pers.ninja](https://docs.pers.ninja)
|
|
1263
|
+
- Issues: [GitHub Issues](https://github.com/eXplorins/pers-sdk/issues)
|
|
1264
|
+
|
|
1265
|
+
---
|
|
1266
|
+
|
|
1245
1267
|
Built with care by [eXplorins](https://explorins.com) for the tourism and loyalty industry.
|