@domino-sdk/relay 1.0.0-next.13 → 1.0.0-next.14
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/authoring.d.ts +12 -12
- package/dist/authoring.js +1 -1
- package/dist/browser.d.ts +80 -80
- package/dist/browser.js +4 -4
- package/dist/{chunk-EZWNGI5B.js → chunk-F5OCJE52.js} +2 -2
- package/dist/{chunk-WQ24ILGO.js → chunk-FWRP6YAQ.js} +7 -3
- package/dist/{chunk-KS33GOUL.js → chunk-HAOW4ERY.js} +2 -2
- package/dist/{chunk-M64XHC42.js → chunk-K4ANDR6G.js} +1 -1
- package/dist/{chunk-OA2KMQVZ.js → chunk-TLGCEVG5.js} +10 -0
- package/dist/{deployments-CNCP0JnD.d.ts → deployments-gbA8IZCK.d.ts} +106 -106
- package/dist/index.js +3 -3
- package/dist/react/index.d.ts +434 -600
- package/dist/react/index.js +1187 -807
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +3 -3
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +3 -3
- package/package.json +4 -2
package/dist/react/index.js
CHANGED
|
@@ -8,40 +8,43 @@ import {
|
|
|
8
8
|
discoverSolanaWallets,
|
|
9
9
|
readSignInMethods,
|
|
10
10
|
signInOptionsFrom
|
|
11
|
-
} from "../chunk-
|
|
12
|
-
import "../chunk-
|
|
13
|
-
import "../chunk-
|
|
11
|
+
} from "../chunk-FWRP6YAQ.js";
|
|
12
|
+
import "../chunk-F5OCJE52.js";
|
|
13
|
+
import "../chunk-K4ANDR6G.js";
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
RelayErrorCode,
|
|
16
|
+
asRelayError,
|
|
17
|
+
isRelayError
|
|
18
|
+
} from "../chunk-TLGCEVG5.js";
|
|
18
19
|
import {
|
|
19
20
|
sessionMember
|
|
20
21
|
} from "../chunk-FXYJ4OMX.js";
|
|
21
22
|
import "../chunk-CRNQP7NV.js";
|
|
22
23
|
|
|
23
24
|
// src/react/provider.tsx
|
|
24
|
-
import {
|
|
25
|
+
import { useEffect as useEffect2, useMemo as useMemo2 } from "react";
|
|
25
26
|
|
|
26
27
|
// src/react/context.ts
|
|
27
28
|
import { createContext, useContext } from "react";
|
|
28
|
-
var
|
|
29
|
-
function
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
29
|
+
var StoreContext = createContext(null);
|
|
30
|
+
function useStore() {
|
|
31
|
+
const store = useContext(StoreContext);
|
|
32
|
+
if (!store) {
|
|
32
33
|
throw new Error("Domino hooks require <RelayProvider>");
|
|
33
34
|
}
|
|
34
|
-
return
|
|
35
|
+
return store;
|
|
36
|
+
}
|
|
37
|
+
function useRelay() {
|
|
38
|
+
return useStore().client;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
// src/react/experience.tsx
|
|
38
42
|
import {
|
|
39
43
|
createContext as createContext2,
|
|
44
|
+
useCallback,
|
|
40
45
|
useContext as useContext2,
|
|
41
|
-
useEffect as useEffect2,
|
|
42
46
|
useMemo,
|
|
43
|
-
|
|
44
|
-
useState as useState2
|
|
47
|
+
useSyncExternalStore
|
|
45
48
|
} from "react";
|
|
46
49
|
|
|
47
50
|
// src/react/role-delivery.ts
|
|
@@ -54,19 +57,32 @@ function completionMark(quests) {
|
|
|
54
57
|
function hasPending(deliveries) {
|
|
55
58
|
return deliveries.some((d) => d.status === "pending");
|
|
56
59
|
}
|
|
57
|
-
function
|
|
60
|
+
function roleDeliveryStatus(deliveries, quest) {
|
|
58
61
|
const mine = deliveries.filter((d) => d.quest === quest);
|
|
59
62
|
if (!mine.length) {
|
|
60
63
|
return null;
|
|
61
64
|
}
|
|
62
65
|
if (hasPending(mine)) {
|
|
63
|
-
return "
|
|
66
|
+
return "pending";
|
|
67
|
+
}
|
|
68
|
+
return mine.some((d) => d.status === "failed") ? "failed" : "granted";
|
|
69
|
+
}
|
|
70
|
+
function roleDeliveryLabel(status) {
|
|
71
|
+
switch (status) {
|
|
72
|
+
case "pending":
|
|
73
|
+
return "Role delivery pending";
|
|
74
|
+
case "failed":
|
|
75
|
+
return "Role delivery failed";
|
|
76
|
+
case "granted":
|
|
77
|
+
return "Role granted";
|
|
78
|
+
default:
|
|
79
|
+
return null;
|
|
64
80
|
}
|
|
65
|
-
return mine.some((d) => d.status === "failed") ? "Role delivery failed" : "Role granted";
|
|
66
81
|
}
|
|
67
82
|
var PENDING_POLL_MS = 5e3;
|
|
68
83
|
var RETRY_DELAYS_MS = [1e3, 4e3];
|
|
69
84
|
function watchRoleDeliveries(options) {
|
|
85
|
+
const visible2 = options.visible ?? (() => typeof document === "undefined" || document.visibilityState !== "hidden");
|
|
70
86
|
const schedule = options.schedule ?? ((run2, ms) => {
|
|
71
87
|
const timer = setTimeout(run2, ms);
|
|
72
88
|
return () => clearTimeout(timer);
|
|
@@ -82,6 +98,10 @@ function watchRoleDeliveries(options) {
|
|
|
82
98
|
if (!active || reading) {
|
|
83
99
|
return;
|
|
84
100
|
}
|
|
101
|
+
if (!visible2()) {
|
|
102
|
+
resting = true;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
85
105
|
reading = true;
|
|
86
106
|
resting = false;
|
|
87
107
|
try {
|
|
@@ -124,6 +144,355 @@ function watchRoleDeliveries(options) {
|
|
|
124
144
|
};
|
|
125
145
|
}
|
|
126
146
|
|
|
147
|
+
// src/react/use-role-deliveries.ts
|
|
148
|
+
import { useEffect, useState } from "react";
|
|
149
|
+
function useRoleDeliveries(relay, member, completions) {
|
|
150
|
+
const [state, setState] = useState(null);
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!member || !completions) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const watch = watchRoleDeliveries({
|
|
156
|
+
read: async () => (await relay.me.rewardDeliveries()).deliveries,
|
|
157
|
+
onChange: (deliveries) => setState({ member, deliveries })
|
|
158
|
+
});
|
|
159
|
+
const wake = () => {
|
|
160
|
+
if (document.visibilityState === "visible") {
|
|
161
|
+
watch.wake();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
window.addEventListener("focus", wake);
|
|
165
|
+
document.addEventListener("visibilitychange", wake);
|
|
166
|
+
return () => {
|
|
167
|
+
watch.dispose();
|
|
168
|
+
window.removeEventListener("focus", wake);
|
|
169
|
+
document.removeEventListener("visibilitychange", wake);
|
|
170
|
+
};
|
|
171
|
+
}, [relay, member, completions]);
|
|
172
|
+
return member && completions && state?.member === member ? state.deliveries : [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/react/experience.tsx
|
|
176
|
+
import { jsx } from "react/jsx-runtime";
|
|
177
|
+
var Deliveries = createContext2([]);
|
|
178
|
+
function useExperienceSnapshot() {
|
|
179
|
+
const store = useStore();
|
|
180
|
+
const get = () => store.experience;
|
|
181
|
+
return useSyncExternalStore(store.subscribe, get, get);
|
|
182
|
+
}
|
|
183
|
+
function DeliveriesProvider({ children }) {
|
|
184
|
+
const store = useStore();
|
|
185
|
+
const experience = useExperienceSnapshot();
|
|
186
|
+
const member = useSyncExternalStore(
|
|
187
|
+
store.subscribe,
|
|
188
|
+
() => store.scope?.member ?? null,
|
|
189
|
+
() => null
|
|
190
|
+
);
|
|
191
|
+
const ready = experience.status === "ready";
|
|
192
|
+
const deliveries = useRoleDeliveries(
|
|
193
|
+
store.client,
|
|
194
|
+
ready ? member : null,
|
|
195
|
+
ready ? completionMark([
|
|
196
|
+
...experience.data.quests,
|
|
197
|
+
...experience.data.collections.flatMap((c) => c.quests)
|
|
198
|
+
]) : ""
|
|
199
|
+
);
|
|
200
|
+
return /* @__PURE__ */ jsx(Deliveries, { value: deliveries, children });
|
|
201
|
+
}
|
|
202
|
+
function useExperience() {
|
|
203
|
+
const store = useStore();
|
|
204
|
+
const experience = useExperienceSnapshot();
|
|
205
|
+
const deliveries = useContext2(Deliveries);
|
|
206
|
+
const refresh = useCallback(() => store.refreshExperience(), [store]);
|
|
207
|
+
return useMemo(
|
|
208
|
+
() => ({ ...experience, refresh, deliveries }),
|
|
209
|
+
[experience, refresh, deliveries]
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/react/cache.ts
|
|
214
|
+
var LOADING = Object.freeze({
|
|
215
|
+
status: "loading",
|
|
216
|
+
data: void 0,
|
|
217
|
+
error: null
|
|
218
|
+
});
|
|
219
|
+
var UNUSED_MS = 5 * 6e4;
|
|
220
|
+
var STALE_MS = 3e4;
|
|
221
|
+
var relayError = (cause, fallback) => asRelayError(cause, fallback);
|
|
222
|
+
function hashKey(key) {
|
|
223
|
+
return JSON.stringify(key);
|
|
224
|
+
}
|
|
225
|
+
function startsWith(key, prefix) {
|
|
226
|
+
return prefix.every((part, index) => key[index] === part);
|
|
227
|
+
}
|
|
228
|
+
function visible() {
|
|
229
|
+
return typeof document === "undefined" || document.visibilityState !== "hidden";
|
|
230
|
+
}
|
|
231
|
+
var QueryCache = class {
|
|
232
|
+
session;
|
|
233
|
+
#entries = /* @__PURE__ */ new Map();
|
|
234
|
+
#disposed = false;
|
|
235
|
+
#onStaleSession;
|
|
236
|
+
constructor(session, onStaleSession) {
|
|
237
|
+
this.session = session;
|
|
238
|
+
this.#onStaleSession = onStaleSession;
|
|
239
|
+
}
|
|
240
|
+
get disposed() {
|
|
241
|
+
return this.#disposed;
|
|
242
|
+
}
|
|
243
|
+
snapshot(key) {
|
|
244
|
+
return this.#entries.get(hashKey(key))?.state ?? LOADING;
|
|
245
|
+
}
|
|
246
|
+
/** Starts observing `key`, reading it now unless a read is cached or in flight. */
|
|
247
|
+
subscribe(key, read, options, listener) {
|
|
248
|
+
if (this.#disposed) {
|
|
249
|
+
return () => {
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const hash = hashKey(key);
|
|
253
|
+
let entry = this.#entries.get(hash);
|
|
254
|
+
if (!entry) {
|
|
255
|
+
entry = {
|
|
256
|
+
key,
|
|
257
|
+
read,
|
|
258
|
+
state: LOADING,
|
|
259
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
260
|
+
intervals: /* @__PURE__ */ new Map(),
|
|
261
|
+
focus: /* @__PURE__ */ new Set(),
|
|
262
|
+
controller: null,
|
|
263
|
+
pending: null,
|
|
264
|
+
settledAt: 0,
|
|
265
|
+
version: 0,
|
|
266
|
+
timer: void 0,
|
|
267
|
+
unused: void 0
|
|
268
|
+
};
|
|
269
|
+
this.#entries.set(hash, entry);
|
|
270
|
+
}
|
|
271
|
+
const current = entry;
|
|
272
|
+
const token = {};
|
|
273
|
+
const unseen = current.listeners.size === 0;
|
|
274
|
+
current.read = read;
|
|
275
|
+
clearTimeout(current.unused);
|
|
276
|
+
current.listeners.add(listener);
|
|
277
|
+
if (options.intervalMs !== void 0 && options.intervalMs > 0) {
|
|
278
|
+
current.intervals.set(token, options.intervalMs);
|
|
279
|
+
}
|
|
280
|
+
if (options.refetchOnFocus) {
|
|
281
|
+
current.focus.add(token);
|
|
282
|
+
}
|
|
283
|
+
if (!current.pending && (current.settledAt === 0 || unseen && Date.now() - current.settledAt >= STALE_MS)) {
|
|
284
|
+
void this.#fetch(current, false);
|
|
285
|
+
} else {
|
|
286
|
+
this.#schedule(current);
|
|
287
|
+
}
|
|
288
|
+
return () => {
|
|
289
|
+
current.listeners.delete(listener);
|
|
290
|
+
current.intervals.delete(token);
|
|
291
|
+
current.focus.delete(token);
|
|
292
|
+
if (current.listeners.size === 0 && !this.#disposed) {
|
|
293
|
+
clearTimeout(current.timer);
|
|
294
|
+
current.unused = setTimeout(() => {
|
|
295
|
+
if (current.listeners.size === 0) {
|
|
296
|
+
this.#drop(hash, current);
|
|
297
|
+
}
|
|
298
|
+
}, UNUSED_MS);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/** Reads `key` again now, replacing any read in flight. */
|
|
303
|
+
refresh(key) {
|
|
304
|
+
const entry = this.#entries.get(hashKey(key));
|
|
305
|
+
return entry ? this.#fetch(entry, true) : Promise.resolve();
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Marks every query under each prefix out of date: those on screen are read again at once, the
|
|
309
|
+
* rest are dropped. A read in flight is replaced, because it may have started before the change.
|
|
310
|
+
* No prefixes means every query.
|
|
311
|
+
*/
|
|
312
|
+
invalidate(prefixes) {
|
|
313
|
+
const reads = [];
|
|
314
|
+
for (const [hash, entry] of this.#entries) {
|
|
315
|
+
if (prefixes.length > 0 && !prefixes.some((prefix) => startsWith(entry.key, prefix))) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (entry.listeners.size > 0) {
|
|
319
|
+
reads.push(this.#fetch(entry, true));
|
|
320
|
+
} else {
|
|
321
|
+
this.#drop(hash, entry);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return Promise.all(reads).then(() => {
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Changes the cached value of `key` with `update`, unless a read wrote a newer value since
|
|
329
|
+
* `version` (from `version(key)`). Returns whether it applied.
|
|
330
|
+
*/
|
|
331
|
+
update(key, version, update) {
|
|
332
|
+
const entry = this.#entries.get(hashKey(key));
|
|
333
|
+
if (this.#disposed || !entry || entry.version !== version || entry.state.status !== "ready") {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
entry.version++;
|
|
337
|
+
this.#write(entry, {
|
|
338
|
+
status: "ready",
|
|
339
|
+
data: update(entry.state.data),
|
|
340
|
+
error: null
|
|
341
|
+
});
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Replaces the cached value of `key` with one a change returned, cancelling a read in flight that
|
|
346
|
+
* may have started before the change. Does nothing for a key nobody has read.
|
|
347
|
+
*/
|
|
348
|
+
set(key, data) {
|
|
349
|
+
const entry = this.#entries.get(hashKey(key));
|
|
350
|
+
if (this.#disposed || !entry) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
entry.controller?.abort();
|
|
354
|
+
entry.controller = null;
|
|
355
|
+
entry.pending = null;
|
|
356
|
+
entry.version++;
|
|
357
|
+
entry.settledAt = Date.now();
|
|
358
|
+
this.#write(entry, { status: "ready", data, error: null });
|
|
359
|
+
this.#schedule(entry);
|
|
360
|
+
}
|
|
361
|
+
version(key) {
|
|
362
|
+
return this.#entries.get(hashKey(key))?.version ?? -1;
|
|
363
|
+
}
|
|
364
|
+
/** The tab became visible again: read whatever came due while it was hidden. */
|
|
365
|
+
resume() {
|
|
366
|
+
for (const entry of this.#entries.values()) {
|
|
367
|
+
this.#schedule(entry);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/** The window regained focus. */
|
|
371
|
+
focus() {
|
|
372
|
+
for (const entry of this.#entries.values()) {
|
|
373
|
+
if (entry.focus.size > 0 && entry.listeners.size > 0) {
|
|
374
|
+
void this.#fetch(entry, false);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/** Stops every read and timer for good. Answers that arrive later are dropped. */
|
|
379
|
+
dispose() {
|
|
380
|
+
this.#disposed = true;
|
|
381
|
+
for (const [hash, entry] of this.#entries) {
|
|
382
|
+
this.#drop(hash, entry);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
#drop(hash, entry) {
|
|
386
|
+
entry.controller?.abort();
|
|
387
|
+
entry.controller = null;
|
|
388
|
+
entry.pending = null;
|
|
389
|
+
clearTimeout(entry.timer);
|
|
390
|
+
clearTimeout(entry.unused);
|
|
391
|
+
if (this.#entries.get(hash) === entry) {
|
|
392
|
+
this.#entries.delete(hash);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
#write(entry, state) {
|
|
396
|
+
entry.state = state;
|
|
397
|
+
for (const listener of [...entry.listeners]) {
|
|
398
|
+
listener();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
#fetch(entry, replace) {
|
|
402
|
+
if (this.#disposed) {
|
|
403
|
+
return Promise.resolve();
|
|
404
|
+
}
|
|
405
|
+
if (entry.pending && !replace) {
|
|
406
|
+
return entry.pending;
|
|
407
|
+
}
|
|
408
|
+
entry.controller?.abort();
|
|
409
|
+
clearTimeout(entry.timer);
|
|
410
|
+
const controller = new AbortController();
|
|
411
|
+
entry.controller = controller;
|
|
412
|
+
const counts = () => !this.#disposed && entry.controller === controller;
|
|
413
|
+
const pending = (async () => {
|
|
414
|
+
try {
|
|
415
|
+
const data = await Promise.resolve().then(
|
|
416
|
+
() => entry.read({ signal: controller.signal, session: this.session })
|
|
417
|
+
);
|
|
418
|
+
if (counts()) {
|
|
419
|
+
entry.version++;
|
|
420
|
+
this.#write(entry, { status: "ready", data, error: null });
|
|
421
|
+
}
|
|
422
|
+
} catch (cause) {
|
|
423
|
+
if (!counts()) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const error = relayError(cause);
|
|
427
|
+
if (error.code === RelayErrorCode.StaleSession) {
|
|
428
|
+
this.#onStaleSession();
|
|
429
|
+
}
|
|
430
|
+
const gone = error.kind === "http" && error.status === 404 || error.code === RelayErrorCode.StaleSession;
|
|
431
|
+
entry.version++;
|
|
432
|
+
this.#write(
|
|
433
|
+
entry,
|
|
434
|
+
entry.state.status === "ready" && !gone ? { status: "ready", data: entry.state.data, error } : { status: "error", data: void 0, error }
|
|
435
|
+
);
|
|
436
|
+
} finally {
|
|
437
|
+
if (counts()) {
|
|
438
|
+
entry.controller = null;
|
|
439
|
+
entry.pending = null;
|
|
440
|
+
entry.settledAt = Date.now();
|
|
441
|
+
this.#schedule(entry);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
})();
|
|
445
|
+
entry.pending = pending;
|
|
446
|
+
return pending;
|
|
447
|
+
}
|
|
448
|
+
/** Arms the poll for the shortest interval anyone on screen asked for. */
|
|
449
|
+
#schedule(entry) {
|
|
450
|
+
clearTimeout(entry.timer);
|
|
451
|
+
if (this.#disposed || entry.pending || entry.listeners.size === 0 || entry.intervals.size === 0) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const interval = Math.min(...entry.intervals.values());
|
|
455
|
+
const wait = Math.max(0, entry.settledAt + interval - Date.now());
|
|
456
|
+
entry.timer = setTimeout(() => {
|
|
457
|
+
if (visible()) {
|
|
458
|
+
void this.#fetch(entry, false);
|
|
459
|
+
}
|
|
460
|
+
}, wait);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// src/react/keys.ts
|
|
465
|
+
var relayKeys = {
|
|
466
|
+
/** `useExperience`: invalidating it reads the quest experience again. */
|
|
467
|
+
experience: ["experience"],
|
|
468
|
+
/** `useMe`. */
|
|
469
|
+
me: ["me"],
|
|
470
|
+
/** `useAccounts`. */
|
|
471
|
+
accounts: ["accounts"],
|
|
472
|
+
/** `useWallets`. */
|
|
473
|
+
wallets: ["wallets"],
|
|
474
|
+
/** `useMyProfile`, `useProfile`, `useProfileCompletions`, `useProfileReferral`. */
|
|
475
|
+
profiles: ["profiles"],
|
|
476
|
+
/** `useLeaderboard`, `useLeaderboardPlacement`. */
|
|
477
|
+
leaderboards: ["leaderboards"],
|
|
478
|
+
/** `useReferralSummary`. */
|
|
479
|
+
referrals: ["referrals"],
|
|
480
|
+
/** `usePointLedgers`. */
|
|
481
|
+
pointLedgers: ["point-ledgers"],
|
|
482
|
+
/** `useTieredRewards`. */
|
|
483
|
+
tieredRewards: ["tiered-rewards"],
|
|
484
|
+
/** `useSignInMethods`. */
|
|
485
|
+
signInMethods: ["sign-in-methods"],
|
|
486
|
+
/** `useBalances`, `usePointHistory`. */
|
|
487
|
+
balances: ["balances"]
|
|
488
|
+
};
|
|
489
|
+
var completionChanges = [
|
|
490
|
+
relayKeys.balances,
|
|
491
|
+
relayKeys.leaderboards,
|
|
492
|
+
relayKeys.profiles,
|
|
493
|
+
relayKeys.referrals
|
|
494
|
+
];
|
|
495
|
+
|
|
127
496
|
// src/react/status.ts
|
|
128
497
|
function isFinished(view) {
|
|
129
498
|
return view.availability.kind === "completed" || view.completion.kind !== "keyed" && view.completed;
|
|
@@ -148,112 +517,125 @@ function visibleQuests(quests) {
|
|
|
148
517
|
}
|
|
149
518
|
function questStatus(view) {
|
|
150
519
|
if (isFinished(view)) {
|
|
151
|
-
return { tone: "done",
|
|
520
|
+
return { tone: "done", code: "completed" };
|
|
152
521
|
}
|
|
153
522
|
if (view.lifecycle === "paused") {
|
|
154
|
-
return {
|
|
155
|
-
tone: "off",
|
|
156
|
-
label: "Paused",
|
|
157
|
-
detail: "This activity is paused for now."
|
|
158
|
-
};
|
|
523
|
+
return { tone: "off", code: "paused" };
|
|
159
524
|
}
|
|
160
525
|
if (view.lifecycle === "archived") {
|
|
161
|
-
return {
|
|
162
|
-
tone: "off",
|
|
163
|
-
label: "Closed",
|
|
164
|
-
detail: "This activity is no longer running."
|
|
165
|
-
};
|
|
526
|
+
return { tone: "off", code: "closed" };
|
|
166
527
|
}
|
|
167
528
|
const availability = view.availability;
|
|
168
529
|
const ownSubmission = view.trigger.kind === "manual" && view.trigger.actor !== "staff";
|
|
169
530
|
const submissionFirst = ownSubmission && (availability.kind === "checking" || availability.kind === "needs-review" || availability.kind === "retryable");
|
|
170
531
|
if (view.streak && availability.kind !== "locked" && !submissionFirst) {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
tone: "attention",
|
|
176
|
-
label: "Milestone reached",
|
|
177
|
-
detail: rejection ?? "Your qualifying days are saved. This milestone's reward budget was exhausted."
|
|
178
|
-
};
|
|
532
|
+
const rejected = ownSubmission && view.attempt?.status === "rejected" ? { reason: view.attempt.reason ?? "" } : {};
|
|
533
|
+
if (view.streak.milestone.status === "budget-exhausted") {
|
|
534
|
+
return { tone: "attention", code: "streak-budget-exhausted", ...rejected };
|
|
179
535
|
}
|
|
180
|
-
const deadline = new Intl.DateTimeFormat("en", {
|
|
181
|
-
timeZone: streak.timeZone,
|
|
182
|
-
month: "short",
|
|
183
|
-
day: "numeric",
|
|
184
|
-
hour: "numeric",
|
|
185
|
-
minute: "2-digit",
|
|
186
|
-
timeZoneName: "short"
|
|
187
|
-
}).format(streak.nextBoundary);
|
|
188
|
-
const progress = streak.todayQualified ? `Today counts. Your next day starts ${deadline}. Best streak: ${streak.best} days.` : `Complete today's activity before ${deadline} ${streak.current ? "to keep your streak" : "to start your streak"}. Best streak: ${streak.best} days.`;
|
|
189
536
|
return {
|
|
190
|
-
tone:
|
|
191
|
-
|
|
192
|
-
|
|
537
|
+
tone: "reason" in rejected ? "attention" : view.streak.todayQualified ? "waiting" : "open",
|
|
538
|
+
code: "streak",
|
|
539
|
+
...rejected
|
|
193
540
|
};
|
|
194
541
|
}
|
|
195
542
|
if (runsSilently(view) && availability.kind !== "locked") {
|
|
196
|
-
return {
|
|
197
|
-
tone: "waiting",
|
|
198
|
-
label: "Automatic",
|
|
199
|
-
detail: view.completion.kind === "keyed" ? [
|
|
200
|
-
completionCountLabel(view),
|
|
201
|
-
"This counts by itself each time you qualify."
|
|
202
|
-
].filter(Boolean).join(". ") : "This updates by itself when you qualify."
|
|
203
|
-
};
|
|
543
|
+
return { tone: "waiting", code: "automatic" };
|
|
204
544
|
}
|
|
205
545
|
if (isAutomatic(view) && availability.kind !== "locked") {
|
|
206
|
-
return {
|
|
207
|
-
tone: "waiting",
|
|
208
|
-
label: "In progress",
|
|
209
|
-
detail: "This updates by itself when you qualify."
|
|
210
|
-
};
|
|
546
|
+
return { tone: "waiting", code: "in-progress" };
|
|
211
547
|
}
|
|
212
548
|
switch (availability.kind) {
|
|
213
549
|
case "completed":
|
|
214
|
-
return { tone: "done",
|
|
550
|
+
return { tone: "done", code: "completed" };
|
|
215
551
|
case "locked":
|
|
216
|
-
return {
|
|
217
|
-
tone: "locked",
|
|
218
|
-
label: "Locked",
|
|
219
|
-
detail: `Complete ${list(availability.missing.map((m) => m.title))} first.`
|
|
220
|
-
};
|
|
552
|
+
return { tone: "locked", code: "locked" };
|
|
221
553
|
case "checking":
|
|
222
|
-
return {
|
|
223
|
-
tone: "waiting",
|
|
224
|
-
label: "Checking",
|
|
225
|
-
detail: "Your submission is being verified."
|
|
226
|
-
};
|
|
554
|
+
return { tone: "waiting", code: "checking" };
|
|
227
555
|
case "needs-review":
|
|
228
|
-
return {
|
|
229
|
-
tone: "waiting",
|
|
230
|
-
label: "In review",
|
|
231
|
-
detail: "A person is reviewing your submission."
|
|
232
|
-
};
|
|
556
|
+
return { tone: "waiting", code: "in-review" };
|
|
233
557
|
case "retryable":
|
|
234
|
-
return {
|
|
235
|
-
tone: "attention",
|
|
236
|
-
label: "Didn't go through",
|
|
237
|
-
detail: "Verification couldn't run. Try again. Nothing was lost."
|
|
238
|
-
};
|
|
558
|
+
return { tone: "attention", code: "retryable" };
|
|
239
559
|
case "staff-required":
|
|
240
|
-
return {
|
|
241
|
-
tone: "waiting",
|
|
242
|
-
label: "Waiting for staff",
|
|
243
|
-
detail: "A staff member confirms this activity for you."
|
|
244
|
-
};
|
|
560
|
+
return { tone: "waiting", code: "staff-required" };
|
|
245
561
|
case "automatic":
|
|
246
|
-
return {
|
|
247
|
-
tone: "waiting",
|
|
248
|
-
label: "In progress",
|
|
249
|
-
detail: "This updates by itself when you qualify."
|
|
250
|
-
};
|
|
562
|
+
return { tone: "waiting", code: "in-progress" };
|
|
251
563
|
case "available":
|
|
252
564
|
return view.attempt?.status === "rejected" ? {
|
|
253
565
|
tone: "attention",
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
} : { tone: "open",
|
|
566
|
+
code: "rejected",
|
|
567
|
+
reason: view.attempt.reason ?? ""
|
|
568
|
+
} : { tone: "open", code: "ready" };
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function questStatusText(view) {
|
|
572
|
+
const status = questStatus(view);
|
|
573
|
+
const words = (label, detail) => detail === void 0 ? { ...status, label } : { ...status, label, detail };
|
|
574
|
+
const rejection = status.reason === void 0 ? null : sentence(status.reason || "That submission wasn't accepted.");
|
|
575
|
+
switch (status.code) {
|
|
576
|
+
case "completed":
|
|
577
|
+
return words("Completed");
|
|
578
|
+
case "paused":
|
|
579
|
+
return words("Paused", "This activity is paused for now.");
|
|
580
|
+
case "closed":
|
|
581
|
+
return words("Closed", "This activity is no longer running.");
|
|
582
|
+
case "streak-budget-exhausted":
|
|
583
|
+
return words(
|
|
584
|
+
"Milestone reached",
|
|
585
|
+
rejection ?? "Your qualifying days are saved. This milestone's reward budget was exhausted."
|
|
586
|
+
);
|
|
587
|
+
case "streak": {
|
|
588
|
+
const streak = view.streak;
|
|
589
|
+
const deadline = new Intl.DateTimeFormat("en", {
|
|
590
|
+
timeZone: streak.timeZone,
|
|
591
|
+
month: "short",
|
|
592
|
+
day: "numeric",
|
|
593
|
+
hour: "numeric",
|
|
594
|
+
minute: "2-digit",
|
|
595
|
+
timeZoneName: "short"
|
|
596
|
+
}).format(streak.nextBoundary);
|
|
597
|
+
const progress = streak.todayQualified ? `Today counts. Your next day starts ${deadline}. Best streak: ${streak.best} days.` : `Complete today's activity before ${deadline} ${streak.current ? "to keep your streak" : "to start your streak"}. Best streak: ${streak.best} days.`;
|
|
598
|
+
return words(
|
|
599
|
+
`${streak.current} / ${streak.goal} days`,
|
|
600
|
+
rejection ? `${rejection} ${progress}` : progress
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
case "automatic":
|
|
604
|
+
return words(
|
|
605
|
+
"Automatic",
|
|
606
|
+
view.completion.kind === "keyed" ? [
|
|
607
|
+
completionCountLabel(view),
|
|
608
|
+
"This counts by itself each time you qualify."
|
|
609
|
+
].filter(Boolean).join(". ") : "This updates by itself when you qualify."
|
|
610
|
+
);
|
|
611
|
+
case "in-progress":
|
|
612
|
+
return words("In progress", "This updates by itself when you qualify.");
|
|
613
|
+
case "locked":
|
|
614
|
+
return words(
|
|
615
|
+
"Locked",
|
|
616
|
+
view.availability.kind === "locked" ? `Complete ${list(view.availability.missing.map((m) => m.title))} first.` : void 0
|
|
617
|
+
);
|
|
618
|
+
case "checking":
|
|
619
|
+
return words("Checking", "Your submission is being verified.");
|
|
620
|
+
case "in-review":
|
|
621
|
+
return words("In review", "A person is reviewing your submission.");
|
|
622
|
+
case "retryable":
|
|
623
|
+
return words(
|
|
624
|
+
"Didn't go through",
|
|
625
|
+
"Verification couldn't run. Try again. Nothing was lost."
|
|
626
|
+
);
|
|
627
|
+
case "staff-required":
|
|
628
|
+
return words(
|
|
629
|
+
"Waiting for staff",
|
|
630
|
+
"A staff member confirms this activity for you."
|
|
631
|
+
);
|
|
632
|
+
case "rejected":
|
|
633
|
+
return words(
|
|
634
|
+
"Try again",
|
|
635
|
+
status.reason || "That submission wasn't accepted."
|
|
636
|
+
);
|
|
637
|
+
case "ready":
|
|
638
|
+
return words("Ready");
|
|
257
639
|
}
|
|
258
640
|
}
|
|
259
641
|
function canSubmit(view) {
|
|
@@ -278,109 +660,289 @@ function list(items) {
|
|
|
278
660
|
);
|
|
279
661
|
}
|
|
280
662
|
|
|
281
|
-
// src/react/
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
663
|
+
// src/react/store.ts
|
|
664
|
+
var SIGNED_OUT = Object.freeze({
|
|
665
|
+
status: "signed-out",
|
|
666
|
+
data: void 0,
|
|
667
|
+
error: null
|
|
668
|
+
});
|
|
669
|
+
function scopeKey(session) {
|
|
670
|
+
if (!session) {
|
|
671
|
+
return "visitor";
|
|
672
|
+
}
|
|
673
|
+
const principal = session.principal;
|
|
674
|
+
return JSON.stringify([
|
|
675
|
+
session.organization,
|
|
676
|
+
session.project,
|
|
677
|
+
session.environment,
|
|
678
|
+
principal.kind,
|
|
679
|
+
sessionMember(session) ?? principal.subject ?? null,
|
|
680
|
+
session.id
|
|
681
|
+
]);
|
|
682
|
+
}
|
|
683
|
+
function isVisible() {
|
|
684
|
+
return typeof document === "undefined" || document.visibilityState !== "hidden";
|
|
685
|
+
}
|
|
686
|
+
var RelayStore = class {
|
|
687
|
+
client;
|
|
688
|
+
#session = LOADING;
|
|
689
|
+
#scope = null;
|
|
690
|
+
#experience = { owner: null, state: LOADING };
|
|
691
|
+
#experienceView = LOADING;
|
|
692
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
693
|
+
#observer = null;
|
|
694
|
+
#starting = false;
|
|
695
|
+
#mounted = false;
|
|
696
|
+
/** Bumped by every session the store learns of, so an older read cannot overwrite a newer one. */
|
|
697
|
+
#sessionVersion = 0;
|
|
698
|
+
#sessionRead = null;
|
|
699
|
+
/** The completions the experience last showed, and for whom. */
|
|
700
|
+
#completions = null;
|
|
701
|
+
constructor(client) {
|
|
702
|
+
this.client = client;
|
|
703
|
+
}
|
|
704
|
+
/** `refreshSession` with one identity per store, for hooks to hand out as `refresh`. */
|
|
705
|
+
rereadSession = () => this.refreshSession();
|
|
706
|
+
subscribe = (listener) => {
|
|
707
|
+
this.#listeners.add(listener);
|
|
708
|
+
return () => {
|
|
709
|
+
this.#listeners.delete(listener);
|
|
710
|
+
};
|
|
711
|
+
};
|
|
712
|
+
get session() {
|
|
713
|
+
return this.#session;
|
|
714
|
+
}
|
|
715
|
+
get scope() {
|
|
716
|
+
return this.#scope;
|
|
717
|
+
}
|
|
718
|
+
get experience() {
|
|
719
|
+
return this.#experienceView;
|
|
720
|
+
}
|
|
721
|
+
/** Starts observing; the returned function stops every read, timer and listener. */
|
|
722
|
+
mount() {
|
|
723
|
+
this.#mounted = true;
|
|
724
|
+
if (isVisible()) {
|
|
725
|
+
this.#startObserver();
|
|
288
726
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
727
|
+
void this.refreshSession();
|
|
728
|
+
const onVisibility = () => {
|
|
729
|
+
if (isVisible()) {
|
|
730
|
+
this.#startObserver();
|
|
731
|
+
this.#scope?.cache.resume();
|
|
732
|
+
} else {
|
|
733
|
+
this.#observer?.dispose();
|
|
734
|
+
this.#observer = null;
|
|
296
735
|
}
|
|
297
736
|
};
|
|
298
|
-
|
|
299
|
-
|
|
737
|
+
const onFocus = () => this.#scope?.cache.focus();
|
|
738
|
+
if (typeof window !== "undefined") {
|
|
739
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
740
|
+
window.addEventListener("focus", onFocus);
|
|
741
|
+
}
|
|
300
742
|
return () => {
|
|
301
|
-
|
|
302
|
-
window
|
|
303
|
-
|
|
743
|
+
this.#mounted = false;
|
|
744
|
+
if (typeof window !== "undefined") {
|
|
745
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
746
|
+
window.removeEventListener("focus", onFocus);
|
|
747
|
+
}
|
|
748
|
+
this.#observer?.dispose();
|
|
749
|
+
this.#observer = null;
|
|
750
|
+
this.#sessionRead?.abort();
|
|
751
|
+
this.#sessionRead = null;
|
|
752
|
+
this.#sessionVersion++;
|
|
753
|
+
this.#scope?.cache.dispose();
|
|
754
|
+
this.#scope = null;
|
|
755
|
+
this.#session = LOADING;
|
|
756
|
+
this.#experience = { owner: null, state: LOADING };
|
|
757
|
+
this.#emit();
|
|
304
758
|
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
759
|
+
}
|
|
760
|
+
/** Reads the session again now, without waiting for the experience observer. */
|
|
761
|
+
async refreshSession() {
|
|
762
|
+
if (!this.#mounted) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const version = ++this.#sessionVersion;
|
|
766
|
+
this.#sessionRead?.abort();
|
|
767
|
+
const controller = new AbortController();
|
|
768
|
+
this.#sessionRead = controller;
|
|
769
|
+
try {
|
|
770
|
+
const session = await this.client.auth.session({
|
|
771
|
+
signal: controller.signal
|
|
772
|
+
});
|
|
773
|
+
if (version === this.#sessionVersion && this.#mounted) {
|
|
774
|
+
const before = this.#scope?.key;
|
|
775
|
+
this.#setSession({ status: "ready", data: session, error: null });
|
|
776
|
+
this.#emit();
|
|
777
|
+
if (before !== void 0 && this.#scope?.key !== before) {
|
|
778
|
+
void this.#observer?.refresh();
|
|
779
|
+
}
|
|
326
780
|
}
|
|
327
|
-
|
|
328
|
-
|
|
781
|
+
} catch (cause) {
|
|
782
|
+
if (version === this.#sessionVersion && this.#mounted) {
|
|
783
|
+
this.#failSession(relayError(cause));
|
|
784
|
+
this.#emit();
|
|
785
|
+
}
|
|
786
|
+
} finally {
|
|
787
|
+
if (this.#sessionRead === controller) {
|
|
788
|
+
this.#sessionRead = null;
|
|
329
789
|
}
|
|
330
|
-
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/** Writes a value a change returned into the current participant's cache (`QueryCache.set`). */
|
|
793
|
+
set(key, data) {
|
|
794
|
+
this.#scope?.cache.set(key, data);
|
|
795
|
+
}
|
|
796
|
+
refreshExperience() {
|
|
797
|
+
return this.#observer?.refresh() ?? Promise.resolve();
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Marks queries out of date (see `QueryCache.invalidate`). The `["experience"]` key, or no keys at
|
|
801
|
+
* all, also reads the quest experience again.
|
|
802
|
+
*/
|
|
803
|
+
invalidate(keys) {
|
|
804
|
+
const reads = [];
|
|
805
|
+
if (keys.length === 0 || keys.some((key) => key[0] === "experience")) {
|
|
806
|
+
reads.push(this.refreshExperience());
|
|
807
|
+
}
|
|
808
|
+
reads.push(this.#scope?.cache.invalidate(keys) ?? Promise.resolve());
|
|
809
|
+
return Promise.all(reads).then(() => {
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
#startObserver() {
|
|
813
|
+
if (this.#observer || !this.#mounted) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
this.#starting = true;
|
|
817
|
+
try {
|
|
818
|
+
this.#observer = this.client.quests.observe((observation) => {
|
|
331
819
|
switch (observation.kind) {
|
|
332
820
|
case "loading":
|
|
333
|
-
|
|
821
|
+
if (this.#starting) {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
this.#sessionVersion++;
|
|
825
|
+
this.#sessionRead?.abort();
|
|
826
|
+
return;
|
|
334
827
|
case "session":
|
|
335
|
-
|
|
828
|
+
this.#sessionVersion++;
|
|
829
|
+
this.#setSession({
|
|
830
|
+
status: "ready",
|
|
831
|
+
data: observation.session,
|
|
832
|
+
error: null
|
|
833
|
+
});
|
|
834
|
+
break;
|
|
336
835
|
case "ready":
|
|
337
|
-
|
|
836
|
+
this.#sessionVersion++;
|
|
837
|
+
this.#setSession({
|
|
338
838
|
status: "ready",
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
839
|
+
data: observation.session,
|
|
840
|
+
error: null
|
|
841
|
+
});
|
|
842
|
+
this.#completed(observation.progress);
|
|
843
|
+
this.#experience = {
|
|
844
|
+
owner: observation.progress.member,
|
|
845
|
+
state: {
|
|
846
|
+
status: "ready",
|
|
847
|
+
// Hidden quests never reach the UI, on any poll.
|
|
848
|
+
data: {
|
|
849
|
+
quests: visibleQuests(observation.progress.quests),
|
|
850
|
+
collections: observation.progress.collections.map(
|
|
851
|
+
(collection) => ({
|
|
852
|
+
...collection,
|
|
853
|
+
quests: visibleQuests(collection.quests)
|
|
854
|
+
})
|
|
855
|
+
)
|
|
856
|
+
},
|
|
857
|
+
error: null
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
break;
|
|
861
|
+
case "error": {
|
|
862
|
+
const error = relayError(observation.error);
|
|
863
|
+
const previous = this.#experience;
|
|
864
|
+
this.#experience = previous.state.status === "ready" ? {
|
|
865
|
+
owner: previous.owner,
|
|
866
|
+
state: { ...previous.state, error }
|
|
867
|
+
} : {
|
|
868
|
+
owner: this.#scope?.member ?? null,
|
|
869
|
+
state: { status: "error", data: void 0, error }
|
|
348
870
|
};
|
|
349
|
-
|
|
350
|
-
|
|
871
|
+
if (this.#session.status === "loading") {
|
|
872
|
+
void this.refreshSession();
|
|
873
|
+
}
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
351
876
|
}
|
|
877
|
+
this.#emit();
|
|
352
878
|
});
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}, [relay]);
|
|
357
|
-
const mine = experience.status === "ready" && member !== null && member === owner;
|
|
358
|
-
const deliveries = useRoleDeliveries(
|
|
359
|
-
relay,
|
|
360
|
-
mine ? member : null,
|
|
361
|
-
mine ? completionMark([
|
|
362
|
-
...experience.quests,
|
|
363
|
-
...experience.collections.flatMap((c) => c.quests)
|
|
364
|
-
]) : ""
|
|
365
|
-
);
|
|
366
|
-
const value = useMemo(
|
|
367
|
-
() => ({
|
|
368
|
-
experience,
|
|
369
|
-
member: mine ? member : null,
|
|
370
|
-
deliveries,
|
|
371
|
-
refresh: async () => observer.current?.refresh()
|
|
372
|
-
}),
|
|
373
|
-
[experience, mine, member, deliveries]
|
|
374
|
-
);
|
|
375
|
-
return /* @__PURE__ */ jsx(Context, { value, children });
|
|
376
|
-
}
|
|
377
|
-
function useExperience() {
|
|
378
|
-
const value = useContext2(Context);
|
|
379
|
-
if (!value) {
|
|
380
|
-
throw new Error("useExperience requires <RelayProvider>");
|
|
879
|
+
} finally {
|
|
880
|
+
this.#starting = false;
|
|
881
|
+
}
|
|
381
882
|
}
|
|
382
|
-
|
|
383
|
-
|
|
883
|
+
/**
|
|
884
|
+
* A new completion changes points, scores, profile completions and referral progress, whether
|
|
885
|
+
* the participant submitted it or Domino completed it by itself: read them again.
|
|
886
|
+
*/
|
|
887
|
+
#completed(progress) {
|
|
888
|
+
const mark = completionMark([
|
|
889
|
+
...progress.quests,
|
|
890
|
+
...progress.collections.flatMap((collection) => collection.quests)
|
|
891
|
+
]);
|
|
892
|
+
const previous = this.#completions;
|
|
893
|
+
this.#completions = { member: progress.member, mark };
|
|
894
|
+
if (previous?.member === progress.member && previous.mark !== mark) {
|
|
895
|
+
void this.#scope?.cache.invalidate(completionChanges);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
#failSession(error) {
|
|
899
|
+
this.#setSession(
|
|
900
|
+
this.#session.status === "ready" ? { ...this.#session, error } : { status: "error", data: void 0, error }
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
#setSession(next) {
|
|
904
|
+
this.#session = next;
|
|
905
|
+
const key = next.status === "loading" ? null : next.status === "error" ? "unknown" : scopeKey(next.data);
|
|
906
|
+
if (key === (this.#scope?.key ?? null)) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
this.#scope?.cache.dispose();
|
|
910
|
+
if (key === null) {
|
|
911
|
+
this.#scope = null;
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const session = next.status === "ready" ? next.data : null;
|
|
915
|
+
this.#scope = {
|
|
916
|
+
key,
|
|
917
|
+
session,
|
|
918
|
+
member: sessionMember(session),
|
|
919
|
+
cache: new QueryCache(session?.id, () => void this.refreshSession())
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
#deriveExperience() {
|
|
923
|
+
const session = this.#session;
|
|
924
|
+
if (session.status === "loading") {
|
|
925
|
+
return LOADING;
|
|
926
|
+
}
|
|
927
|
+
if (session.status === "error") {
|
|
928
|
+
return session;
|
|
929
|
+
}
|
|
930
|
+
const member = this.#scope?.member ?? null;
|
|
931
|
+
if (!member) {
|
|
932
|
+
return SIGNED_OUT;
|
|
933
|
+
}
|
|
934
|
+
return this.#experience.owner === member ? this.#experience.state : LOADING;
|
|
935
|
+
}
|
|
936
|
+
#emit() {
|
|
937
|
+
const experience = this.#deriveExperience();
|
|
938
|
+
if (experience !== this.#experienceView) {
|
|
939
|
+
this.#experienceView = experience;
|
|
940
|
+
}
|
|
941
|
+
for (const listener of [...this.#listeners]) {
|
|
942
|
+
listener();
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
};
|
|
384
946
|
|
|
385
947
|
// src/react/provider.tsx
|
|
386
948
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
@@ -389,10 +951,234 @@ function RelayProvider({
|
|
|
389
951
|
baseUrl = "/relay",
|
|
390
952
|
children
|
|
391
953
|
}) {
|
|
392
|
-
const
|
|
393
|
-
|
|
954
|
+
const origin = client ? null : baseUrl;
|
|
955
|
+
const store = useMemo2(
|
|
956
|
+
() => new RelayStore(
|
|
957
|
+
client ?? createBrowserRelayClient({ baseUrl: origin ?? baseUrl })
|
|
958
|
+
),
|
|
959
|
+
// A given client ignores `baseUrl`.
|
|
960
|
+
[client, origin]
|
|
961
|
+
);
|
|
962
|
+
useEffect2(() => store.mount(), [store]);
|
|
963
|
+
return /* @__PURE__ */ jsx2(StoreContext, { value: store, children: /* @__PURE__ */ jsx2(DeliveriesProvider, { children }) });
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/react/query.ts
|
|
967
|
+
import {
|
|
968
|
+
useCallback as useCallback2,
|
|
969
|
+
useEffect as useEffect3,
|
|
970
|
+
useLayoutEffect,
|
|
971
|
+
useMemo as useMemo3,
|
|
972
|
+
useRef,
|
|
973
|
+
useState as useState2,
|
|
974
|
+
useSyncExternalStore as useSyncExternalStore2
|
|
975
|
+
} from "react";
|
|
976
|
+
function useLatest(value) {
|
|
977
|
+
const ref = useRef(value);
|
|
978
|
+
useLayoutEffect(() => {
|
|
979
|
+
ref.current = value;
|
|
980
|
+
});
|
|
981
|
+
return ref;
|
|
982
|
+
}
|
|
983
|
+
function useSessionSnapshot(store) {
|
|
984
|
+
const get = () => store.session;
|
|
985
|
+
return useSyncExternalStore2(store.subscribe, get, get);
|
|
986
|
+
}
|
|
987
|
+
function useScope(store) {
|
|
988
|
+
const get = () => store.scope;
|
|
989
|
+
return useSyncExternalStore2(store.subscribe, get, get);
|
|
990
|
+
}
|
|
991
|
+
function useCached(cache, key, read, options) {
|
|
992
|
+
const hash = key ? hashKey(key) : null;
|
|
993
|
+
const { intervalMs, refetchOnFocus } = options;
|
|
994
|
+
const latest = useLatest({ key, read });
|
|
995
|
+
const subscribe = useCallback2(
|
|
996
|
+
(listener) => {
|
|
997
|
+
const { key: current } = latest.current;
|
|
998
|
+
if (!cache || !current) {
|
|
999
|
+
return () => {
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
return cache.subscribe(
|
|
1003
|
+
current,
|
|
1004
|
+
(context) => latest.current.read(context),
|
|
1005
|
+
{ intervalMs, refetchOnFocus },
|
|
1006
|
+
listener
|
|
1007
|
+
);
|
|
1008
|
+
},
|
|
1009
|
+
// `hash` stands for the key.
|
|
1010
|
+
[cache, hash, intervalMs, refetchOnFocus]
|
|
1011
|
+
);
|
|
1012
|
+
const get = () => cache && key ? cache.snapshot(key) : LOADING;
|
|
1013
|
+
const snapshot = useSyncExternalStore2(subscribe, get, get);
|
|
1014
|
+
const refresh = useCallback2(
|
|
1015
|
+
() => cache && latest.current.key ? cache.refresh(latest.current.key) : Promise.resolve(),
|
|
1016
|
+
[cache, hash]
|
|
1017
|
+
);
|
|
1018
|
+
return useMemo3(() => ({ ...snapshot, refresh }), [snapshot, refresh]);
|
|
1019
|
+
}
|
|
1020
|
+
function useRelayQuery(key, read, options = {}) {
|
|
1021
|
+
const store = useStore();
|
|
1022
|
+
const scope = useScope(store);
|
|
1023
|
+
return useCached(scope?.cache ?? null, key, read, options);
|
|
1024
|
+
}
|
|
1025
|
+
function useMemberQuery(key, read, options = {}) {
|
|
1026
|
+
const store = useStore();
|
|
1027
|
+
const scope = useScope(store);
|
|
1028
|
+
const member = scope?.member ?? null;
|
|
1029
|
+
const query = useCached(
|
|
1030
|
+
member ? scope.cache : null,
|
|
1031
|
+
member ? key : null,
|
|
1032
|
+
read,
|
|
1033
|
+
options
|
|
1034
|
+
);
|
|
1035
|
+
return useMemberGate(store, member, query);
|
|
1036
|
+
}
|
|
1037
|
+
function useMemberGate(store, member, query) {
|
|
1038
|
+
const session = useSessionSnapshot(store);
|
|
1039
|
+
return useMemo3(() => {
|
|
1040
|
+
if (session.status === "loading") {
|
|
1041
|
+
return { ...LOADING, refresh: query.refresh };
|
|
1042
|
+
}
|
|
1043
|
+
if (session.status === "error") {
|
|
1044
|
+
return {
|
|
1045
|
+
status: "error",
|
|
1046
|
+
data: void 0,
|
|
1047
|
+
error: session.error,
|
|
1048
|
+
refresh: store.rereadSession
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
return member ? query : { ...SIGNED_OUT, refresh: query.refresh };
|
|
1052
|
+
}, [session, member, query, store]);
|
|
1053
|
+
}
|
|
1054
|
+
function usePages(cache, key, read, options) {
|
|
1055
|
+
const hash = key ? hashKey(key) : null;
|
|
1056
|
+
const query = useCached(
|
|
1057
|
+
cache,
|
|
1058
|
+
key,
|
|
1059
|
+
(context) => read(context),
|
|
1060
|
+
options
|
|
1061
|
+
);
|
|
1062
|
+
const [more, setMore] = useState2(null);
|
|
1063
|
+
const busy = useRef(null);
|
|
1064
|
+
const latest = useLatest({ key, read, query });
|
|
1065
|
+
useEffect3(
|
|
1066
|
+
() => () => {
|
|
1067
|
+
busy.current?.abort();
|
|
1068
|
+
busy.current = null;
|
|
1069
|
+
},
|
|
1070
|
+
[cache, hash]
|
|
1071
|
+
);
|
|
1072
|
+
const loadMore = useCallback2(async () => {
|
|
1073
|
+
const { key: current, read: reader, query: shown } = latest.current;
|
|
1074
|
+
if (!cache || !current || busy.current || shown.status !== "ready" || shown.data.nextCursor === null) {
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const owner = { cache, hash };
|
|
1078
|
+
const version = cache.version(current);
|
|
1079
|
+
const controller = new AbortController();
|
|
1080
|
+
busy.current = controller;
|
|
1081
|
+
setMore({ owner, loading: true, error: null });
|
|
1082
|
+
try {
|
|
1083
|
+
const page = await reader({
|
|
1084
|
+
signal: controller.signal,
|
|
1085
|
+
session: cache.session,
|
|
1086
|
+
cursor: shown.data.nextCursor
|
|
1087
|
+
});
|
|
1088
|
+
if (busy.current === controller) {
|
|
1089
|
+
cache.update(current, version, (data) => ({
|
|
1090
|
+
items: [...data.items, ...page.items],
|
|
1091
|
+
nextCursor: page.nextCursor
|
|
1092
|
+
}));
|
|
1093
|
+
}
|
|
1094
|
+
} catch (cause) {
|
|
1095
|
+
if (busy.current === controller) {
|
|
1096
|
+
setMore({ owner, loading: false, error: relayError(cause) });
|
|
1097
|
+
}
|
|
1098
|
+
} finally {
|
|
1099
|
+
if (busy.current === controller) {
|
|
1100
|
+
busy.current = null;
|
|
1101
|
+
setMore(
|
|
1102
|
+
(value) => value?.owner === owner ? { ...value, loading: false } : value
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}, [cache, hash]);
|
|
1107
|
+
const mine = more && more.owner.cache === cache && more.owner.hash === hash ? more : null;
|
|
1108
|
+
return useMemo3(
|
|
1109
|
+
() => ({
|
|
1110
|
+
...query,
|
|
1111
|
+
loadMore,
|
|
1112
|
+
loadingMore: mine?.loading ?? false,
|
|
1113
|
+
loadMoreError: mine?.error ?? null
|
|
1114
|
+
}),
|
|
1115
|
+
[query, loadMore, mine]
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
function useRelayPages(key, read, options = {}) {
|
|
1119
|
+
const store = useStore();
|
|
1120
|
+
const scope = useScope(store);
|
|
1121
|
+
return usePages(scope?.cache ?? null, key, read, options);
|
|
1122
|
+
}
|
|
1123
|
+
function useMemberPages(key, read, options = {}) {
|
|
1124
|
+
const store = useStore();
|
|
1125
|
+
const scope = useScope(store);
|
|
1126
|
+
const member = scope?.member ?? null;
|
|
1127
|
+
const pages = usePages(
|
|
1128
|
+
member ? scope.cache : null,
|
|
1129
|
+
member ? key : null,
|
|
1130
|
+
read,
|
|
1131
|
+
options
|
|
1132
|
+
);
|
|
1133
|
+
const gate = useMemberGate(store, member, pages);
|
|
1134
|
+
return useMemo3(
|
|
1135
|
+
() => gate === pages ? pages : {
|
|
1136
|
+
...gate,
|
|
1137
|
+
loadMore: pages.loadMore,
|
|
1138
|
+
loadingMore: false,
|
|
1139
|
+
loadMoreError: null
|
|
1140
|
+
},
|
|
1141
|
+
[gate, pages]
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
function useInvalidate() {
|
|
1145
|
+
const store = useStore();
|
|
1146
|
+
return useCallback2((keys = []) => store.invalidate(keys), [store]);
|
|
1147
|
+
}
|
|
1148
|
+
function useSession() {
|
|
1149
|
+
const store = useStore();
|
|
1150
|
+
const session = useSessionSnapshot(store);
|
|
1151
|
+
const refresh = store.rereadSession;
|
|
1152
|
+
return useMemo3(
|
|
1153
|
+
() => ({
|
|
1154
|
+
...session,
|
|
1155
|
+
refresh,
|
|
1156
|
+
member: session.status === "ready" ? sessionMember(session.data) : null
|
|
1157
|
+
}),
|
|
1158
|
+
[session, refresh]
|
|
394
1159
|
);
|
|
395
|
-
|
|
1160
|
+
}
|
|
1161
|
+
function useAction() {
|
|
1162
|
+
const [busy, setBusy] = useState2(false);
|
|
1163
|
+
const [actionError, setActionError] = useState2(null);
|
|
1164
|
+
const run = useCallback2(async (task) => {
|
|
1165
|
+
setBusy(true);
|
|
1166
|
+
setActionError(null);
|
|
1167
|
+
try {
|
|
1168
|
+
await task();
|
|
1169
|
+
return true;
|
|
1170
|
+
} catch (cause) {
|
|
1171
|
+
setActionError(relayError(cause));
|
|
1172
|
+
return false;
|
|
1173
|
+
} finally {
|
|
1174
|
+
setBusy(false);
|
|
1175
|
+
}
|
|
1176
|
+
}, []);
|
|
1177
|
+
return { busy, actionError, run };
|
|
1178
|
+
}
|
|
1179
|
+
function useScopeSession() {
|
|
1180
|
+
const store = useStore();
|
|
1181
|
+
return useScope(store)?.session?.id;
|
|
396
1182
|
}
|
|
397
1183
|
|
|
398
1184
|
// src/react/quest-groups.ts
|
|
@@ -416,24 +1202,24 @@ function questGroups(experience, order = []) {
|
|
|
416
1202
|
}
|
|
417
1203
|
|
|
418
1204
|
// src/react/use-quest.ts
|
|
419
|
-
import { useCallback, useEffect as
|
|
1205
|
+
import { useCallback as useCallback3, useEffect as useEffect4, useState as useState3 } from "react";
|
|
420
1206
|
function useQuest(observed) {
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
const [latest, setLatest] =
|
|
424
|
-
const [draft, setDraft] =
|
|
425
|
-
const [notice, setNotice] =
|
|
426
|
-
const [error, setError] =
|
|
427
|
-
const [busy, setBusy] =
|
|
428
|
-
const [discardedAttempt, setDiscardedAttempt] =
|
|
429
|
-
const [saved, setSaved] =
|
|
430
|
-
const [receipt, setReceipt] =
|
|
431
|
-
|
|
1207
|
+
const store = useStore();
|
|
1208
|
+
const relay = store.client;
|
|
1209
|
+
const [latest, setLatest] = useState3(null);
|
|
1210
|
+
const [draft, setDraft] = useState3(null);
|
|
1211
|
+
const [notice, setNotice] = useState3(null);
|
|
1212
|
+
const [error, setError] = useState3(null);
|
|
1213
|
+
const [busy, setBusy] = useState3(false);
|
|
1214
|
+
const [discardedAttempt, setDiscardedAttempt] = useState3(null);
|
|
1215
|
+
const [saved, setSaved] = useState3("none");
|
|
1216
|
+
const [receipt, setReceipt] = useState3(null);
|
|
1217
|
+
useEffect4(() => setLatest(null), [observed.release]);
|
|
432
1218
|
const view = latest ?? observed;
|
|
433
1219
|
const { quest, release } = view;
|
|
434
1220
|
const completed = isFinished(view) || isAutomatic(view);
|
|
435
1221
|
const attempt = view.attempt?.id;
|
|
436
|
-
|
|
1222
|
+
useEffect4(() => {
|
|
437
1223
|
let active = true;
|
|
438
1224
|
relay.quests.open(view).then((controller) => controller.status()).then((status) => {
|
|
439
1225
|
if (!active) {
|
|
@@ -448,7 +1234,7 @@ function useQuest(observed) {
|
|
|
448
1234
|
active = false;
|
|
449
1235
|
};
|
|
450
1236
|
}, [relay, quest, release, completed, attempt, busy]);
|
|
451
|
-
const run =
|
|
1237
|
+
const run = useCallback3(
|
|
452
1238
|
async (action) => {
|
|
453
1239
|
setBusy(true);
|
|
454
1240
|
setError(null);
|
|
@@ -464,16 +1250,14 @@ function useQuest(observed) {
|
|
|
464
1250
|
setReceipt(result.attempt.id);
|
|
465
1251
|
setNotice("received");
|
|
466
1252
|
}
|
|
467
|
-
await
|
|
1253
|
+
await store.refreshExperience();
|
|
468
1254
|
} catch (cause) {
|
|
469
|
-
setError(
|
|
470
|
-
cause instanceof Error ? cause.message : "Something went wrong."
|
|
471
|
-
);
|
|
1255
|
+
setError(asRelayError(cause, "Something went wrong."));
|
|
472
1256
|
} finally {
|
|
473
1257
|
setBusy(false);
|
|
474
1258
|
}
|
|
475
1259
|
},
|
|
476
|
-
[
|
|
1260
|
+
[store]
|
|
477
1261
|
);
|
|
478
1262
|
return {
|
|
479
1263
|
/** The newest known version of the quest. Render this, not the prop. */
|
|
@@ -497,9 +1281,7 @@ function useQuest(observed) {
|
|
|
497
1281
|
setDraft(null);
|
|
498
1282
|
setNotice(null);
|
|
499
1283
|
} catch (cause) {
|
|
500
|
-
setError(
|
|
501
|
-
cause instanceof Error ? cause.message : "Could not start a new submission."
|
|
502
|
-
);
|
|
1284
|
+
setError(asRelayError(cause, "Could not start a new submission."));
|
|
503
1285
|
} finally {
|
|
504
1286
|
setBusy(false);
|
|
505
1287
|
}
|
|
@@ -550,72 +1332,41 @@ function useQuest(observed) {
|
|
|
550
1332
|
}
|
|
551
1333
|
|
|
552
1334
|
// src/react/auth.ts
|
|
553
|
-
import { useCallback as
|
|
1335
|
+
import { useCallback as useCallback4, useMemo as useMemo5, useRef as useRef2, useState as useState5 } from "react";
|
|
554
1336
|
|
|
555
1337
|
// src/react/wallets.ts
|
|
556
|
-
import {
|
|
557
|
-
var WALLETS_CHANGED = "relay-wallets-changed";
|
|
1338
|
+
import { useEffect as useEffect5, useMemo as useMemo4, useState as useState4 } from "react";
|
|
558
1339
|
function useWallets() {
|
|
559
1340
|
const relay = useRelay();
|
|
560
|
-
const
|
|
561
|
-
const
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
return () => {
|
|
584
|
-
window.removeEventListener("focus", update);
|
|
585
|
-
window.removeEventListener(WALLETS_CHANGED, update);
|
|
1341
|
+
const invalidate = useInvalidate();
|
|
1342
|
+
const session = useScopeSession();
|
|
1343
|
+
const query = useMemberQuery(
|
|
1344
|
+
relayKeys.wallets,
|
|
1345
|
+
(options) => relay.wallets.list(options),
|
|
1346
|
+
{ refetchOnFocus: true }
|
|
1347
|
+
);
|
|
1348
|
+
const { busy, actionError, run } = useAction();
|
|
1349
|
+
const actions = useMemo4(() => {
|
|
1350
|
+
const changed = () => void invalidate([relayKeys.wallets, relayKeys.experience]);
|
|
1351
|
+
return {
|
|
1352
|
+
/** Links the wallet to the member the list was read for; another session is refused. */
|
|
1353
|
+
link: (signer) => run(async () => {
|
|
1354
|
+
await relay.wallets.link(signer, { session });
|
|
1355
|
+
changed();
|
|
1356
|
+
}),
|
|
1357
|
+
unlink: (wallet) => run(async () => {
|
|
1358
|
+
await relay.wallets.unlink(
|
|
1359
|
+
{ chain: wallet.chain, address: wallet.address },
|
|
1360
|
+
{ session }
|
|
1361
|
+
);
|
|
1362
|
+
changed();
|
|
1363
|
+
})
|
|
586
1364
|
};
|
|
587
|
-
}, [
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
await task();
|
|
593
|
-
} catch (cause) {
|
|
594
|
-
setError(
|
|
595
|
-
cause instanceof Error && cause.message ? cause.message : fallback
|
|
596
|
-
);
|
|
597
|
-
} finally {
|
|
598
|
-
setBusy(false);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
return {
|
|
602
|
-
/** Null until read, and while nobody is signed in. */
|
|
603
|
-
wallets,
|
|
604
|
-
error,
|
|
605
|
-
busy,
|
|
606
|
-
refresh,
|
|
607
|
-
link: (signer) => run(async () => {
|
|
608
|
-
await relay.wallets.link(signer);
|
|
609
|
-
window.dispatchEvent(new Event(WALLETS_CHANGED));
|
|
610
|
-
}, "Couldn't link this wallet. Try again."),
|
|
611
|
-
unlink: (wallet) => run(async () => {
|
|
612
|
-
await relay.wallets.unlink({
|
|
613
|
-
chain: wallet.chain,
|
|
614
|
-
address: wallet.address
|
|
615
|
-
});
|
|
616
|
-
window.dispatchEvent(new Event(WALLETS_CHANGED));
|
|
617
|
-
}, "Couldn't unlink this wallet. Try again.")
|
|
618
|
-
};
|
|
1365
|
+
}, [relay, invalidate, session, run]);
|
|
1366
|
+
return useMemo4(
|
|
1367
|
+
() => ({ ...query, busy, actionError, ...actions }),
|
|
1368
|
+
[query, busy, actionError, actions]
|
|
1369
|
+
);
|
|
619
1370
|
}
|
|
620
1371
|
function discoverChains(key) {
|
|
621
1372
|
return JSON.parse(key).flatMap(
|
|
@@ -624,8 +1375,8 @@ function discoverChains(key) {
|
|
|
624
1375
|
}
|
|
625
1376
|
function useWalletDiscovery(options = {}) {
|
|
626
1377
|
const chains = JSON.stringify(cosmosChainList(options.cosmos));
|
|
627
|
-
const [found, setFound] =
|
|
628
|
-
|
|
1378
|
+
const [found, setFound] = useState4({ evm: [], solana: [], cosmos: [] });
|
|
1379
|
+
useEffect5(() => {
|
|
629
1380
|
let active = true;
|
|
630
1381
|
const solana = discoverSolanaWallets();
|
|
631
1382
|
const cosmos = discoverChains(chains);
|
|
@@ -647,8 +1398,8 @@ function useSignInWallets(networks, options = {}) {
|
|
|
647
1398
|
);
|
|
648
1399
|
const key = networks ? `${networks.join(" ")}
|
|
649
1400
|
${chains}` : null;
|
|
650
|
-
const [found, setFound] =
|
|
651
|
-
|
|
1401
|
+
const [found, setFound] = useState4(null);
|
|
1402
|
+
useEffect5(() => {
|
|
652
1403
|
if (key === null) {
|
|
653
1404
|
return;
|
|
654
1405
|
}
|
|
@@ -672,71 +1423,44 @@ ${chains}` : null;
|
|
|
672
1423
|
// src/react/auth.ts
|
|
673
1424
|
function useSignInMethods() {
|
|
674
1425
|
const relay = useRelay();
|
|
675
|
-
|
|
676
|
-
useEffect5(() => {
|
|
677
|
-
let active = true;
|
|
678
|
-
void readSignInMethods(relay).then((result) => {
|
|
679
|
-
if (active) {
|
|
680
|
-
setMethods(result);
|
|
681
|
-
}
|
|
682
|
-
});
|
|
683
|
-
return () => {
|
|
684
|
-
active = false;
|
|
685
|
-
};
|
|
686
|
-
}, [relay]);
|
|
687
|
-
return methods;
|
|
1426
|
+
return useRelayQuery(relayKeys.signInMethods, () => readSignInMethods(relay));
|
|
688
1427
|
}
|
|
689
1428
|
function useMe() {
|
|
690
1429
|
const relay = useRelay();
|
|
691
|
-
|
|
692
|
-
const [me, setMe] = useState6(null);
|
|
693
|
-
useEffect5(() => {
|
|
694
|
-
if (!member) {
|
|
695
|
-
setMe(null);
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
let active = true;
|
|
699
|
-
void relay.me.get().then((value) => active && setMe(value)).catch(() => active && setMe(null));
|
|
700
|
-
return () => {
|
|
701
|
-
active = false;
|
|
702
|
-
};
|
|
703
|
-
}, [relay, member]);
|
|
704
|
-
return me;
|
|
1430
|
+
return useMemberQuery(relayKeys.me, (options) => relay.me.get(options));
|
|
705
1431
|
}
|
|
706
1432
|
function useSignIn() {
|
|
707
1433
|
const relay = useRelay();
|
|
708
|
-
const [challenge, setChallengeState] =
|
|
1434
|
+
const [challenge, setChallengeState] = useState5(null);
|
|
709
1435
|
const latest = useRef2(null);
|
|
710
|
-
const [busy, setBusy] =
|
|
711
|
-
const [error, setError] =
|
|
712
|
-
const setChallenge =
|
|
1436
|
+
const [busy, setBusy] = useState5(false);
|
|
1437
|
+
const [error, setError] = useState5(null);
|
|
1438
|
+
const setChallenge = useCallback4((value) => {
|
|
713
1439
|
latest.current = value;
|
|
714
1440
|
setChallengeState(value);
|
|
715
1441
|
}, []);
|
|
716
|
-
const run =
|
|
1442
|
+
const run = useCallback4(
|
|
717
1443
|
async (task, fallback) => {
|
|
718
1444
|
setBusy(true);
|
|
719
1445
|
setError(null);
|
|
720
1446
|
try {
|
|
721
1447
|
await task();
|
|
722
1448
|
} catch (cause) {
|
|
723
|
-
setError(
|
|
724
|
-
cause instanceof Error && cause.message ? cause.message : fallback
|
|
725
|
-
);
|
|
1449
|
+
setError(asRelayError(cause, fallback));
|
|
726
1450
|
} finally {
|
|
727
1451
|
setBusy(false);
|
|
728
1452
|
}
|
|
729
1453
|
},
|
|
730
1454
|
[]
|
|
731
1455
|
);
|
|
732
|
-
const sendCode =
|
|
1456
|
+
const sendCode = useCallback4(
|
|
733
1457
|
(email) => run(async () => {
|
|
734
1458
|
setChallenge(null);
|
|
735
1459
|
setChallenge((await relay.auth.emailStart(email)).challenge);
|
|
736
1460
|
}, "Couldn't send a code. Check the address and try again."),
|
|
737
1461
|
[relay, run, setChallenge]
|
|
738
1462
|
);
|
|
739
|
-
const verifyCode =
|
|
1463
|
+
const verifyCode = useCallback4(
|
|
740
1464
|
(code) => run(async () => {
|
|
741
1465
|
const sent = latest.current;
|
|
742
1466
|
if (!sent) {
|
|
@@ -747,18 +1471,18 @@ function useSignIn() {
|
|
|
747
1471
|
}, "That code didn't work. Check it, or request a new one."),
|
|
748
1472
|
[relay, run, setChallenge]
|
|
749
1473
|
);
|
|
750
|
-
const startOver =
|
|
1474
|
+
const startOver = useCallback4(() => {
|
|
751
1475
|
setChallenge(null);
|
|
752
1476
|
setError(null);
|
|
753
1477
|
}, [setChallenge]);
|
|
754
|
-
const withProvider =
|
|
1478
|
+
const withProvider = useCallback4(
|
|
755
1479
|
(provider) => run(async () => {
|
|
756
1480
|
const { url } = await relay.auth.start(provider, "sign-in");
|
|
757
1481
|
window.location.assign(url);
|
|
758
1482
|
}, "Couldn't start sign-in. Try again."),
|
|
759
1483
|
[relay, run]
|
|
760
1484
|
);
|
|
761
|
-
const withWallet =
|
|
1485
|
+
const withWallet = useCallback4(
|
|
762
1486
|
(signer) => run(async () => {
|
|
763
1487
|
await relay.auth.wallet.signIn(signer);
|
|
764
1488
|
setChallenge(null);
|
|
@@ -780,7 +1504,7 @@ function useSignIn() {
|
|
|
780
1504
|
}
|
|
781
1505
|
function useSignOut() {
|
|
782
1506
|
const relay = useRelay();
|
|
783
|
-
const [busy, setBusy] =
|
|
1507
|
+
const [busy, setBusy] = useState5(false);
|
|
784
1508
|
return {
|
|
785
1509
|
busy,
|
|
786
1510
|
signOut: async () => {
|
|
@@ -795,11 +1519,11 @@ function useSignOut() {
|
|
|
795
1519
|
}
|
|
796
1520
|
function useSignInOptions(input = {}) {
|
|
797
1521
|
const chains = JSON.stringify(cosmosChainList(input.cosmos));
|
|
798
|
-
const cosmos =
|
|
1522
|
+
const cosmos = useMemo5(
|
|
799
1523
|
() => JSON.parse(chains),
|
|
800
1524
|
[chains]
|
|
801
1525
|
);
|
|
802
|
-
const methods = useSignInMethods();
|
|
1526
|
+
const methods = useSignInMethods().data ?? null;
|
|
803
1527
|
const wallets = useSignInWallets(methods?.wallets, { cosmos });
|
|
804
1528
|
const {
|
|
805
1529
|
busy,
|
|
@@ -811,7 +1535,7 @@ function useSignInOptions(input = {}) {
|
|
|
811
1535
|
withProvider,
|
|
812
1536
|
withWallet
|
|
813
1537
|
} = useSignIn();
|
|
814
|
-
const built =
|
|
1538
|
+
const built = useMemo5(() => {
|
|
815
1539
|
if (!methods) {
|
|
816
1540
|
return null;
|
|
817
1541
|
}
|
|
@@ -841,8 +1565,7 @@ function useSignInOptions(input = {}) {
|
|
|
841
1565
|
}
|
|
842
1566
|
|
|
843
1567
|
// src/react/accounts.ts
|
|
844
|
-
import {
|
|
845
|
-
var CHANGED = "relay-accounts-changed";
|
|
1568
|
+
import { useMemo as useMemo6 } from "react";
|
|
846
1569
|
function isConnected(accounts, provider) {
|
|
847
1570
|
return Boolean(
|
|
848
1571
|
accounts?.some(
|
|
@@ -852,304 +1575,123 @@ function isConnected(accounts, provider) {
|
|
|
852
1575
|
}
|
|
853
1576
|
function useAccounts() {
|
|
854
1577
|
const relay = useRelay();
|
|
855
|
-
const
|
|
856
|
-
const
|
|
857
|
-
const
|
|
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
|
-
setBusy(false);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
return {
|
|
897
|
-
/** Null until read, and while nobody is signed in. */
|
|
898
|
-
accounts,
|
|
899
|
-
error,
|
|
900
|
-
busy,
|
|
901
|
-
refresh,
|
|
902
|
-
isConnected: (provider) => isConnected(accounts, provider),
|
|
903
|
-
connect: (provider) => run(async () => {
|
|
904
|
-
const { url } = await relay.auth.start(provider, "connect");
|
|
905
|
-
window.location.assign(url);
|
|
906
|
-
}, "Couldn't start connecting. Try again."),
|
|
907
|
-
disconnect: (provider) => run(async () => {
|
|
908
|
-
await relay.auth.disconnect(provider);
|
|
909
|
-
window.dispatchEvent(new Event(CHANGED));
|
|
910
|
-
}, "Couldn't disconnect. Try again.")
|
|
911
|
-
};
|
|
1578
|
+
const invalidate = useInvalidate();
|
|
1579
|
+
const session = useScopeSession();
|
|
1580
|
+
const query = useMemberQuery(
|
|
1581
|
+
relayKeys.accounts,
|
|
1582
|
+
(options) => relay.auth.accounts(options),
|
|
1583
|
+
{ refetchOnFocus: true }
|
|
1584
|
+
);
|
|
1585
|
+
const { busy, actionError, run } = useAction();
|
|
1586
|
+
const actions = useMemo6(
|
|
1587
|
+
() => ({
|
|
1588
|
+
connect: (provider) => run(async () => {
|
|
1589
|
+
const { url } = await relay.auth.start(provider, "connect", {
|
|
1590
|
+
session
|
|
1591
|
+
});
|
|
1592
|
+
window.location.assign(url);
|
|
1593
|
+
}),
|
|
1594
|
+
disconnect: (provider) => run(async () => {
|
|
1595
|
+
await relay.auth.disconnect(provider, { session });
|
|
1596
|
+
void invalidate([
|
|
1597
|
+
relayKeys.accounts,
|
|
1598
|
+
relayKeys.experience,
|
|
1599
|
+
relayKeys.profiles
|
|
1600
|
+
]);
|
|
1601
|
+
})
|
|
1602
|
+
}),
|
|
1603
|
+
[relay, invalidate, session, run]
|
|
1604
|
+
);
|
|
1605
|
+
const accounts = query.data;
|
|
1606
|
+
return useMemo6(
|
|
1607
|
+
() => ({
|
|
1608
|
+
...query,
|
|
1609
|
+
busy,
|
|
1610
|
+
actionError,
|
|
1611
|
+
isConnected: (provider) => isConnected(accounts, provider),
|
|
1612
|
+
...actions
|
|
1613
|
+
}),
|
|
1614
|
+
[query, busy, actionError, accounts, actions]
|
|
1615
|
+
);
|
|
912
1616
|
}
|
|
913
1617
|
|
|
914
1618
|
// src/react/profiles.ts
|
|
915
|
-
import {
|
|
916
|
-
var
|
|
1619
|
+
import { useMemo as useMemo7 } from "react";
|
|
1620
|
+
var MINE = [...relayKeys.profiles, "mine"];
|
|
917
1621
|
function useMyProfile() {
|
|
918
1622
|
const relay = useRelay();
|
|
919
|
-
const
|
|
920
|
-
const
|
|
921
|
-
const
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1623
|
+
const store = useStore();
|
|
1624
|
+
const invalidate = useInvalidate();
|
|
1625
|
+
const session = useScopeSession();
|
|
1626
|
+
const query = useMemberQuery(
|
|
1627
|
+
MINE,
|
|
1628
|
+
(options) => relay.profiles.mine(options),
|
|
1629
|
+
{ refetchOnFocus: true }
|
|
1630
|
+
);
|
|
1631
|
+
const { busy, actionError, run } = useAction();
|
|
1632
|
+
const actions = useMemo7(() => {
|
|
1633
|
+
const change = (task) => run(async () => {
|
|
1634
|
+
store.set(MINE, await task());
|
|
1635
|
+
void invalidate([
|
|
1636
|
+
[...relayKeys.profiles, "get"],
|
|
1637
|
+
relayKeys.leaderboards
|
|
929
1638
|
]);
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
if (!member) {
|
|
941
|
-
setProfile(null);
|
|
942
|
-
session.current = null;
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
const update = () => void refresh();
|
|
946
|
-
update();
|
|
947
|
-
window.addEventListener("focus", update);
|
|
948
|
-
window.addEventListener(PROFILE_CHANGED, update);
|
|
949
|
-
return () => {
|
|
950
|
-
window.removeEventListener("focus", update);
|
|
951
|
-
window.removeEventListener(PROFILE_CHANGED, update);
|
|
952
|
-
};
|
|
953
|
-
}, [member, refresh]);
|
|
954
|
-
async function run(task, fallback) {
|
|
955
|
-
setBusy(true);
|
|
956
|
-
setError(null);
|
|
957
|
-
try {
|
|
958
|
-
const value = await task(session.current ?? void 0);
|
|
959
|
-
setProfile(value);
|
|
960
|
-
window.dispatchEvent(new Event(PROFILE_CHANGED));
|
|
961
|
-
return true;
|
|
962
|
-
} catch (cause) {
|
|
963
|
-
setError(
|
|
964
|
-
cause instanceof Error && cause.message ? cause.message : fallback
|
|
965
|
-
);
|
|
966
|
-
return false;
|
|
967
|
-
} finally {
|
|
968
|
-
setBusy(false);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
return {
|
|
972
|
-
/** Null until read, and while nobody is signed in. */
|
|
973
|
-
profile,
|
|
974
|
-
error,
|
|
975
|
-
busy,
|
|
976
|
-
refresh,
|
|
977
|
-
/** Resolves true once saved; a failure sets `error`. */
|
|
978
|
-
update: (input) => run(
|
|
979
|
-
(value) => relay.profiles.update(input, { session: value }),
|
|
980
|
-
"Couldn't save your profile. Try again."
|
|
981
|
-
),
|
|
982
|
-
/** Uploads a JPEG, PNG or WebP of at most 5 MB as the avatar; resolves true once saved. */
|
|
983
|
-
uploadAvatar: (file) => run(
|
|
984
|
-
(value) => relay.profiles.uploadAvatar(file, {
|
|
985
|
-
uploadId: crypto.randomUUID(),
|
|
986
|
-
session: value
|
|
987
|
-
}),
|
|
988
|
-
"Couldn't upload this picture. Try again."
|
|
989
|
-
)
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
function useProfileRead(read, intervalMs, fallback) {
|
|
993
|
-
const { member } = useExperience();
|
|
994
|
-
const [state, setState] = useState8({
|
|
995
|
-
status: "loading"
|
|
996
|
-
});
|
|
997
|
-
const refresh = useCallback5(async () => {
|
|
998
|
-
try {
|
|
999
|
-
const value = await read();
|
|
1000
|
-
setState({ status: "ready", value, stale: null });
|
|
1001
|
-
} catch (cause) {
|
|
1002
|
-
if (cause instanceof RelayError && cause.status === 404) {
|
|
1003
|
-
setState({ status: "missing" });
|
|
1004
|
-
return;
|
|
1005
|
-
}
|
|
1006
|
-
const message = cause instanceof Error && cause.message ? cause.message : fallback;
|
|
1007
|
-
setState(
|
|
1008
|
-
(previous) => previous.status === "ready" ? { ...previous, stale: message } : { status: "error", message }
|
|
1009
|
-
);
|
|
1010
|
-
}
|
|
1011
|
-
}, [read, fallback]);
|
|
1012
|
-
useEffect7(() => {
|
|
1013
|
-
setState({ status: "loading" });
|
|
1014
|
-
void refresh();
|
|
1015
|
-
const timer = setInterval(() => {
|
|
1016
|
-
if (document.visibilityState === "visible") {
|
|
1017
|
-
void refresh();
|
|
1018
|
-
}
|
|
1019
|
-
}, intervalMs);
|
|
1020
|
-
const changed = () => void refresh();
|
|
1021
|
-
window.addEventListener(PROFILE_CHANGED, changed);
|
|
1022
|
-
return () => {
|
|
1023
|
-
clearInterval(timer);
|
|
1024
|
-
window.removeEventListener(PROFILE_CHANGED, changed);
|
|
1639
|
+
});
|
|
1640
|
+
return {
|
|
1641
|
+
update: (input) => change(() => relay.profiles.update(input, { session })),
|
|
1642
|
+
/** Uploads a JPEG, PNG or WebP of at most 5 MB as the avatar. */
|
|
1643
|
+
uploadAvatar: (file) => change(
|
|
1644
|
+
() => relay.profiles.uploadAvatar(file, {
|
|
1645
|
+
uploadId: crypto.randomUUID(),
|
|
1646
|
+
session
|
|
1647
|
+
})
|
|
1648
|
+
)
|
|
1025
1649
|
};
|
|
1026
|
-
}, [
|
|
1027
|
-
return
|
|
1650
|
+
}, [relay, store, invalidate, session, run]);
|
|
1651
|
+
return useMemo7(
|
|
1652
|
+
() => ({ ...query, busy, actionError, ...actions }),
|
|
1653
|
+
[query, busy, actionError, actions]
|
|
1654
|
+
);
|
|
1028
1655
|
}
|
|
1029
1656
|
function useProfile(id, options = {}) {
|
|
1030
1657
|
const relay = useRelay();
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
options.intervalMs ?? 6e4
|
|
1035
|
-
"Could not load the profile."
|
|
1658
|
+
return useRelayQuery(
|
|
1659
|
+
[...relayKeys.profiles, "get", id],
|
|
1660
|
+
(request) => relay.profiles.get(id, request),
|
|
1661
|
+
{ intervalMs: options.intervalMs ?? 6e4 }
|
|
1036
1662
|
);
|
|
1037
|
-
const value = state.status === "ready" ? { status: "ready", profile: state.value, stale: state.stale } : state;
|
|
1038
|
-
return { ...value, refresh };
|
|
1039
1663
|
}
|
|
1040
1664
|
function useLeaderboardPlacement(board, profile, options = {}) {
|
|
1041
1665
|
const relay = useRelay();
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
const { state, refresh } = useProfileRead(
|
|
1047
|
-
read,
|
|
1048
|
-
options.intervalMs ?? 3e4,
|
|
1049
|
-
"Could not load the leaderboard placement."
|
|
1666
|
+
return useRelayQuery(
|
|
1667
|
+
[...relayKeys.leaderboards, board, "placement", profile],
|
|
1668
|
+
(request) => relay.leaderboards.placement(board, profile, request),
|
|
1669
|
+
{ intervalMs: options.intervalMs ?? 3e4 }
|
|
1050
1670
|
);
|
|
1051
|
-
return { ...state, refresh };
|
|
1052
1671
|
}
|
|
1053
1672
|
function useProfileReferral(profile, program, options = {}) {
|
|
1054
1673
|
const relay = useRelay();
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
const { state, refresh } = useProfileRead(
|
|
1060
|
-
read,
|
|
1061
|
-
options.intervalMs ?? 6e4,
|
|
1062
|
-
"Could not load referral progress."
|
|
1674
|
+
return useMemberQuery(
|
|
1675
|
+
[...relayKeys.profiles, "referral", profile, program],
|
|
1676
|
+
(request) => relay.profiles.referral(profile, program, request),
|
|
1677
|
+
{ intervalMs: options.intervalMs ?? 6e4 }
|
|
1063
1678
|
);
|
|
1064
|
-
return { ...state, refresh };
|
|
1065
1679
|
}
|
|
1066
1680
|
function useProfileCompletions(profile, options = {}) {
|
|
1067
1681
|
const relay = useRelay();
|
|
1068
|
-
const { member } = useExperience();
|
|
1069
1682
|
const { limit = 20 } = options;
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
async (first) => {
|
|
1078
|
-
if (first) {
|
|
1079
|
-
generation.current++;
|
|
1080
|
-
paging.current = { ready: false, busy: false };
|
|
1081
|
-
} else {
|
|
1082
|
-
if (!paging.current.ready || paging.current.busy || cursor.current === null) {
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
paging.current.busy = true;
|
|
1086
|
-
setState(
|
|
1087
|
-
(previous) => previous.status === "ready" ? { ...previous, loading: true, error: null } : previous
|
|
1088
|
-
);
|
|
1089
|
-
}
|
|
1090
|
-
const current = generation.current;
|
|
1091
|
-
const own = paging.current;
|
|
1092
|
-
try {
|
|
1093
|
-
const page = await relay.profiles.completions(profile, {
|
|
1094
|
-
limit,
|
|
1095
|
-
...!first && cursor.current ? { cursor: cursor.current } : {}
|
|
1096
|
-
});
|
|
1097
|
-
if (current !== generation.current) {
|
|
1098
|
-
return;
|
|
1099
|
-
}
|
|
1100
|
-
cursor.current = page.nextCursor;
|
|
1101
|
-
own.ready = true;
|
|
1102
|
-
setState((previous) => ({
|
|
1103
|
-
status: "ready",
|
|
1104
|
-
items: !first && previous.status === "ready" ? [...previous.items, ...page.items] : page.items,
|
|
1105
|
-
more: page.nextCursor !== null,
|
|
1106
|
-
loading: false,
|
|
1107
|
-
error: null
|
|
1108
|
-
}));
|
|
1109
|
-
} catch (cause) {
|
|
1110
|
-
if (current !== generation.current) {
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
if (first && cause instanceof RelayError && cause.status === 404) {
|
|
1114
|
-
setState({ status: "missing" });
|
|
1115
|
-
return;
|
|
1116
|
-
}
|
|
1117
|
-
const message = cause instanceof Error && cause.message ? cause.message : "Could not load completed quests.";
|
|
1118
|
-
setState(
|
|
1119
|
-
(previous) => !first && previous.status === "ready" ? { ...previous, loading: false, error: message } : { status: "error", message }
|
|
1120
|
-
);
|
|
1121
|
-
} finally {
|
|
1122
|
-
if (!first) {
|
|
1123
|
-
own.busy = false;
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
},
|
|
1127
|
-
[relay, profile, limit]
|
|
1683
|
+
return useRelayPages(
|
|
1684
|
+
[...relayKeys.profiles, "completions", profile, limit],
|
|
1685
|
+
({ cursor, ...request }) => relay.profiles.completions(
|
|
1686
|
+
profile,
|
|
1687
|
+
cursor ? { limit, cursor } : { limit },
|
|
1688
|
+
request
|
|
1689
|
+
)
|
|
1128
1690
|
);
|
|
1129
|
-
useEffect7(() => {
|
|
1130
|
-
setState({ status: "loading" });
|
|
1131
|
-
cursor.current = null;
|
|
1132
|
-
void load(true);
|
|
1133
|
-
const changed = () => void load(true);
|
|
1134
|
-
window.addEventListener(PROFILE_CHANGED, changed);
|
|
1135
|
-
return () => {
|
|
1136
|
-
generation.current++;
|
|
1137
|
-
window.removeEventListener(PROFILE_CHANGED, changed);
|
|
1138
|
-
};
|
|
1139
|
-
}, [load, member]);
|
|
1140
|
-
return {
|
|
1141
|
-
...state,
|
|
1142
|
-
refresh: () => load(true),
|
|
1143
|
-
/**
|
|
1144
|
-
* Appends the next page; does nothing while one loads, while the first page reloads, or when
|
|
1145
|
-
* there is none.
|
|
1146
|
-
*/
|
|
1147
|
-
loadMore: () => load(false)
|
|
1148
|
-
};
|
|
1149
1691
|
}
|
|
1150
1692
|
|
|
1151
1693
|
// src/react/referrals.ts
|
|
1152
|
-
import { useCallback as
|
|
1694
|
+
import { useCallback as useCallback5, useEffect as useEffect6, useMemo as useMemo8, useRef as useRef3, useState as useState6 } from "react";
|
|
1153
1695
|
var PENDING = "domino:pending-referral";
|
|
1154
1696
|
var CODE = /^[a-zA-Z0-9_-]{1,100}$/;
|
|
1155
1697
|
function captureReferralCode(param = "ref") {
|
|
@@ -1174,34 +1716,36 @@ function forgetReferralCode() {
|
|
|
1174
1716
|
}
|
|
1175
1717
|
function useInvitation(param = "ref") {
|
|
1176
1718
|
const relay = useRelay();
|
|
1177
|
-
const
|
|
1178
|
-
const
|
|
1719
|
+
const invalidate = useInvalidate();
|
|
1720
|
+
const { member } = useSession();
|
|
1721
|
+
const [state, setState] = useState6(() => {
|
|
1179
1722
|
const code = captureReferralCode(param);
|
|
1180
1723
|
return code ? { status: "pending", code } : { status: "none" };
|
|
1181
1724
|
});
|
|
1182
|
-
const started =
|
|
1183
|
-
const accept =
|
|
1725
|
+
const started = useRef3(null);
|
|
1726
|
+
const accept = useCallback5(
|
|
1184
1727
|
async (code) => {
|
|
1185
1728
|
setState({ status: "accepting", code });
|
|
1186
1729
|
try {
|
|
1187
1730
|
await relay.referrals.accept({ code, actionId: `accept-${code}` });
|
|
1188
1731
|
forgetReferralCode();
|
|
1189
1732
|
setState({ status: "accepted", code });
|
|
1733
|
+
void invalidate([relayKeys.referrals, relayKeys.profiles]);
|
|
1190
1734
|
} catch (cause) {
|
|
1191
|
-
const refused = cause
|
|
1735
|
+
const refused = isRelayError(cause) && (cause.kind === "http" && cause.status !== void 0 && cause.status >= 400 && cause.status < 500 || cause.code === RelayErrorCode.InvalidInput);
|
|
1192
1736
|
if (refused) {
|
|
1193
1737
|
forgetReferralCode();
|
|
1194
1738
|
}
|
|
1195
1739
|
setState({
|
|
1196
1740
|
status: refused ? "rejected" : "failed",
|
|
1197
1741
|
code,
|
|
1198
|
-
|
|
1742
|
+
error: asRelayError(cause, "That invitation code couldn't be used.")
|
|
1199
1743
|
});
|
|
1200
1744
|
}
|
|
1201
1745
|
},
|
|
1202
|
-
[relay]
|
|
1746
|
+
[relay, invalidate]
|
|
1203
1747
|
);
|
|
1204
|
-
|
|
1748
|
+
useEffect6(() => {
|
|
1205
1749
|
if (member && state.status === "pending" && started.current !== `${member}:${state.code}`) {
|
|
1206
1750
|
started.current = `${member}:${state.code}`;
|
|
1207
1751
|
void accept(state.code);
|
|
@@ -1211,255 +1755,90 @@ function useInvitation(param = "ref") {
|
|
|
1211
1755
|
}
|
|
1212
1756
|
function useReferralSummary(options = {}) {
|
|
1213
1757
|
const relay = useRelay();
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
);
|
|
1225
|
-
}
|
|
1226
|
-
}, [relay]);
|
|
1227
|
-
useEffect8(() => {
|
|
1228
|
-
setSummary(null);
|
|
1229
|
-
if (member) {
|
|
1230
|
-
void refresh();
|
|
1758
|
+
const query = useMemberQuery(
|
|
1759
|
+
[...relayKeys.referrals, "summary"],
|
|
1760
|
+
(request) => relay.referrals.summary(request)
|
|
1761
|
+
);
|
|
1762
|
+
const { path = "/", param = "ref" } = options;
|
|
1763
|
+
return useMemo8(() => {
|
|
1764
|
+
let link = null;
|
|
1765
|
+
if (query.data && typeof window !== "undefined") {
|
|
1766
|
+
const url = new URL(path, window.location.origin);
|
|
1767
|
+
url.searchParams.set(param, query.data.code);
|
|
1768
|
+
link = url.toString();
|
|
1231
1769
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
if (summary && typeof window !== "undefined") {
|
|
1235
|
-
const url = new URL(options.path ?? "/", window.location.origin);
|
|
1236
|
-
url.searchParams.set(options.param ?? "ref", summary.code);
|
|
1237
|
-
link = url.toString();
|
|
1238
|
-
}
|
|
1239
|
-
return { summary, link, error, refresh };
|
|
1770
|
+
return { ...query, link };
|
|
1771
|
+
}, [query, path, param]);
|
|
1240
1772
|
}
|
|
1241
1773
|
|
|
1242
1774
|
// src/react/leaderboards.ts
|
|
1243
|
-
import { useCallback as useCallback7, useEffect as useEffect9, useState as useState10 } from "react";
|
|
1244
1775
|
function useLeaderboard(board, options = {}) {
|
|
1245
1776
|
const relay = useRelay();
|
|
1246
|
-
const { member } = useExperience();
|
|
1247
1777
|
const { limit = 25, intervalMs = 3e4 } = options;
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
} catch (cause) {
|
|
1254
|
-
const message = cause instanceof Error && cause.message ? cause.message : "Could not load the leaderboard.";
|
|
1255
|
-
setState(
|
|
1256
|
-
(previous) => previous.status === "ready" ? { ...previous, stale: message } : { status: "error", message }
|
|
1257
|
-
);
|
|
1258
|
-
}
|
|
1259
|
-
}, [relay, board, limit]);
|
|
1260
|
-
useEffect9(() => {
|
|
1261
|
-
void refresh();
|
|
1262
|
-
const timer = setInterval(() => {
|
|
1263
|
-
if (document.visibilityState === "visible") {
|
|
1264
|
-
void refresh();
|
|
1265
|
-
}
|
|
1266
|
-
}, intervalMs);
|
|
1267
|
-
return () => clearInterval(timer);
|
|
1268
|
-
}, [refresh, member, intervalMs]);
|
|
1269
|
-
return { ...state, refresh };
|
|
1778
|
+
return useRelayQuery(
|
|
1779
|
+
[...relayKeys.leaderboards, board, "standings", limit],
|
|
1780
|
+
(request) => relay.leaderboards.standings(board, { limit }, request),
|
|
1781
|
+
{ intervalMs }
|
|
1782
|
+
);
|
|
1270
1783
|
}
|
|
1271
1784
|
|
|
1272
1785
|
// src/react/balances.ts
|
|
1273
|
-
import { useCallback as useCallback8, useEffect as useEffect10, useRef as useRef5, useState as useState11 } from "react";
|
|
1274
|
-
var signedOut = (cause) => cause instanceof RelayError && cause.status === 401;
|
|
1275
|
-
var messageOf = (cause, fallback) => cause instanceof Error && cause.message ? cause.message : fallback;
|
|
1276
|
-
function useCompletions() {
|
|
1277
|
-
const { experience, member } = useExperience();
|
|
1278
|
-
const mark = experience.status === "ready" ? completionMark([
|
|
1279
|
-
...experience.quests,
|
|
1280
|
-
...experience.collections.flatMap((c) => c.quests)
|
|
1281
|
-
]) : "";
|
|
1282
|
-
return { member, mark };
|
|
1283
|
-
}
|
|
1284
1786
|
function useBalances() {
|
|
1285
1787
|
const relay = useRelay();
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
const current = ++generation.current;
|
|
1291
|
-
try {
|
|
1292
|
-
const { items } = await relay.me.balances();
|
|
1293
|
-
if (current === generation.current) {
|
|
1294
|
-
setState({ status: "ready", items, stale: null });
|
|
1295
|
-
}
|
|
1296
|
-
} catch (cause) {
|
|
1297
|
-
if (current !== generation.current) {
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
if (signedOut(cause)) {
|
|
1301
|
-
setState({ status: "signed-out" });
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
const message = messageOf(cause, "Could not load your points.");
|
|
1305
|
-
setState(
|
|
1306
|
-
(previous) => previous.status === "ready" ? { ...previous, stale: message } : { status: "error", message }
|
|
1307
|
-
);
|
|
1308
|
-
}
|
|
1309
|
-
}, [relay]);
|
|
1310
|
-
const shown = useRef5(null);
|
|
1311
|
-
useEffect10(() => {
|
|
1312
|
-
if (shown.current !== null && shown.current !== member) {
|
|
1313
|
-
setState({ status: "loading" });
|
|
1314
|
-
}
|
|
1315
|
-
shown.current = member;
|
|
1316
|
-
void refresh();
|
|
1317
|
-
}, [refresh, member, mark]);
|
|
1318
|
-
return { ...state, refresh };
|
|
1788
|
+
return useMemberQuery(
|
|
1789
|
+
relayKeys.balances,
|
|
1790
|
+
(options) => relay.me.balances(options)
|
|
1791
|
+
);
|
|
1319
1792
|
}
|
|
1320
1793
|
function usePointHistory(ledger) {
|
|
1321
1794
|
const relay = useRelay();
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
const cursor = useRef5(null);
|
|
1326
|
-
const generation = useRef5(0);
|
|
1327
|
-
const paging = useRef5({ ready: false, busy: false });
|
|
1328
|
-
const load = useCallback8(
|
|
1329
|
-
async (first) => {
|
|
1330
|
-
if (first) {
|
|
1331
|
-
generation.current++;
|
|
1332
|
-
paging.current = { ready: false, busy: false };
|
|
1333
|
-
} else {
|
|
1334
|
-
if (!paging.current.ready || paging.current.busy || cursor.current === null) {
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
paging.current.busy = true;
|
|
1338
|
-
setState(
|
|
1339
|
-
(previous) => previous.status === "ready" ? { ...previous, loading: true, error: null } : previous
|
|
1340
|
-
);
|
|
1341
|
-
}
|
|
1342
|
-
const current = generation.current;
|
|
1343
|
-
const own = paging.current;
|
|
1344
|
-
try {
|
|
1345
|
-
const page = await relay.me.pointHistory({
|
|
1346
|
-
ledger,
|
|
1347
|
-
...!first && cursor.current ? { cursor: cursor.current } : {}
|
|
1348
|
-
});
|
|
1349
|
-
if (current !== generation.current) {
|
|
1350
|
-
return;
|
|
1351
|
-
}
|
|
1352
|
-
cursor.current = page.nextCursor;
|
|
1353
|
-
own.ready = true;
|
|
1354
|
-
setState((previous) => ({
|
|
1355
|
-
status: "ready",
|
|
1356
|
-
items: !first && previous.status === "ready" ? [...previous.items, ...page.items] : page.items,
|
|
1357
|
-
more: page.nextCursor !== null,
|
|
1358
|
-
loading: false,
|
|
1359
|
-
error: null
|
|
1360
|
-
}));
|
|
1361
|
-
} catch (cause) {
|
|
1362
|
-
if (current !== generation.current) {
|
|
1363
|
-
return;
|
|
1364
|
-
}
|
|
1365
|
-
if (first && signedOut(cause)) {
|
|
1366
|
-
setState({ status: "signed-out" });
|
|
1367
|
-
return;
|
|
1368
|
-
}
|
|
1369
|
-
const code = cause instanceof RelayError ? cause.code : void 0;
|
|
1370
|
-
if (first && code === RelayErrorCode.PointLedgerNotFound) {
|
|
1371
|
-
setState({ status: "missing" });
|
|
1372
|
-
return;
|
|
1373
|
-
}
|
|
1374
|
-
if (first && code === RelayErrorCode.NotJoined) {
|
|
1375
|
-
setState({ status: "not-joined" });
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
const message = messageOf(cause, "Could not load your point history.");
|
|
1379
|
-
setState(
|
|
1380
|
-
(previous) => !first && previous.status === "ready" ? { ...previous, loading: false, error: message } : { status: "error", message }
|
|
1381
|
-
);
|
|
1382
|
-
} finally {
|
|
1383
|
-
own.busy = false;
|
|
1384
|
-
}
|
|
1385
|
-
},
|
|
1386
|
-
[relay, ledger]
|
|
1795
|
+
return useMemberPages(
|
|
1796
|
+
[...relayKeys.balances, "history", ledger],
|
|
1797
|
+
({ cursor, ...options }) => relay.me.pointHistory(cursor ? { ledger, cursor } : { ledger }, options)
|
|
1387
1798
|
);
|
|
1388
|
-
useEffect10(() => {
|
|
1389
|
-
const previous = shown.current;
|
|
1390
|
-
if (previous && (previous.ledger !== ledger || previous.member !== null && previous.member !== member)) {
|
|
1391
|
-
setState({ status: "loading" });
|
|
1392
|
-
}
|
|
1393
|
-
shown.current = { member, ledger };
|
|
1394
|
-
cursor.current = null;
|
|
1395
|
-
void load(true);
|
|
1396
|
-
return () => {
|
|
1397
|
-
generation.current++;
|
|
1398
|
-
};
|
|
1399
|
-
}, [load, ledger, member, mark]);
|
|
1400
|
-
return {
|
|
1401
|
-
...state,
|
|
1402
|
-
refresh: () => load(true),
|
|
1403
|
-
/** Appends the next page; does nothing while a page loads or when there is none. */
|
|
1404
|
-
loadMore: () => load(false)
|
|
1405
|
-
};
|
|
1406
1799
|
}
|
|
1407
1800
|
|
|
1408
1801
|
// src/react/references.ts
|
|
1409
|
-
import { useCallback as useCallback9, useEffect as useEffect11, useState as useState12 } from "react";
|
|
1410
|
-
function useReferences(read, failure) {
|
|
1411
|
-
const relay = useRelay();
|
|
1412
|
-
const [state, setState] = useState12({ status: "loading" });
|
|
1413
|
-
const refresh = useCallback9(async () => {
|
|
1414
|
-
try {
|
|
1415
|
-
setState({ status: "ready", items: await read(relay) });
|
|
1416
|
-
} catch (cause) {
|
|
1417
|
-
setState({
|
|
1418
|
-
status: "error",
|
|
1419
|
-
message: cause instanceof Error && cause.message ? cause.message : failure
|
|
1420
|
-
});
|
|
1421
|
-
}
|
|
1422
|
-
}, [relay, read, failure]);
|
|
1423
|
-
useEffect11(() => {
|
|
1424
|
-
void refresh();
|
|
1425
|
-
}, [refresh]);
|
|
1426
|
-
return { ...state, refresh };
|
|
1427
|
-
}
|
|
1428
|
-
var readPointLedgers = (relay) => relay.pointLedgers.list();
|
|
1429
|
-
var readTieredRewards = (relay) => relay.tieredRewards.list();
|
|
1430
1802
|
function usePointLedgers() {
|
|
1431
|
-
|
|
1803
|
+
const relay = useRelay();
|
|
1804
|
+
return useRelayQuery(
|
|
1805
|
+
relayKeys.pointLedgers,
|
|
1806
|
+
(options) => relay.pointLedgers.list(options)
|
|
1807
|
+
);
|
|
1432
1808
|
}
|
|
1433
1809
|
function useTieredRewards() {
|
|
1434
|
-
|
|
1810
|
+
const relay = useRelay();
|
|
1811
|
+
return useRelayQuery(
|
|
1812
|
+
relayKeys.tieredRewards,
|
|
1813
|
+
(options) => relay.tieredRewards.list(options)
|
|
1814
|
+
);
|
|
1435
1815
|
}
|
|
1436
1816
|
export {
|
|
1437
|
-
PENDING_POLL_MS,
|
|
1438
|
-
PROFILE_CHANGED,
|
|
1439
|
-
RETRY_DELAYS_MS,
|
|
1440
1817
|
RelayProvider,
|
|
1441
1818
|
canRepeat,
|
|
1442
1819
|
canSubmit,
|
|
1443
1820
|
captureReferralCode,
|
|
1444
1821
|
completionCountLabel,
|
|
1445
|
-
completionMark,
|
|
1446
|
-
hasPending,
|
|
1447
1822
|
isAutomatic,
|
|
1448
1823
|
isConnected,
|
|
1449
1824
|
isFinished,
|
|
1450
1825
|
progressOf,
|
|
1451
1826
|
questGroups,
|
|
1452
1827
|
questStatus,
|
|
1828
|
+
questStatusText,
|
|
1829
|
+
relayKeys,
|
|
1453
1830
|
roleDeliveryLabel,
|
|
1454
|
-
|
|
1455
|
-
sanitizeDeliveries,
|
|
1831
|
+
roleDeliveryStatus,
|
|
1456
1832
|
useAccounts,
|
|
1457
1833
|
useBalances,
|
|
1458
1834
|
useExperience,
|
|
1835
|
+
useInvalidate,
|
|
1459
1836
|
useInvitation,
|
|
1460
1837
|
useLeaderboard,
|
|
1461
1838
|
useLeaderboardPlacement,
|
|
1462
1839
|
useMe,
|
|
1840
|
+
useMemberPages,
|
|
1841
|
+
useMemberQuery,
|
|
1463
1842
|
useMyProfile,
|
|
1464
1843
|
usePointHistory,
|
|
1465
1844
|
usePointLedgers,
|
|
@@ -1469,6 +1848,9 @@ export {
|
|
|
1469
1848
|
useQuest,
|
|
1470
1849
|
useReferralSummary,
|
|
1471
1850
|
useRelay,
|
|
1851
|
+
useRelayPages,
|
|
1852
|
+
useRelayQuery,
|
|
1853
|
+
useSession,
|
|
1472
1854
|
useSignIn,
|
|
1473
1855
|
useSignInMethods,
|
|
1474
1856
|
useSignInOptions,
|
|
@@ -1476,7 +1858,5 @@ export {
|
|
|
1476
1858
|
useSignOut,
|
|
1477
1859
|
useTieredRewards,
|
|
1478
1860
|
useWalletDiscovery,
|
|
1479
|
-
useWallets
|
|
1480
|
-
visibleQuests,
|
|
1481
|
-
watchRoleDeliveries
|
|
1861
|
+
useWallets
|
|
1482
1862
|
};
|