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