@stamprally/react 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -84
- package/dist/index.cjs +13 -608
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -73
- package/dist/index.d.ts +10 -73
- package/dist/index.js +15 -608
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1,629 +1,34 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var core = require('@stamprally/core');
|
|
4
3
|
var react = require('react');
|
|
5
4
|
|
|
6
5
|
// src/useStampRally.ts
|
|
7
|
-
function getServerSnapshot() {
|
|
8
|
-
return null;
|
|
9
|
-
}
|
|
10
|
-
function toError(error) {
|
|
11
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
12
|
-
}
|
|
13
|
-
function isObject(value) {
|
|
14
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
-
}
|
|
16
|
-
function applyOptimisticAcquire(currentState, action) {
|
|
17
|
-
if (currentState === null || currentState.records.some((record) => record.stampId === action.stampId)) {
|
|
18
|
-
return currentState;
|
|
19
|
-
}
|
|
20
|
-
return {
|
|
21
|
-
...currentState,
|
|
22
|
-
records: [...currentState.records, { stampId: action.stampId, acquiredAt: action.acquiredAt }],
|
|
23
|
-
updatedAt: action.acquiredAt
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
function createIdempotencyKey() {
|
|
27
|
-
const cryptoApi = globalThis.crypto;
|
|
28
|
-
if (cryptoApi !== void 0 && typeof cryptoApi.randomUUID === "function") {
|
|
29
|
-
return cryptoApi.randomUUID();
|
|
30
|
-
}
|
|
31
|
-
return `stamp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
32
|
-
}
|
|
33
|
-
function isOnline(adapter) {
|
|
34
|
-
if (adapter?.isOnline === void 0) {
|
|
35
|
-
return typeof navigator === "undefined" || navigator.onLine !== false;
|
|
36
|
-
}
|
|
37
|
-
return typeof adapter.isOnline === "function" ? adapter.isOnline() : adapter.isOnline;
|
|
38
|
-
}
|
|
39
|
-
function isRejectedVerification(value) {
|
|
40
|
-
return value === false || typeof value === "object" && value !== null && "ok" in value && value.ok === false;
|
|
41
|
-
}
|
|
42
|
-
function isNetworkFailure(error) {
|
|
43
|
-
if (typeof navigator !== "undefined" && navigator.onLine === false) return true;
|
|
44
|
-
if (!(error instanceof Error)) return true;
|
|
45
|
-
return /aborted|connection|fetch|network|offline|timeout/i.test(`${error.name} ${error.message}`);
|
|
46
|
-
}
|
|
47
|
-
function notifyAcquisitionEvents(result, events, onStampClaimed, onRewardUnlocked) {
|
|
48
|
-
for (const event of result.events) {
|
|
49
|
-
if (event.type === "stampAcquired") {
|
|
50
|
-
(events?.onStampClaimed ?? onStampClaimed)?.(event.record);
|
|
51
|
-
}
|
|
52
|
-
if (event.type === "rewardUnlocked") {
|
|
53
|
-
(events?.onRewardUnlocked ?? onRewardUnlocked)?.(event.rewardId);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
function useStampRally(client, options = {}) {
|
|
58
|
-
const syncAdapter = options.syncAdapter;
|
|
59
|
-
const events = options.events;
|
|
60
|
-
const [offlineQueue, setOfflineQueue] = react.useState([]);
|
|
61
|
-
const queuedMetadata = react.useRef(/* @__PURE__ */ new Map());
|
|
62
|
-
const isFlushingQueue = react.useRef(false);
|
|
63
|
-
const activeClient = react.useRef(client);
|
|
64
|
-
const subscribe = react.useCallback(
|
|
65
|
-
(onStoreChange) => client.subscribe(() => onStoreChange()),
|
|
66
|
-
[client]
|
|
67
|
-
);
|
|
68
|
-
const getSnapshot = react.useCallback(() => client.getState(), [client]);
|
|
69
|
-
const rawState = react.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
70
|
-
const [clientStatus, setClientStatus] = react.useState(() => ({
|
|
71
|
-
client,
|
|
72
|
-
isInitializing: client.getState() === null
|
|
73
|
-
}));
|
|
74
|
-
const [clientError, setClientError] = react.useState(null);
|
|
75
|
-
const [isPending] = react.useTransition();
|
|
76
|
-
const [isOperationPending, setIsOperationPending] = react.useState(false);
|
|
77
|
-
const [optimisticState, setOptimisticState] = react.useState(rawState);
|
|
78
|
-
react.useEffect(() => {
|
|
79
|
-
setOptimisticState(rawState);
|
|
80
|
-
}, [rawState]);
|
|
81
|
-
react.useEffect(() => {
|
|
82
|
-
if (activeClient.current === client) return;
|
|
83
|
-
activeClient.current = client;
|
|
84
|
-
setOfflineQueue([]);
|
|
85
|
-
queuedMetadata.current.clear();
|
|
86
|
-
}, [client]);
|
|
87
|
-
react.useEffect(() => {
|
|
88
|
-
if (syncAdapter?.onStateChange === void 0) return;
|
|
89
|
-
return client.subscribe(syncAdapter.onStateChange);
|
|
90
|
-
}, [client, syncAdapter]);
|
|
91
|
-
react.useEffect(() => {
|
|
92
|
-
let active = true;
|
|
93
|
-
setClientError(null);
|
|
94
|
-
if (rawState !== null) {
|
|
95
|
-
setClientStatus({ client, isInitializing: false });
|
|
96
|
-
return () => {
|
|
97
|
-
active = false;
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
setClientStatus({ client, isInitializing: true });
|
|
101
|
-
void client.init().catch((initializationError) => {
|
|
102
|
-
if (active) {
|
|
103
|
-
setClientError({ client, value: toError(initializationError) });
|
|
104
|
-
}
|
|
105
|
-
}).finally(() => {
|
|
106
|
-
if (active) {
|
|
107
|
-
setClientStatus({ client, isInitializing: false });
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
return () => {
|
|
111
|
-
active = false;
|
|
112
|
-
};
|
|
113
|
-
}, [client, rawState]);
|
|
114
|
-
const queueRequest = react.useCallback(
|
|
115
|
-
(request, metadata) => {
|
|
116
|
-
if (metadata !== void 0) queuedMetadata.current.set(request.idempotencyKey, metadata);
|
|
117
|
-
setOfflineQueue(
|
|
118
|
-
(current) => current.some((item) => item.idempotencyKey === request.idempotencyKey) ? current : [...current, request]
|
|
119
|
-
);
|
|
120
|
-
},
|
|
121
|
-
[]
|
|
122
|
-
);
|
|
123
|
-
const acquire = react.useCallback(
|
|
124
|
-
(stampId, context, now, idempotencyKey) => {
|
|
125
|
-
const acquiredAt = now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
126
|
-
const request = {
|
|
127
|
-
stampId,
|
|
128
|
-
context,
|
|
129
|
-
now: acquiredAt,
|
|
130
|
-
idempotencyKey: idempotencyKey ?? createIdempotencyKey()
|
|
131
|
-
};
|
|
132
|
-
setClientError(null);
|
|
133
|
-
if (!isOnline(syncAdapter)) {
|
|
134
|
-
queueRequest(request);
|
|
135
|
-
const queued = {
|
|
136
|
-
ok: false,
|
|
137
|
-
error: { code: "OFFLINE_QUEUED", stampId, idempotencyKey: request.idempotencyKey }
|
|
138
|
-
};
|
|
139
|
-
setClientError({ client, value: queued.error });
|
|
140
|
-
return Promise.resolve(queued);
|
|
141
|
-
}
|
|
142
|
-
setIsOperationPending(true);
|
|
143
|
-
setOptimisticState((current) => applyOptimisticAcquire(current, { stampId, acquiredAt }));
|
|
144
|
-
return new Promise((resolve, reject) => {
|
|
145
|
-
void (async () => {
|
|
146
|
-
try {
|
|
147
|
-
const before = await syncAdapter?.onBeforeCheckIn?.(request);
|
|
148
|
-
if (before === false) {
|
|
149
|
-
const rejected = {
|
|
150
|
-
ok: false,
|
|
151
|
-
error: { code: "INVALID_PROOF", stampId }
|
|
152
|
-
};
|
|
153
|
-
setClientError({ client, value: rejected.error });
|
|
154
|
-
setIsOperationPending(false);
|
|
155
|
-
resolve(rejected);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
const previousState = client.getState();
|
|
159
|
-
const result = await client.acquire(stampId, context, acquiredAt);
|
|
160
|
-
if (result.ok) {
|
|
161
|
-
let verified;
|
|
162
|
-
try {
|
|
163
|
-
verified = await syncAdapter?.onServerVerify?.(request);
|
|
164
|
-
} catch (verificationError) {
|
|
165
|
-
if (!isNetworkFailure(verificationError)) throw verificationError;
|
|
166
|
-
queueRequest(request, { previousState, result: result.value });
|
|
167
|
-
const queued = {
|
|
168
|
-
ok: false,
|
|
169
|
-
error: {
|
|
170
|
-
code: "OFFLINE_QUEUED",
|
|
171
|
-
stampId,
|
|
172
|
-
idempotencyKey: request.idempotencyKey
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
setClientError({ client, value: toError(verificationError) });
|
|
176
|
-
setOptimisticState(client.getState());
|
|
177
|
-
setIsOperationPending(false);
|
|
178
|
-
resolve(queued);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
if (isRejectedVerification(verified)) {
|
|
182
|
-
await client.restore(rawState ?? client.getState() ?? result.value.nextState);
|
|
183
|
-
const rejected = {
|
|
184
|
-
ok: false,
|
|
185
|
-
error: { code: "INVALID_PROOF", stampId }
|
|
186
|
-
};
|
|
187
|
-
setClientError({ client, value: rejected.error });
|
|
188
|
-
setOptimisticState(client.getState());
|
|
189
|
-
setIsOperationPending(false);
|
|
190
|
-
resolve(rejected);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
notifyAcquisitionEvents(
|
|
194
|
-
result.value,
|
|
195
|
-
events,
|
|
196
|
-
options.onStampClaimed,
|
|
197
|
-
options.onRewardUnlocked
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
if (!result.ok) {
|
|
201
|
-
setClientError({ client, value: result.error });
|
|
202
|
-
setOptimisticState(client.getState());
|
|
203
|
-
}
|
|
204
|
-
setIsOperationPending(false);
|
|
205
|
-
resolve(result);
|
|
206
|
-
} catch (acquireError) {
|
|
207
|
-
if (isNetworkFailure(acquireError)) {
|
|
208
|
-
queueRequest(request);
|
|
209
|
-
const queued = {
|
|
210
|
-
ok: false,
|
|
211
|
-
error: { code: "OFFLINE_QUEUED", stampId, idempotencyKey: request.idempotencyKey }
|
|
212
|
-
};
|
|
213
|
-
setClientError({ client, value: queued.error });
|
|
214
|
-
setOptimisticState(client.getState());
|
|
215
|
-
setIsOperationPending(false);
|
|
216
|
-
resolve(queued);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
const normalizedError = toError(acquireError);
|
|
220
|
-
setClientError({ client, value: normalizedError });
|
|
221
|
-
setOptimisticState(client.getState());
|
|
222
|
-
setIsOperationPending(false);
|
|
223
|
-
reject(normalizedError);
|
|
224
|
-
}
|
|
225
|
-
})();
|
|
226
|
-
});
|
|
227
|
-
},
|
|
228
|
-
[
|
|
229
|
-
client,
|
|
230
|
-
events,
|
|
231
|
-
options.onRewardUnlocked,
|
|
232
|
-
options.onStampClaimed,
|
|
233
|
-
queueRequest,
|
|
234
|
-
rawState,
|
|
235
|
-
syncAdapter
|
|
236
|
-
]
|
|
237
|
-
);
|
|
238
|
-
const reset = react.useCallback(
|
|
239
|
-
(now) => {
|
|
240
|
-
setClientError(null);
|
|
241
|
-
setIsOperationPending(true);
|
|
242
|
-
return new Promise((resolve, reject) => {
|
|
243
|
-
void (async () => {
|
|
244
|
-
try {
|
|
245
|
-
const nextState = now === void 0 ? await client.reset() : await client.reset(now);
|
|
246
|
-
setIsOperationPending(false);
|
|
247
|
-
resolve(nextState);
|
|
248
|
-
} catch (resetError) {
|
|
249
|
-
const normalizedError = toError(resetError);
|
|
250
|
-
setClientError({ client, value: normalizedError });
|
|
251
|
-
setIsOperationPending(false);
|
|
252
|
-
reject(normalizedError);
|
|
253
|
-
}
|
|
254
|
-
})();
|
|
255
|
-
});
|
|
256
|
-
},
|
|
257
|
-
[client]
|
|
258
|
-
);
|
|
259
|
-
const redeem = react.useCallback(
|
|
260
|
-
(rewardId, redeemOptions = {}) => {
|
|
261
|
-
setClientError(null);
|
|
262
|
-
setIsOperationPending(true);
|
|
263
|
-
return new Promise((resolve, reject) => {
|
|
264
|
-
void (async () => {
|
|
265
|
-
const reward = client.getConfig().rewards?.find((item) => item.id === rewardId);
|
|
266
|
-
if (reward === void 0) {
|
|
267
|
-
const result2 = {
|
|
268
|
-
ok: false,
|
|
269
|
-
error: { code: "REWARD_NOT_FOUND", rewardId }
|
|
270
|
-
};
|
|
271
|
-
setClientError({ client, value: result2.error });
|
|
272
|
-
setIsOperationPending(false);
|
|
273
|
-
resolve(result2);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
const currentState = client.getState();
|
|
277
|
-
const currentRewardState = currentState?.rewards?.find(
|
|
278
|
-
(state) => state.rewardId === rewardId
|
|
279
|
-
);
|
|
280
|
-
if (currentState === null || currentRewardState === void 0) {
|
|
281
|
-
const result2 = {
|
|
282
|
-
ok: false,
|
|
283
|
-
error: { code: "NOT_AVAILABLE", rewardId }
|
|
284
|
-
};
|
|
285
|
-
setClientError({ client, value: result2.error });
|
|
286
|
-
setIsOperationPending(false);
|
|
287
|
-
resolve(result2);
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
const result = core.consumeReward({
|
|
291
|
-
reward,
|
|
292
|
-
currentState: currentRewardState,
|
|
293
|
-
now: (/* @__PURE__ */ new Date()).toISOString(),
|
|
294
|
-
...redeemOptions.passcode === void 0 ? {} : { inputPasscode: redeemOptions.passcode },
|
|
295
|
-
...redeemOptions.staffId === void 0 ? {} : { staffId: redeemOptions.staffId }
|
|
296
|
-
});
|
|
297
|
-
if (!result.ok) {
|
|
298
|
-
setClientError({ client, value: result.error });
|
|
299
|
-
setIsOperationPending(false);
|
|
300
|
-
resolve(result);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
if (result.value === currentRewardState) {
|
|
304
|
-
setIsOperationPending(false);
|
|
305
|
-
resolve(result);
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
const nextState = {
|
|
309
|
-
...currentState,
|
|
310
|
-
rewards: (currentState.rewards ?? []).map(
|
|
311
|
-
(state) => state.rewardId === rewardId ? result.value : state
|
|
312
|
-
),
|
|
313
|
-
updatedAt: result.value.consumedAt ?? currentState.updatedAt
|
|
314
|
-
};
|
|
315
|
-
try {
|
|
316
|
-
await client.restore(nextState);
|
|
317
|
-
(events?.onRewardConsumed ?? options.onRewardConsumed)?.(rewardId);
|
|
318
|
-
setIsOperationPending(false);
|
|
319
|
-
resolve(result);
|
|
320
|
-
} catch (redeemError) {
|
|
321
|
-
const normalizedError = toError(redeemError);
|
|
322
|
-
setClientError({ client, value: normalizedError });
|
|
323
|
-
setIsOperationPending(false);
|
|
324
|
-
reject(normalizedError);
|
|
325
|
-
}
|
|
326
|
-
})();
|
|
327
|
-
});
|
|
328
|
-
},
|
|
329
|
-
[client, events, options.onRewardConsumed]
|
|
330
|
-
);
|
|
331
|
-
const exportRecoveryCode = react.useCallback(() => {
|
|
332
|
-
const state = client.getState();
|
|
333
|
-
if (state === null) {
|
|
334
|
-
throw new Error("Cannot export recovery code before the rally is initialized.");
|
|
335
|
-
}
|
|
336
|
-
return core.exportProgressToken({
|
|
337
|
-
version: 1,
|
|
338
|
-
rallyId: state.rallyId,
|
|
339
|
-
stamps: state.records,
|
|
340
|
-
rewards: state.rewards ?? [],
|
|
341
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
342
|
-
});
|
|
343
|
-
}, [client]);
|
|
344
|
-
const importRecoveryCode = react.useCallback(
|
|
345
|
-
(token) => {
|
|
346
|
-
setClientError(null);
|
|
347
|
-
setIsOperationPending(true);
|
|
348
|
-
return new Promise((resolve, reject) => {
|
|
349
|
-
void (async () => {
|
|
350
|
-
const config = client.getConfig();
|
|
351
|
-
const snapshot = core.importProgressToken(token, config.id);
|
|
352
|
-
if (snapshot === null) {
|
|
353
|
-
setIsOperationPending(false);
|
|
354
|
-
resolve(false);
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
const stampIds = new Set(config.stamps.map((stamp) => stamp.id));
|
|
358
|
-
const rewardIds = new Set((config.rewards ?? []).map((reward) => reward.id));
|
|
359
|
-
const importedStampIds = /* @__PURE__ */ new Set();
|
|
360
|
-
const importedRewardIds = /* @__PURE__ */ new Set();
|
|
361
|
-
const stamps = snapshot.stamps.filter((record) => {
|
|
362
|
-
if (!stampIds.has(record.stampId) || importedStampIds.has(record.stampId)) return false;
|
|
363
|
-
importedStampIds.add(record.stampId);
|
|
364
|
-
return true;
|
|
365
|
-
});
|
|
366
|
-
const rewards = snapshot.rewards.filter((state) => {
|
|
367
|
-
if (!rewardIds.has(state.rewardId) || importedRewardIds.has(state.rewardId))
|
|
368
|
-
return false;
|
|
369
|
-
importedRewardIds.add(state.rewardId);
|
|
370
|
-
return true;
|
|
371
|
-
});
|
|
372
|
-
try {
|
|
373
|
-
await client.restore({
|
|
374
|
-
rallyId: config.id,
|
|
375
|
-
records: stamps,
|
|
376
|
-
...config.rewards === void 0 && rewards.length === 0 ? {} : { rewards },
|
|
377
|
-
updatedAt: snapshot.exportedAt
|
|
378
|
-
});
|
|
379
|
-
setIsOperationPending(false);
|
|
380
|
-
resolve(true);
|
|
381
|
-
} catch (importError) {
|
|
382
|
-
const normalizedError = toError(importError);
|
|
383
|
-
setClientError({ client, value: normalizedError });
|
|
384
|
-
setIsOperationPending(false);
|
|
385
|
-
reject(normalizedError);
|
|
386
|
-
}
|
|
387
|
-
})();
|
|
388
|
-
});
|
|
389
|
-
},
|
|
390
|
-
[client]
|
|
391
|
-
);
|
|
392
|
-
const exportRecoveryToken = react.useCallback(
|
|
393
|
-
async (secretKey, tokenOptions = {}) => {
|
|
394
|
-
const state = client.getState();
|
|
395
|
-
if (state === null)
|
|
396
|
-
throw new Error("Cannot export recovery token before the rally is initialized.");
|
|
397
|
-
return core.createSecureToken(
|
|
398
|
-
{
|
|
399
|
-
type: "recovery",
|
|
400
|
-
rallyId: state.rallyId,
|
|
401
|
-
state,
|
|
402
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
403
|
-
},
|
|
404
|
-
secretKey,
|
|
405
|
-
{ encrypt: tokenOptions.encrypt ?? true, ...tokenOptions }
|
|
406
|
-
);
|
|
407
|
-
},
|
|
408
|
-
[client]
|
|
409
|
-
);
|
|
410
|
-
const importRecoveryToken = react.useCallback(
|
|
411
|
-
async (token, secretKey) => {
|
|
412
|
-
setClientError(null);
|
|
413
|
-
setIsOperationPending(true);
|
|
414
|
-
try {
|
|
415
|
-
const verified = await core.verifySecureToken(token, secretKey);
|
|
416
|
-
if (!verified.ok || verified.payload.type !== "recovery" || verified.payload.rallyId !== client.getConfig().id) {
|
|
417
|
-
setIsOperationPending(false);
|
|
418
|
-
return false;
|
|
419
|
-
}
|
|
420
|
-
const candidate = verified.payload.state;
|
|
421
|
-
if (!core.isStampRallyState(candidate)) {
|
|
422
|
-
setIsOperationPending(false);
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
await client.restore(candidate);
|
|
426
|
-
setIsOperationPending(false);
|
|
427
|
-
return true;
|
|
428
|
-
} catch (error2) {
|
|
429
|
-
setClientError({ client, value: toError(error2) });
|
|
430
|
-
setIsOperationPending(false);
|
|
431
|
-
return false;
|
|
432
|
-
}
|
|
433
|
-
},
|
|
434
|
-
[client]
|
|
435
|
-
);
|
|
436
|
-
const syncWithServer = react.useCallback(
|
|
437
|
-
async (serverEndpoint, authHeader) => {
|
|
438
|
-
if (typeof fetch !== "function")
|
|
439
|
-
throw new Error("Fetch API is unavailable in this environment.");
|
|
440
|
-
setClientError(null);
|
|
441
|
-
setIsOperationPending(true);
|
|
442
|
-
try {
|
|
443
|
-
const endpoint = serverEndpoint.replace(/\/$/u, "").endsWith("/sync") ? serverEndpoint : `${serverEndpoint.replace(/\/$/u, "")}/api/sync`;
|
|
444
|
-
const response = await fetch(endpoint, {
|
|
445
|
-
method: "POST",
|
|
446
|
-
headers: {
|
|
447
|
-
"content-type": "application/json",
|
|
448
|
-
...authHeader === void 0 ? {} : { authorization: authHeader }
|
|
449
|
-
},
|
|
450
|
-
body: JSON.stringify({
|
|
451
|
-
userId: options.serverUserId ?? "anonymous",
|
|
452
|
-
queue: offlineQueue.map((request) => ({
|
|
453
|
-
...request,
|
|
454
|
-
userId: options.serverUserId ?? "anonymous",
|
|
455
|
-
spotId: request.stampId,
|
|
456
|
-
claimMethod: request.context.type,
|
|
457
|
-
proofData: request.context.type === "token" ? { token: request.context.token } : request.context.type === "geo" ? {
|
|
458
|
-
latitude: request.context.currentLatitude,
|
|
459
|
-
longitude: request.context.currentLongitude
|
|
460
|
-
} : void 0
|
|
461
|
-
}))
|
|
462
|
-
})
|
|
463
|
-
});
|
|
464
|
-
const payload = await response.json();
|
|
465
|
-
if (!response.ok || !isObject(payload) || payload.ok !== true || !core.isStampRallyState(payload.state)) {
|
|
466
|
-
throw new Error("Server synchronization was rejected.");
|
|
467
|
-
}
|
|
468
|
-
await client.restore(payload.state);
|
|
469
|
-
queuedMetadata.current.clear();
|
|
470
|
-
setOfflineQueue([]);
|
|
471
|
-
client.notifySyncCompleted(payload.state);
|
|
472
|
-
} catch (error2) {
|
|
473
|
-
setClientError({ client, value: toError(error2) });
|
|
474
|
-
throw error2;
|
|
475
|
-
} finally {
|
|
476
|
-
setIsOperationPending(false);
|
|
477
|
-
}
|
|
478
|
-
},
|
|
479
|
-
[client, offlineQueue, options.serverUserId]
|
|
480
|
-
);
|
|
481
|
-
const isLoading = rawState === null && (clientStatus.client !== client || clientStatus.isInitializing);
|
|
482
|
-
const error = clientError?.client === client ? clientError.value : null;
|
|
483
|
-
const flushQueue = react.useCallback(async () => {
|
|
484
|
-
if (!isOnline(syncAdapter) || isFlushingQueue.current) return;
|
|
485
|
-
isFlushingQueue.current = true;
|
|
486
|
-
setIsOperationPending(true);
|
|
487
|
-
try {
|
|
488
|
-
for (const request of offlineQueue) {
|
|
489
|
-
if (!isOnline(syncAdapter)) break;
|
|
490
|
-
const metadata = queuedMetadata.current.get(request.idempotencyKey);
|
|
491
|
-
const previousState = metadata?.previousState ?? client.getState();
|
|
492
|
-
try {
|
|
493
|
-
const before = await syncAdapter?.onBeforeCheckIn?.(request);
|
|
494
|
-
if (before === false) {
|
|
495
|
-
if (metadata !== void 0 && previousState !== null)
|
|
496
|
-
await client.restore(previousState);
|
|
497
|
-
setClientError({ client, value: { code: "INVALID_PROOF", stampId: request.stampId } });
|
|
498
|
-
queuedMetadata.current.delete(request.idempotencyKey);
|
|
499
|
-
setOfflineQueue(
|
|
500
|
-
(current) => current.filter((item) => item.idempotencyKey !== request.idempotencyKey)
|
|
501
|
-
);
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
let result = metadata?.result;
|
|
505
|
-
if (result === void 0) {
|
|
506
|
-
const localResult = await client.acquire(request.stampId, request.context, request.now);
|
|
507
|
-
if (!localResult.ok) {
|
|
508
|
-
setClientError({ client, value: localResult.error });
|
|
509
|
-
queuedMetadata.current.delete(request.idempotencyKey);
|
|
510
|
-
setOfflineQueue(
|
|
511
|
-
(current) => current.filter((item) => item.idempotencyKey !== request.idempotencyKey)
|
|
512
|
-
);
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
result = localResult.value;
|
|
516
|
-
queuedMetadata.current.set(request.idempotencyKey, {
|
|
517
|
-
previousState,
|
|
518
|
-
result
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
const verified = await syncAdapter?.onServerVerify?.(request);
|
|
522
|
-
if (isRejectedVerification(verified)) {
|
|
523
|
-
if (previousState !== null) await client.restore(previousState);
|
|
524
|
-
setClientError({
|
|
525
|
-
client,
|
|
526
|
-
value: { code: "INVALID_PROOF", stampId: request.stampId }
|
|
527
|
-
});
|
|
528
|
-
} else {
|
|
529
|
-
notifyAcquisitionEvents(
|
|
530
|
-
result,
|
|
531
|
-
events,
|
|
532
|
-
options.onStampClaimed,
|
|
533
|
-
options.onRewardUnlocked
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
queuedMetadata.current.delete(request.idempotencyKey);
|
|
537
|
-
setOfflineQueue(
|
|
538
|
-
(current) => current.filter((item) => item.idempotencyKey !== request.idempotencyKey)
|
|
539
|
-
);
|
|
540
|
-
} catch (flushError) {
|
|
541
|
-
if (isNetworkFailure(flushError)) {
|
|
542
|
-
setClientError({ client, value: toError(flushError) });
|
|
543
|
-
break;
|
|
544
|
-
}
|
|
545
|
-
setClientError({ client, value: toError(flushError) });
|
|
546
|
-
queuedMetadata.current.delete(request.idempotencyKey);
|
|
547
|
-
setOfflineQueue(
|
|
548
|
-
(current) => current.filter((item) => item.idempotencyKey !== request.idempotencyKey)
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
} finally {
|
|
553
|
-
isFlushingQueue.current = false;
|
|
554
|
-
setIsOperationPending(false);
|
|
555
|
-
}
|
|
556
|
-
}, [client, events, offlineQueue, options.onRewardUnlocked, options.onStampClaimed, syncAdapter]);
|
|
557
|
-
react.useEffect(() => {
|
|
558
|
-
if (typeof window === "undefined") return;
|
|
559
|
-
const handleOnline = () => {
|
|
560
|
-
void flushQueue();
|
|
561
|
-
};
|
|
562
|
-
window.addEventListener("online", handleOnline);
|
|
563
|
-
return () => window.removeEventListener("online", handleOnline);
|
|
564
|
-
}, [flushQueue]);
|
|
565
|
-
return {
|
|
566
|
-
state: optimisticState,
|
|
567
|
-
isLoading,
|
|
568
|
-
isPending: isPending || isOperationPending,
|
|
569
|
-
error,
|
|
570
|
-
rewardsState: optimisticState?.rewards ?? [],
|
|
571
|
-
acquire,
|
|
572
|
-
reset,
|
|
573
|
-
redeem,
|
|
574
|
-
exportRecoveryCode,
|
|
575
|
-
importRecoveryCode,
|
|
576
|
-
exportRecoveryToken,
|
|
577
|
-
importRecoveryToken,
|
|
578
|
-
syncWithServer,
|
|
579
|
-
offlineQueue,
|
|
580
|
-
queuedCount: offlineQueue.length,
|
|
581
|
-
flushQueue
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
function usePublicStampRally(config, options) {
|
|
585
|
-
return { ...useStampRally(options.client, options), config };
|
|
586
|
-
}
|
|
587
6
|
function serverSnapshot() {
|
|
588
7
|
return null;
|
|
589
8
|
}
|
|
590
|
-
function
|
|
591
|
-
return
|
|
9
|
+
function errorFrom(value) {
|
|
10
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
592
11
|
}
|
|
593
|
-
function
|
|
594
|
-
const subscribe = react.useCallback(
|
|
595
|
-
(listener) => client.subscribe(() => listener()),
|
|
596
|
-
[client]
|
|
597
|
-
);
|
|
12
|
+
function useStampRally(client, options = {}) {
|
|
13
|
+
const subscribe = react.useCallback((listener) => client.subscribe(listener), [client]);
|
|
598
14
|
const getSnapshot = react.useCallback(() => client.getState(), [client]);
|
|
599
|
-
const
|
|
600
|
-
const state = rawState ?? {
|
|
601
|
-
rallyId: client.getConfig().id,
|
|
602
|
-
records: [],
|
|
603
|
-
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
604
|
-
};
|
|
15
|
+
const state = react.useSyncExternalStore(subscribe, getSnapshot, serverSnapshot);
|
|
605
16
|
const [error, setError] = react.useState(null);
|
|
606
17
|
react.useEffect(() => {
|
|
607
18
|
if (options.initialize === false || client.getState() !== null) return;
|
|
608
19
|
let active = true;
|
|
609
20
|
void client.init().catch((reason) => {
|
|
610
|
-
if (active) setError(
|
|
21
|
+
if (active) setError(errorFrom(reason));
|
|
611
22
|
});
|
|
612
23
|
return () => {
|
|
613
24
|
active = false;
|
|
614
25
|
};
|
|
615
26
|
}, [client, options.initialize]);
|
|
616
|
-
react.useEffect(
|
|
617
|
-
() => client.subscribeEvents((event) => {
|
|
618
|
-
if (event.type === "error") setError(new Error(errorMessage(event.error)));
|
|
619
|
-
}),
|
|
620
|
-
[client]
|
|
621
|
-
);
|
|
622
27
|
const onCheckIn = react.useCallback(
|
|
623
28
|
(spotId, proof, checkInOptions = {}) => {
|
|
624
29
|
setError(null);
|
|
625
30
|
return client.checkIn(spotId, proof, checkInOptions).then((result) => {
|
|
626
|
-
if (!result.ok) setError(
|
|
31
|
+
if (!result.ok) setError(errorFrom(result.error));
|
|
627
32
|
return result;
|
|
628
33
|
});
|
|
629
34
|
},
|
|
@@ -633,7 +38,7 @@ function useUniversalStampRally(client, options = {}) {
|
|
|
633
38
|
(rewardId, claimOptions = {}) => {
|
|
634
39
|
setError(null);
|
|
635
40
|
return client.claimReward(rewardId, claimOptions).then((result) => {
|
|
636
|
-
if (!result.ok) setError(
|
|
41
|
+
if (!result.ok) setError(errorFrom(result.error));
|
|
637
42
|
return result;
|
|
638
43
|
});
|
|
639
44
|
},
|
|
@@ -642,7 +47,7 @@ function useUniversalStampRally(client, options = {}) {
|
|
|
642
47
|
const onSync = react.useCallback(() => {
|
|
643
48
|
setError(null);
|
|
644
49
|
return client.sync().catch((reason) => {
|
|
645
|
-
const next =
|
|
50
|
+
const next = errorFrom(reason);
|
|
646
51
|
setError(next);
|
|
647
52
|
throw next;
|
|
648
53
|
});
|
|
@@ -650,16 +55,16 @@ function useUniversalStampRally(client, options = {}) {
|
|
|
650
55
|
return {
|
|
651
56
|
state,
|
|
652
57
|
config: client.getConfig(),
|
|
653
|
-
isLoading:
|
|
58
|
+
isLoading: state === null,
|
|
654
59
|
error,
|
|
655
60
|
onCheckIn,
|
|
656
61
|
onClaimReward,
|
|
657
|
-
onSync
|
|
62
|
+
onSync,
|
|
63
|
+
switchUser: client.switchUser.bind(client),
|
|
64
|
+
clearUserState: client.clearUserState.bind(client)
|
|
658
65
|
};
|
|
659
66
|
}
|
|
660
67
|
|
|
661
|
-
exports.usePublicStampRally = usePublicStampRally;
|
|
662
68
|
exports.useStampRally = useStampRally;
|
|
663
|
-
exports.useUniversalStampRally = useUniversalStampRally;
|
|
664
69
|
//# sourceMappingURL=index.cjs.map
|
|
665
70
|
//# sourceMappingURL=index.cjs.map
|