@explorins/pers-sdk-react-native 2.2.0-beta.1 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/__tests__/PersSDKProvider.test.d.ts +2 -0
- package/dist/__tests__/PersSDKProvider.test.d.ts.map +1 -0
- package/dist/__tests__/PersSDKProvider.test.js +290 -0
- package/dist/__tests__/setup.d.ts +2 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +35 -0
- package/dist/hooks/useRedemptions.d.ts +1 -0
- package/dist/hooks/useRedemptions.d.ts.map +1 -1
- package/dist/hooks/useRedemptions.js +69 -3
- package/dist/hooks/useTokenBalances.d.ts +34 -1
- package/dist/hooks/useTokenBalances.d.ts.map +1 -1
- package/dist/hooks/useTokens.d.ts +4 -1
- package/dist/hooks/useTokens.d.ts.map +1 -1
- package/dist/hooks/useTokens.js +96 -0
- package/dist/index.js +948 -81
- package/dist/index.js.map +1 -1
- package/dist/providers/PersSDKProvider.d.ts +36 -2
- package/dist/providers/PersSDKProvider.d.ts.map +1 -1
- package/dist/providers/PersSDKProvider.js +1 -1
- package/package.json +12 -2
- package/src/__tests__/PersSDKProvider.test.tsx +420 -0
- package/src/__tests__/setup.ts +40 -0
- package/src/hooks/useRedemptions.ts +74 -4
- package/src/hooks/useTokenBalances.ts +34 -1
- package/src/hooks/useTokens.ts +100 -1
- package/src/providers/PersSDKProvider.tsx +37 -3
package/README.md
CHANGED
|
@@ -1127,11 +1127,19 @@ All documentation is available on GitHub:
|
|
|
1127
1127
|
| Guide | Description |
|
|
1128
1128
|
|-------|-------------|
|
|
1129
1129
|
| [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 |
|
|
1130
|
+
| [**Troubleshooting**](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/docs/TROUBLESHOOTING.md) | **Common issues & solutions** |
|
|
1130
1131
|
| [Passkey Setup](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/REACT_NATIVE_PASSKEY_SETUP.md) | WebAuthn/Passkey configuration |
|
|
1131
1132
|
| [Android Compatibility](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/docs/ANDROID_COMPATIBILITY_FAQ.md) | New Architecture troubleshooting |
|
|
1132
1133
|
| [Auth State Handling](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/docs/AUTH_STATE_HANDLING.md) | Session management patterns |
|
|
1133
1134
|
| [Changelog](https://github.com/eXplorins/pers-sdk/blob/main/packages/pers-sdk-react-native/CHANGELOG.md) | Version history |
|
|
1134
1135
|
|
|
1136
|
+
### Quick Reference: Common Issues
|
|
1137
|
+
|
|
1138
|
+
- **User is null after restart?** → Session restoration is async. [See solution](./docs/TROUBLESHOOTING.md#1-sdkuser-is-null-after-app-restart)
|
|
1139
|
+
- **Stamp transaction fails?** → Use `tokenBalance.tokenId`, not metadata ID. [See solution](./docs/TROUBLESHOOTING.md#3-stamp-erc721-voucher-transaction-fails)
|
|
1140
|
+
- **Which wallet to use?** → Match by `token.chainId`, never use `wallets[0]`. [See solution](./docs/TROUBLESHOOTING.md#6-which-wallet-to-use-multiple-wallets)
|
|
1141
|
+
- **Passkey "NotConfigured"?** → Device needs Google Play Services. [See solution](./docs/TROUBLESHOOTING.md#2-passkey-notconfigured-error-on-android)
|
|
1142
|
+
|
|
1135
1143
|
---
|
|
1136
1144
|
|
|
1137
1145
|
## Contributing
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PersSDKProvider.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/PersSDKProvider.test.tsx"],"names":[],"mappings":"AAEA,OAAO,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render, act, waitFor } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
import { PersSDKProvider, usePersSDK } from '../providers/PersSDKProvider';
|
|
6
|
+
// Store SDK instances for event emission
|
|
7
|
+
let mockSdkInstance = null;
|
|
8
|
+
// Mock the SDK core module BEFORE any imports use it
|
|
9
|
+
jest.mock('@explorins/pers-sdk/core', () => {
|
|
10
|
+
const createMockEventEmitter = () => {
|
|
11
|
+
const subscribers = [];
|
|
12
|
+
return {
|
|
13
|
+
subscribe: jest.fn((callback, options) => {
|
|
14
|
+
subscribers.push({ callback, options });
|
|
15
|
+
return () => {
|
|
16
|
+
const index = subscribers.findIndex(s => s.callback === callback);
|
|
17
|
+
if (index > -1)
|
|
18
|
+
subscribers.splice(index, 1);
|
|
19
|
+
};
|
|
20
|
+
}),
|
|
21
|
+
emit: (event) => {
|
|
22
|
+
subscribers.forEach(({ callback, options }) => {
|
|
23
|
+
if (!options?.domains || options.domains.includes(event.domain)) {
|
|
24
|
+
callback(event);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
_subscribers: subscribers
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
const MockPersSDK = jest.fn().mockImplementation(() => {
|
|
32
|
+
const events = createMockEventEmitter();
|
|
33
|
+
mockSdkInstance = {
|
|
34
|
+
events,
|
|
35
|
+
auth: {
|
|
36
|
+
ensureValidToken: jest.fn().mockResolvedValue(true)
|
|
37
|
+
},
|
|
38
|
+
users: {
|
|
39
|
+
getCurrentUser: jest.fn().mockResolvedValue({ id: 'refreshed-user', email: 'refreshed@test.com' })
|
|
40
|
+
},
|
|
41
|
+
restoreSession: jest.fn().mockResolvedValue(null)
|
|
42
|
+
};
|
|
43
|
+
return mockSdkInstance;
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
PersSDK: MockPersSDK,
|
|
47
|
+
DefaultAuthProvider: jest.fn().mockImplementation(() => ({
|
|
48
|
+
getToken: jest.fn().mockResolvedValue('mock-token')
|
|
49
|
+
}))
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
// Mock platform-specific providers
|
|
53
|
+
jest.mock('../providers/react-native-http-client', () => ({
|
|
54
|
+
ReactNativeHttpClient: jest.fn().mockImplementation(() => ({}))
|
|
55
|
+
}));
|
|
56
|
+
jest.mock('../providers/react-native-auth-provider', () => ({
|
|
57
|
+
createReactNativeAuthProvider: jest.fn().mockReturnValue({
|
|
58
|
+
getToken: jest.fn().mockResolvedValue('mock-token')
|
|
59
|
+
})
|
|
60
|
+
}));
|
|
61
|
+
jest.mock('../providers/rn-dpop-provider', () => ({
|
|
62
|
+
ReactNativeDPoPProvider: jest.fn().mockImplementation(() => ({}))
|
|
63
|
+
}));
|
|
64
|
+
// Helper component to access context
|
|
65
|
+
const TestConsumer = ({ onContext }) => {
|
|
66
|
+
const context = usePersSDK();
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
onContext(context);
|
|
69
|
+
}, [context, onContext]);
|
|
70
|
+
return _jsx("div", { "data-testid": "consumer", children: "Consumer" });
|
|
71
|
+
};
|
|
72
|
+
// Helper to emit events on the mock SDK
|
|
73
|
+
const emitAuthEvent = (type, details = {}) => {
|
|
74
|
+
if (mockSdkInstance?.events) {
|
|
75
|
+
mockSdkInstance.events.emit({
|
|
76
|
+
type,
|
|
77
|
+
domain: 'authentication',
|
|
78
|
+
details
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
describe('PersSDKProvider', () => {
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
jest.clearAllMocks();
|
|
85
|
+
mockSdkInstance = null;
|
|
86
|
+
});
|
|
87
|
+
describe('initialization', () => {
|
|
88
|
+
it('should initialize with null SDK when no config provided', async () => {
|
|
89
|
+
let capturedContext = null;
|
|
90
|
+
render(_jsx(PersSDKProvider, { children: _jsx(TestConsumer, { onContext: (ctx) => { capturedContext = ctx; } }) }));
|
|
91
|
+
await waitFor(() => {
|
|
92
|
+
expect(capturedContext).not.toBeNull();
|
|
93
|
+
});
|
|
94
|
+
expect(capturedContext.sdk).toBeNull();
|
|
95
|
+
expect(capturedContext.isInitialized).toBe(false);
|
|
96
|
+
expect(capturedContext.isAuthenticated).toBe(false);
|
|
97
|
+
expect(capturedContext.user).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
it('should auto-initialize when config is provided', async () => {
|
|
100
|
+
let capturedContext = null;
|
|
101
|
+
render(_jsx(PersSDKProvider, { config: { apiProjectKey: 'test-project' }, children: _jsx(TestConsumer, { onContext: (ctx) => { capturedContext = ctx; } }) }));
|
|
102
|
+
await waitFor(() => {
|
|
103
|
+
expect(capturedContext?.isInitialized).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
expect(capturedContext.sdk).not.toBeNull();
|
|
106
|
+
});
|
|
107
|
+
it('should prevent multiple initializations', async () => {
|
|
108
|
+
const { PersSDK } = require('@explorins/pers-sdk/core');
|
|
109
|
+
let capturedContext = null;
|
|
110
|
+
render(_jsx(PersSDKProvider, { config: { apiProjectKey: 'test-project' }, children: _jsx(TestConsumer, { onContext: (ctx) => { capturedContext = ctx; } }) }));
|
|
111
|
+
await waitFor(() => {
|
|
112
|
+
expect(capturedContext?.isInitialized).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
// Try to initialize again - should be ignored
|
|
115
|
+
await act(async () => {
|
|
116
|
+
await capturedContext.initialize({ apiProjectKey: 'different-project' });
|
|
117
|
+
});
|
|
118
|
+
// Should only have been called once
|
|
119
|
+
expect(PersSDK).toHaveBeenCalledTimes(1);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('event handling', () => {
|
|
123
|
+
it('should set user on session_restored event', async () => {
|
|
124
|
+
let capturedContext = null;
|
|
125
|
+
const mockUser = { id: 'user-123', email: 'test@example.com' };
|
|
126
|
+
render(_jsx(PersSDKProvider, { config: { apiProjectKey: 'test-project' }, children: _jsx(TestConsumer, { onContext: (ctx) => { capturedContext = ctx; } }) }));
|
|
127
|
+
await waitFor(() => {
|
|
128
|
+
expect(capturedContext?.sdk).not.toBeNull();
|
|
129
|
+
});
|
|
130
|
+
// Emit session_restored event
|
|
131
|
+
await act(async () => {
|
|
132
|
+
emitAuthEvent('session_restored', { user: mockUser });
|
|
133
|
+
});
|
|
134
|
+
await waitFor(() => {
|
|
135
|
+
expect(capturedContext?.isAuthenticated).toBe(true);
|
|
136
|
+
expect(capturedContext?.user).toEqual(mockUser);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
it('should set user on login_success event', async () => {
|
|
140
|
+
let capturedContext = null;
|
|
141
|
+
const mockUser = { id: 'user-456', email: 'login@example.com' };
|
|
142
|
+
render(_jsx(PersSDKProvider, { config: { apiProjectKey: 'test-project' }, children: _jsx(TestConsumer, { onContext: (ctx) => { capturedContext = ctx; } }) }));
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
expect(capturedContext?.sdk).not.toBeNull();
|
|
145
|
+
});
|
|
146
|
+
await act(async () => {
|
|
147
|
+
emitAuthEvent('login_success', { user: mockUser });
|
|
148
|
+
});
|
|
149
|
+
await waitFor(() => {
|
|
150
|
+
expect(capturedContext?.isAuthenticated).toBe(true);
|
|
151
|
+
expect(capturedContext?.user).toEqual(mockUser);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
it('should clear user on logout_success event', async () => {
|
|
155
|
+
let capturedContext = null;
|
|
156
|
+
const mockUser = { id: 'user-789', email: 'logout@example.com' };
|
|
157
|
+
render(_jsx(PersSDKProvider, { config: { apiProjectKey: 'test-project' }, children: _jsx(TestConsumer, { onContext: (ctx) => { capturedContext = ctx; } }) }));
|
|
158
|
+
await waitFor(() => {
|
|
159
|
+
expect(capturedContext?.sdk).not.toBeNull();
|
|
160
|
+
});
|
|
161
|
+
// First, log in
|
|
162
|
+
await act(async () => {
|
|
163
|
+
emitAuthEvent('login_success', { user: mockUser });
|
|
164
|
+
});
|
|
165
|
+
await waitFor(() => {
|
|
166
|
+
expect(capturedContext?.isAuthenticated).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
// Then logout
|
|
169
|
+
await act(async () => {
|
|
170
|
+
emitAuthEvent('logout_success', {});
|
|
171
|
+
});
|
|
172
|
+
await waitFor(() => {
|
|
173
|
+
expect(capturedContext?.isAuthenticated).toBe(false);
|
|
174
|
+
expect(capturedContext?.user).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
it('should clear user on session_restoration_failed event', async () => {
|
|
178
|
+
let capturedContext = null;
|
|
179
|
+
render(_jsx(PersSDKProvider, { config: { apiProjectKey: 'test-project' }, children: _jsx(TestConsumer, { onContext: (ctx) => { capturedContext = ctx; } }) }));
|
|
180
|
+
await waitFor(() => {
|
|
181
|
+
expect(capturedContext?.sdk).not.toBeNull();
|
|
182
|
+
});
|
|
183
|
+
// Set initial authenticated state
|
|
184
|
+
await act(async () => {
|
|
185
|
+
capturedContext.setAuthenticationState({ id: 'temp-user' }, true);
|
|
186
|
+
});
|
|
187
|
+
await waitFor(() => {
|
|
188
|
+
expect(capturedContext?.isAuthenticated).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
// Emit session_restoration_failed
|
|
191
|
+
await act(async () => {
|
|
192
|
+
emitAuthEvent('session_restoration_failed', { error: 'Token expired' });
|
|
193
|
+
});
|
|
194
|
+
await waitFor(() => {
|
|
195
|
+
expect(capturedContext?.isAuthenticated).toBe(false);
|
|
196
|
+
expect(capturedContext?.user).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
it('should NOT respond to AUTH_FAILED code (dead code was removed)', async () => {
|
|
200
|
+
// This test verifies that we no longer check for event.code === 'AUTH_FAILED'
|
|
201
|
+
// which was dead code that never triggered
|
|
202
|
+
let capturedContext = null;
|
|
203
|
+
const mockUser = { id: 'user-stay', email: 'stay@example.com' };
|
|
204
|
+
render(_jsx(PersSDKProvider, { config: { apiProjectKey: 'test-project' }, children: _jsx(TestConsumer, { onContext: (ctx) => { capturedContext = ctx; } }) }));
|
|
205
|
+
await waitFor(() => {
|
|
206
|
+
expect(capturedContext?.sdk).not.toBeNull();
|
|
207
|
+
});
|
|
208
|
+
// Log in first
|
|
209
|
+
await act(async () => {
|
|
210
|
+
emitAuthEvent('login_success', { user: mockUser });
|
|
211
|
+
});
|
|
212
|
+
await waitFor(() => {
|
|
213
|
+
expect(capturedContext?.isAuthenticated).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
// Emit an event with code: 'AUTH_FAILED' - this should NOT clear user
|
|
216
|
+
// because we removed the dead code that checked for this
|
|
217
|
+
await act(async () => {
|
|
218
|
+
if (mockSdkInstance?.events) {
|
|
219
|
+
mockSdkInstance.events.emit({
|
|
220
|
+
type: 'some_other_event',
|
|
221
|
+
code: 'AUTH_FAILED', // This was the dead code path we removed
|
|
222
|
+
domain: 'authentication',
|
|
223
|
+
details: {}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
// User should STILL be authenticated - the code: 'AUTH_FAILED' check is gone
|
|
228
|
+
expect(capturedContext.isAuthenticated).toBe(true);
|
|
229
|
+
expect(capturedContext.user).toEqual(mockUser);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
describe('usePersSDK hook', () => {
|
|
233
|
+
it('should throw error when used outside provider', () => {
|
|
234
|
+
// Suppress the expected error
|
|
235
|
+
const spy = jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
236
|
+
const TestComponent = () => {
|
|
237
|
+
usePersSDK();
|
|
238
|
+
return null;
|
|
239
|
+
};
|
|
240
|
+
expect(() => render(_jsx(TestComponent, {}))).toThrow('usePersSDK must be used within a PersSDKProvider');
|
|
241
|
+
spy.mockRestore();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe('context methods', () => {
|
|
245
|
+
it('should allow manual authentication state updates', async () => {
|
|
246
|
+
let capturedContext = null;
|
|
247
|
+
const mockUser = { id: 'manual-user', email: 'manual@example.com' };
|
|
248
|
+
render(_jsx(PersSDKProvider, { config: { apiProjectKey: 'test-project' }, children: _jsx(TestConsumer, { onContext: (ctx) => { capturedContext = ctx; } }) }));
|
|
249
|
+
await waitFor(() => {
|
|
250
|
+
expect(capturedContext?.isInitialized).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
await act(async () => {
|
|
253
|
+
capturedContext.setAuthenticationState(mockUser, true);
|
|
254
|
+
});
|
|
255
|
+
await waitFor(() => {
|
|
256
|
+
expect(capturedContext?.isAuthenticated).toBe(true);
|
|
257
|
+
expect(capturedContext?.user).toEqual(mockUser);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
it('should refresh user data from SDK', async () => {
|
|
261
|
+
let capturedContext = null;
|
|
262
|
+
render(_jsx(PersSDKProvider, { config: { apiProjectKey: 'test-project' }, children: _jsx(TestConsumer, { onContext: (ctx) => { capturedContext = ctx; } }) }));
|
|
263
|
+
await waitFor(() => {
|
|
264
|
+
expect(capturedContext?.isInitialized).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
await act(async () => {
|
|
267
|
+
await capturedContext.refreshUserData();
|
|
268
|
+
});
|
|
269
|
+
await waitFor(() => {
|
|
270
|
+
expect(capturedContext?.user).toEqual({ id: 'refreshed-user', email: 'refreshed@test.com' });
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
it('should throw error when refreshUserData called before init', async () => {
|
|
274
|
+
let capturedContext = null;
|
|
275
|
+
render(_jsx(PersSDKProvider, { children: _jsx(TestConsumer, { onContext: (ctx) => { capturedContext = ctx; } }) }));
|
|
276
|
+
await waitFor(() => {
|
|
277
|
+
expect(capturedContext).not.toBeNull();
|
|
278
|
+
});
|
|
279
|
+
await expect(capturedContext.refreshUserData()).rejects.toThrow('SDK not initialized');
|
|
280
|
+
});
|
|
281
|
+
it('should throw error when restoreSession called before init', async () => {
|
|
282
|
+
let capturedContext = null;
|
|
283
|
+
render(_jsx(PersSDKProvider, { children: _jsx(TestConsumer, { onContext: (ctx) => { capturedContext = ctx; } }) }));
|
|
284
|
+
await waitFor(() => {
|
|
285
|
+
expect(capturedContext).not.toBeNull();
|
|
286
|
+
});
|
|
287
|
+
await expect(capturedContext.restoreSession()).rejects.toThrow('SDK not initialized');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/__tests__/setup.ts"],"names":[],"mappings":"AAAA,OAAO,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
// Mock React Native modules
|
|
3
|
+
jest.mock('react-native', () => ({
|
|
4
|
+
Platform: {
|
|
5
|
+
OS: 'ios',
|
|
6
|
+
select: jest.fn((obj) => obj.ios || obj.default)
|
|
7
|
+
},
|
|
8
|
+
AppState: {
|
|
9
|
+
addEventListener: jest.fn(() => ({
|
|
10
|
+
remove: jest.fn()
|
|
11
|
+
})),
|
|
12
|
+
currentState: 'active'
|
|
13
|
+
}
|
|
14
|
+
}));
|
|
15
|
+
// Mock AsyncStorage
|
|
16
|
+
jest.mock('@react-native-async-storage/async-storage', () => ({
|
|
17
|
+
default: {
|
|
18
|
+
getItem: jest.fn(),
|
|
19
|
+
setItem: jest.fn(),
|
|
20
|
+
removeItem: jest.fn(),
|
|
21
|
+
clear: jest.fn(),
|
|
22
|
+
getAllKeys: jest.fn(() => Promise.resolve([]))
|
|
23
|
+
}
|
|
24
|
+
}));
|
|
25
|
+
// Silence console errors in tests unless explicitly needed
|
|
26
|
+
const originalConsoleError = console.error;
|
|
27
|
+
const originalConsoleWarn = console.warn;
|
|
28
|
+
beforeAll(() => {
|
|
29
|
+
console.error = jest.fn();
|
|
30
|
+
console.warn = jest.fn();
|
|
31
|
+
});
|
|
32
|
+
afterAll(() => {
|
|
33
|
+
console.error = originalConsoleError;
|
|
34
|
+
console.warn = originalConsoleWarn;
|
|
35
|
+
});
|
|
@@ -30,6 +30,7 @@ export interface GetRedemptionsOptions {
|
|
|
30
30
|
}
|
|
31
31
|
export declare const useRedemptions: () => {
|
|
32
32
|
getRedemptions: (options?: GetRedemptionsOptions) => Promise<PaginatedResponseDTO<RedemptionDTO>>;
|
|
33
|
+
getRedemptionById: (id: string, include?: RedemptionIncludeRelation[]) => Promise<RedemptionDTO>;
|
|
33
34
|
getUserRedemptions: (filters?: {
|
|
34
35
|
redemptionId?: string;
|
|
35
36
|
status?: ProcessRecordStatus;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRedemptions.d.ts","sourceRoot":"","sources":["../../src/hooks/useRedemptions.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0C,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAClG,OAAO,KAAK,EACV,0BAA0B,EAC1B,aAAa,EACb,mBAAmB,EACnB,kCAAkC,EAClC,iBAAiB,EACjB,oBAAoB,EACpB,+BAA+B,EAC/B,yBAAyB,EACzB,mBAAmB,EACnB,cAAc,EACd,SAAS,EACV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,uBAAuB,
|
|
1
|
+
{"version":3,"file":"useRedemptions.d.ts","sourceRoot":"","sources":["../../src/hooks/useRedemptions.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0C,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAClG,OAAO,KAAK,EACV,0BAA0B,EAC1B,aAAa,EACb,mBAAmB,EACnB,kCAAkC,EAClC,iBAAiB,EACjB,oBAAoB,EACpB,+BAA+B,EAC/B,yBAAyB,EACzB,mBAAmB,EACnB,cAAc,EACd,SAAS,EACV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAG7D,YAAY,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAEvG;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,oFAAoF;IACpF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACzB,wDAAwD;IACxD,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAC1B,uDAAuD;IACvD,OAAO,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IACxB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,yBAAyB,EAAE,CAAC;IACtC,8BAA8B;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uBAAuB;IACvB,MAAM,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAAC;IAC5C,qBAAqB;IACrB,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED,eAAO,MAAM,cAAc;+BAqD2B,qBAAqB,KAAG,QAAQ,qBAAqB,aAAa,CAAC,CAAC;4BAkUlH,MAAM,YACA,yBAAyB,EAAE,KACpC,QAAQ,aAAa,CAAC;mCA3Rb;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,mBAAmB,CAAA;KAAE,YACvD,iBAAiB,YACjB,+BAA+B,EAAE,KAC1C,QAAQ,qBAAqB,mBAAmB,CAAC,CAAC;2BAiGrC,MAAM,YACV;QAAE,OAAO,CAAC,EAAE,cAAc,CAAC;QAAC,cAAc,CAAC,EAAE,gBAAgB,CAAA;KAAE,KACxE,QAAQ,kCAAkC,CAAC;8BAmHG,QAAQ,qBAAqB,iBAAiB,CAAC,CAAC;qCA9BrF,uBAAuB,YACvB,+BAA+B,EAAE,KAC1C,QAAQ,qBAAqB,mBAAmB,CAAC,CAAC;uCAcO,0BAA0B,KAAG,QAAQ,aAAa,CAAC;qCA4BrD,MAAM,kBAAkB,0BAA0B,KAAG,QAAQ,aAAa,CAAC;2CAcrE,MAAM,KAAG,QAAQ,aAAa,CAAC;;;;CAmEhG,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC"}
|
|
@@ -112,6 +112,25 @@ export const useRedemptions = () => {
|
|
|
112
112
|
/**
|
|
113
113
|
* Redeem a redemption offer with optional dynamic context
|
|
114
114
|
*
|
|
115
|
+
* ## When to use `redeem()` vs `createTransaction()`
|
|
116
|
+
*
|
|
117
|
+
* | Use Case | Method | Auto-Signs |
|
|
118
|
+
* |----------|--------|------------|
|
|
119
|
+
* | **Voucher redemption** | `redeem()` | ✅ Yes |
|
|
120
|
+
* | **Reward redemption** | `redeem()` | ✅ Yes |
|
|
121
|
+
* | **Direct token transfer** | `createTransaction()` | ⚠️ Depends |
|
|
122
|
+
* | **POS burn with QR** | `buildPOSBurnRequest()` | ❌ Manual |
|
|
123
|
+
*
|
|
124
|
+
* **Use `redeem()` for voucher/reward redemptions.** It automatically:
|
|
125
|
+
* 1. Creates redemption record
|
|
126
|
+
* 2. Triggers passkey/biometric prompt
|
|
127
|
+
* 3. Signs the transaction
|
|
128
|
+
* 4. Submits to blockchain
|
|
129
|
+
* 5. Returns result with `transactionHash`
|
|
130
|
+
*
|
|
131
|
+
* **No admin approval required** - The "PENDING" state is brief (seconds).
|
|
132
|
+
* The user's passkey signature IS the approval.
|
|
133
|
+
*
|
|
115
134
|
* Supports dynamic context for personalized NFT generation when the redemption
|
|
116
135
|
* issues ERC721 tokens. Context can include validity dates, AI prompt placeholders,
|
|
117
136
|
* and custom metadata.
|
|
@@ -120,11 +139,21 @@ export const useRedemptions = () => {
|
|
|
120
139
|
* @param options - Optional settings
|
|
121
140
|
* @param options.context - Dynamic context for NFT personalization (validity dates, AI prompts)
|
|
122
141
|
* @param options.onStatusUpdate - Callback for transaction signing status updates
|
|
123
|
-
* @returns Promise resolving to redemption result
|
|
142
|
+
* @returns Promise resolving to redemption result with `transactionHash` on success
|
|
124
143
|
*
|
|
125
|
-
* @example Basic redemption
|
|
144
|
+
* @example Basic redemption (most common)
|
|
126
145
|
* ```typescript
|
|
127
|
-
* const
|
|
146
|
+
* const { redeem } = useRedemptions();
|
|
147
|
+
*
|
|
148
|
+
* const result = await redeem('redemption-123', {
|
|
149
|
+
* onStatusUpdate: (status, message) => {
|
|
150
|
+
* console.log(`Status: ${status} - ${message}`);
|
|
151
|
+
* }
|
|
152
|
+
* });
|
|
153
|
+
*
|
|
154
|
+
* if (result.transactionHash) {
|
|
155
|
+
* console.log('Redemption complete!', result.transactionHash);
|
|
156
|
+
* }
|
|
128
157
|
* ```
|
|
129
158
|
*
|
|
130
159
|
* @example Redemption with dynamic context (personalized NFT)
|
|
@@ -155,6 +184,8 @@ export const useRedemptions = () => {
|
|
|
155
184
|
* }
|
|
156
185
|
* });
|
|
157
186
|
* ```
|
|
187
|
+
*
|
|
188
|
+
* @see {@link useTransactions.createTransaction | createTransaction()} for direct transfers without redemption flow
|
|
158
189
|
*/
|
|
159
190
|
const redeem = useCallback(async (redemptionId, options) => {
|
|
160
191
|
if (!isInitialized || !sdk) {
|
|
@@ -304,8 +335,43 @@ export const useRedemptions = () => {
|
|
|
304
335
|
throw error;
|
|
305
336
|
}
|
|
306
337
|
}, [sdk, isInitialized]);
|
|
338
|
+
/**
|
|
339
|
+
* Get a specific redemption by ID
|
|
340
|
+
*
|
|
341
|
+
* Retrieves detailed information about a specific redemption offer.
|
|
342
|
+
*
|
|
343
|
+
* @param id - Redemption UUID
|
|
344
|
+
* @param include - Optional relations to include: 'redeemCount'
|
|
345
|
+
* @returns Promise resolving to the redemption
|
|
346
|
+
*
|
|
347
|
+
* @example Basic usage
|
|
348
|
+
* ```typescript
|
|
349
|
+
* const redemption = await getRedemptionById('7baf4d39-0bd6-45ca-9c66-8c0d655767c3');
|
|
350
|
+
* console.log(redemption.name);
|
|
351
|
+
* ```
|
|
352
|
+
*
|
|
353
|
+
* @example With redeem count
|
|
354
|
+
* ```typescript
|
|
355
|
+
* const redemption = await getRedemptionById('redemption-123', ['redeemCount']);
|
|
356
|
+
* console.log(`${redemption.name} has been redeemed ${redemption.redeemCount} times`);
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
359
|
+
const getRedemptionById = useCallback(async (id, include) => {
|
|
360
|
+
if (!isInitialized || !sdk) {
|
|
361
|
+
throw new Error('SDK not initialized. Call initialize() first.');
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
const result = await sdk.redemptions.getRedemptionById(id, include);
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
console.error('Failed to fetch redemption by ID:', error);
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}, [sdk, isInitialized]);
|
|
307
372
|
return {
|
|
308
373
|
getRedemptions,
|
|
374
|
+
getRedemptionById,
|
|
309
375
|
getUserRedemptions,
|
|
310
376
|
redeem,
|
|
311
377
|
getRedemptionTypes,
|
|
@@ -6,9 +6,42 @@ import type { TokenBalance } from '@explorins/pers-sdk/web3';
|
|
|
6
6
|
* This composite type combines blockchain balance data with token metadata,
|
|
7
7
|
* making it easy to display token information in UI components without
|
|
8
8
|
* needing to join data from multiple sources.
|
|
9
|
+
*
|
|
10
|
+
* ## CRITICAL: Token ID for Transactions
|
|
11
|
+
*
|
|
12
|
+
* For ERC721 (stamps) and ERC1155 (rewards), use `tokenBalance.tokenId` as the
|
|
13
|
+
* `contractTokenId` when creating transactions. This is the **on-chain token ID**
|
|
14
|
+
* assigned by the blockchain, NOT the metadata definition ID.
|
|
15
|
+
*
|
|
16
|
+
* | Token Type | tokenId meaning | Example |
|
|
17
|
+
* |------------|-----------------|---------|
|
|
18
|
+
* | ERC20 | `null` or `""` | N/A (fungible points) |
|
|
19
|
+
* | ERC721 | NFT instance ID | `"46"` (on-chain ID) |
|
|
20
|
+
* | ERC1155 | Token type ID | `"16"` (semi-fungible type) |
|
|
21
|
+
*
|
|
22
|
+
* @example Correct usage for token burn transaction
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const stampBalance = tokenBalances.find(b => b.token.type === 'ERC721');
|
|
25
|
+
*
|
|
26
|
+
* const request = buildPOSBurnRequest({
|
|
27
|
+
* amount: 1,
|
|
28
|
+
* contractAddress: stampBalance.token.contractAddress,
|
|
29
|
+
* contractTokenId: stampBalance.tokenBalance.tokenId, // ✅ Correct
|
|
30
|
+
* chainId: stampBalance.token.chainId,
|
|
31
|
+
* userId: userId,
|
|
32
|
+
* businessId: businessId
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @see {@link https://docs.explorins.com/pers-sdk/troubleshooting#stamp-voucher-transaction-fails | Troubleshooting: Stamp Transaction Fails}
|
|
9
37
|
*/
|
|
10
38
|
export interface TokenBalanceWithToken {
|
|
11
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* The balance data from blockchain
|
|
41
|
+
*
|
|
42
|
+
* **Important:** For NFT operations, use `tokenBalance.tokenId` as the `contractTokenId`
|
|
43
|
+
* in transaction requests. This is the actual on-chain ID, not the metadata definition.
|
|
44
|
+
*/
|
|
12
45
|
tokenBalance: TokenBalance;
|
|
13
46
|
/** The token definition (contract, symbol, metadata, etc.) */
|
|
14
47
|
token: TokenDTO;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTokenBalances.d.ts","sourceRoot":"","sources":["../../src/hooks/useTokenBalances.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,0BAA0B,CAAC;AAElF
|
|
1
|
+
{"version":3,"file":"useTokenBalances.d.ts","sourceRoot":"","sources":["../../src/hooks/useTokenBalances.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,0BAA0B,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,YAAY,EAAE,YAAY,CAAC;IAC3B,8DAA8D;IAC9D,KAAK,EAAE,QAAQ,CAAC;IAChB,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,eAAe,CAAC,EAAE,QAAQ,EAAE,CAAC;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,0DAA0D;IAC1D,aAAa,EAAE,qBAAqB,EAAE,CAAC;IACvC,6CAA6C;IAC7C,SAAS,EAAE,OAAO,CAAC;IACnB,sCAAsC;IACtC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,uCAAuC;IACvC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,sFAAsF;IACtF,WAAW,EAAE,OAAO,CAAC;IACrB,0CAA0C;IAC1C,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8FG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,sBAAsB,CAqLzF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TokenDTO, PaginatedResponseDTO } from '@explorins/pers-sdk';
|
|
1
|
+
import type { TokenDTO, TokenMetadataDTO, PaginatedResponseDTO } from '@explorins/pers-sdk';
|
|
2
2
|
/**
|
|
3
3
|
* React hook for token operations in the PERS SDK
|
|
4
4
|
*
|
|
@@ -34,6 +34,9 @@ export declare const useTokens: () => {
|
|
|
34
34
|
getTokenTypes: () => Promise<any>;
|
|
35
35
|
getStatusTokens: () => Promise<PaginatedResponseDTO<TokenDTO>>;
|
|
36
36
|
getTokenByContract: (contractAddress: string, contractTokenId?: string | null) => Promise<TokenDTO>;
|
|
37
|
+
getTokenMetadata: (options?: any) => Promise<PaginatedResponseDTO<TokenMetadataDTO>>;
|
|
38
|
+
getRewards: (options?: any) => Promise<PaginatedResponseDTO<TokenMetadataDTO>>;
|
|
39
|
+
getStamps: (options?: any) => Promise<PaginatedResponseDTO<TokenMetadataDTO>>;
|
|
37
40
|
isAvailable: boolean;
|
|
38
41
|
};
|
|
39
42
|
export type TokenHook = ReturnType<typeof useTokens>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTokens.d.ts","sourceRoot":"","sources":["../../src/hooks/useTokens.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"useTokens.d.ts","sourceRoot":"","sources":["../../src/hooks/useTokens.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,oBAAoB,EAAyE,MAAM,qBAAqB,CAAC;AAEnK;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,SAAS;qBAoBoB,QAAQ,qBAAqB,QAAQ,CAAC,CAAC;gCA2B5B,QAAQ,QAAQ,CAAC;2BA2BtB,QAAQ,qBAAqB,QAAQ,CAAC,CAAC;yBA2BzC,QAAQ,GAAG,CAAC;2BA2BV,QAAQ,qBAAqB,QAAQ,CAAC,CAAC;0CA6BtB,MAAM,oBAAoB,MAAM,GAAG,IAAI,KAAG,QAAQ,QAAQ,CAAC;yCAkCvC,QAAQ,qBAAqB,gBAAgB,CAAC,CAAC;mCA+B3D,QAAQ,qBAAqB,gBAAgB,CAAC,CAAC;kCA+BjD,QAAQ,qBAAqB,gBAAgB,CAAC,CAAC;;CA0BrH,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC"}
|