@domino-sdk/relay 1.0.0-next.2 → 1.0.0-next.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/build.mjs +18 -1
- package/dist/activity-B8k9Sa_a.d.ts +9112 -0
- package/dist/authoring.d.ts +1355 -182
- package/dist/authoring.js +658 -225
- package/dist/automatic-outcomes-B1xrETxm.d.ts +758 -0
- package/dist/avatar.d.ts +29 -0
- package/dist/avatar.js +21 -0
- package/dist/browser-J7tUpXxy.d.ts +4154 -0
- package/dist/browser.d.ts +11 -4580
- package/dist/browser.js +8 -6
- package/dist/capability-error-DNMwKmlL.d.ts +458 -0
- package/dist/chunk-36HTX2BM.js +1223 -0
- package/dist/chunk-6QJ3LADK.js +112 -0
- package/dist/chunk-BGGKNIRP.js +171 -0
- package/dist/chunk-EYH5RDIE.js +496 -0
- package/dist/chunk-GQKYAQM6.js +84 -0
- package/dist/chunk-GUP7YRR4.js +42 -0
- package/dist/chunk-GZ6FJAXN.js +1659 -0
- package/dist/chunk-JON5SESL.js +2986 -0
- package/dist/chunk-MC6V6VMG.js +49 -0
- package/dist/chunk-NPGENI7K.js +37 -0
- package/dist/chunk-V7UKZLHA.js +35 -0
- package/dist/chunk-VFHHLN6A.js +284 -0
- package/dist/chunk-WJSSJ7AF.js +151 -0
- package/dist/{chunk-VH4OQ5VX.js → chunk-XFMSUPNS.js} +26 -5
- package/dist/{chunk-FDWRBMB3.js → chunk-ZVIRG24J.js} +150 -33
- package/dist/client-XiuFnug_.d.ts +4262 -0
- package/dist/groups-Ck-OxzjD.d.ts +2449 -0
- package/dist/identity-integration-GxAHdKR2.d.ts +58 -0
- package/dist/images-Dv-P5VBw.d.ts +234 -0
- package/dist/index.d.ts +15 -13115
- package/dist/index.js +30 -689
- package/dist/internal.d.ts +159 -0
- package/dist/internal.js +79 -0
- package/dist/offers-Cy-e4o5x.d.ts +431 -0
- package/dist/outcomes-kVB-Juzs.d.ts +4902 -0
- package/dist/pages-BBYD21IE.d.ts +359 -0
- package/dist/participant-rewards-3duLb8YJ.d.ts +668 -0
- package/dist/portal-proxy.js +26 -9
- package/dist/profiles-D2aFcOxu.d.ts +297 -0
- package/dist/react/index.d.ts +3378 -3318
- package/dist/react/index.js +1635 -783
- package/dist/request-error-DWduJCGV.d.ts +62 -0
- package/dist/schema-DfJzH2ci.d.ts +5644 -0
- package/dist/schema.d.ts +15 -3388
- package/dist/schema.js +790 -3
- package/dist/server/index.d.ts +81498 -0
- package/dist/server/index.js +2520 -0
- package/dist/streaks-CWyq8Px5.d.ts +71 -0
- package/dist/submission-input-BZkm6ohU.d.ts +290 -0
- package/dist/units-CwgsPB_r.d.ts +9 -0
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +1 -0
- package/dist/wallet-YssWxGg8.d.ts +228 -0
- package/package.json +24 -5
- package/dist/chunk-4KNTMNXA.js +0 -1847
- package/dist/chunk-B76BAJ25.js +0 -624
- package/dist/chunk-NKGJEYZM.js +0 -1833
- package/dist/chunk-ONDBQSMU.js +0 -373
- package/dist/submission-input-JDwq3EVK.d.ts +0 -4986
package/dist/react/index.js
CHANGED
|
@@ -8,45 +8,401 @@ import {
|
|
|
8
8
|
discoverSolanaWallets,
|
|
9
9
|
readSignInMethods,
|
|
10
10
|
signInOptionsFrom
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-ZVIRG24J.js";
|
|
12
|
+
import "../chunk-GZ6FJAXN.js";
|
|
13
|
+
import "../chunk-EYH5RDIE.js";
|
|
12
14
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import "../chunk-
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
RelayErrorCode,
|
|
16
|
+
asRelayError,
|
|
17
|
+
isRelayError
|
|
18
|
+
} from "../chunk-36HTX2BM.js";
|
|
19
|
+
import "../chunk-6QJ3LADK.js";
|
|
20
|
+
import "../chunk-NPGENI7K.js";
|
|
21
|
+
import {
|
|
22
|
+
sessionMember
|
|
23
|
+
} from "../chunk-XFMSUPNS.js";
|
|
24
|
+
import "../chunk-JON5SESL.js";
|
|
22
25
|
|
|
23
|
-
// src/react/
|
|
24
|
-
import {
|
|
26
|
+
// src/react/activity.ts
|
|
27
|
+
import {
|
|
28
|
+
useCallback,
|
|
29
|
+
useEffect,
|
|
30
|
+
useLayoutEffect,
|
|
31
|
+
useRef,
|
|
32
|
+
useState,
|
|
33
|
+
useSyncExternalStore
|
|
34
|
+
} from "react";
|
|
35
|
+
|
|
36
|
+
// src/react/cache.ts
|
|
37
|
+
var LOADING = Object.freeze({
|
|
38
|
+
status: "loading",
|
|
39
|
+
data: void 0,
|
|
40
|
+
error: null
|
|
41
|
+
});
|
|
42
|
+
var UNUSED_MS = 5 * 6e4;
|
|
43
|
+
var STALE_MS = 3e4;
|
|
44
|
+
var relayError = (cause, fallback) => asRelayError(cause, fallback);
|
|
45
|
+
function hashKey(key) {
|
|
46
|
+
return JSON.stringify(key);
|
|
47
|
+
}
|
|
48
|
+
function startsWith(key, prefix) {
|
|
49
|
+
return prefix.every((part, index) => key[index] === part);
|
|
50
|
+
}
|
|
51
|
+
function visible() {
|
|
52
|
+
return typeof document === "undefined" || document.visibilityState !== "hidden";
|
|
53
|
+
}
|
|
54
|
+
var QueryCache = class {
|
|
55
|
+
session;
|
|
56
|
+
#entries = /* @__PURE__ */ new Map();
|
|
57
|
+
#disposed = false;
|
|
58
|
+
#onStaleSession;
|
|
59
|
+
constructor(session, onStaleSession) {
|
|
60
|
+
this.session = session;
|
|
61
|
+
this.#onStaleSession = onStaleSession;
|
|
62
|
+
}
|
|
63
|
+
get disposed() {
|
|
64
|
+
return this.#disposed;
|
|
65
|
+
}
|
|
66
|
+
snapshot(key) {
|
|
67
|
+
return this.#entries.get(hashKey(key))?.state ?? LOADING;
|
|
68
|
+
}
|
|
69
|
+
/** Starts observing `key`, reading it now unless a read is cached or in flight. */
|
|
70
|
+
subscribe(key, read, options, listener) {
|
|
71
|
+
if (this.#disposed) {
|
|
72
|
+
return () => {
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const hash = hashKey(key);
|
|
76
|
+
let entry = this.#entries.get(hash);
|
|
77
|
+
if (!entry) {
|
|
78
|
+
entry = {
|
|
79
|
+
key,
|
|
80
|
+
read,
|
|
81
|
+
state: LOADING,
|
|
82
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
83
|
+
intervals: /* @__PURE__ */ new Map(),
|
|
84
|
+
focus: /* @__PURE__ */ new Set(),
|
|
85
|
+
controller: null,
|
|
86
|
+
pending: null,
|
|
87
|
+
settledAt: 0,
|
|
88
|
+
version: 0,
|
|
89
|
+
timer: void 0,
|
|
90
|
+
unused: void 0
|
|
91
|
+
};
|
|
92
|
+
this.#entries.set(hash, entry);
|
|
93
|
+
}
|
|
94
|
+
const current = entry;
|
|
95
|
+
const token = {};
|
|
96
|
+
const unseen = current.listeners.size === 0;
|
|
97
|
+
current.read = read;
|
|
98
|
+
clearTimeout(current.unused);
|
|
99
|
+
current.listeners.add(listener);
|
|
100
|
+
if (options.intervalMs !== void 0 && options.intervalMs > 0) {
|
|
101
|
+
current.intervals.set(token, options.intervalMs);
|
|
102
|
+
}
|
|
103
|
+
if (options.refetchOnFocus) {
|
|
104
|
+
current.focus.add(token);
|
|
105
|
+
}
|
|
106
|
+
if (!current.pending && (current.settledAt === 0 || unseen && Date.now() - current.settledAt >= STALE_MS)) {
|
|
107
|
+
void this.#fetch(current, false);
|
|
108
|
+
} else {
|
|
109
|
+
this.#schedule(current);
|
|
110
|
+
}
|
|
111
|
+
return () => {
|
|
112
|
+
current.listeners.delete(listener);
|
|
113
|
+
current.intervals.delete(token);
|
|
114
|
+
current.focus.delete(token);
|
|
115
|
+
if (current.listeners.size === 0 && !this.#disposed) {
|
|
116
|
+
clearTimeout(current.timer);
|
|
117
|
+
current.unused = setTimeout(() => {
|
|
118
|
+
if (current.listeners.size === 0) {
|
|
119
|
+
this.#drop(hash, current);
|
|
120
|
+
}
|
|
121
|
+
}, UNUSED_MS);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Reads `key` again now, replacing any read in flight. */
|
|
126
|
+
refresh(key) {
|
|
127
|
+
const entry = this.#entries.get(hashKey(key));
|
|
128
|
+
return entry ? this.#fetch(entry, true) : Promise.resolve();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Marks every query under each prefix out of date: those on screen are read again at once, the
|
|
132
|
+
* rest are dropped. A read in flight is replaced, because it may have started before the change.
|
|
133
|
+
* No prefixes means every query.
|
|
134
|
+
*/
|
|
135
|
+
invalidate(prefixes) {
|
|
136
|
+
const reads = [];
|
|
137
|
+
for (const [hash, entry] of this.#entries) {
|
|
138
|
+
if (prefixes.length > 0 && !prefixes.some((prefix) => startsWith(entry.key, prefix))) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (entry.listeners.size > 0) {
|
|
142
|
+
reads.push(this.#fetch(entry, true));
|
|
143
|
+
} else {
|
|
144
|
+
this.#drop(hash, entry);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return Promise.all(reads).then(() => {
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Changes the cached value of `key` with `update`, unless a read wrote a newer value since
|
|
152
|
+
* `version` (from `version(key)`). Returns whether it applied.
|
|
153
|
+
*/
|
|
154
|
+
update(key, version, update) {
|
|
155
|
+
const entry = this.#entries.get(hashKey(key));
|
|
156
|
+
if (this.#disposed || !entry || entry.version !== version || entry.state.status !== "ready") {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
entry.version++;
|
|
160
|
+
this.#write(entry, {
|
|
161
|
+
status: "ready",
|
|
162
|
+
data: update(entry.state.data),
|
|
163
|
+
error: null
|
|
164
|
+
});
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Replaces the cached value of `key` with one a change returned, cancelling a read in flight that
|
|
169
|
+
* may have started before the change. Does nothing for a key nobody has read.
|
|
170
|
+
*/
|
|
171
|
+
set(key, data) {
|
|
172
|
+
const entry = this.#entries.get(hashKey(key));
|
|
173
|
+
if (this.#disposed || !entry) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
entry.controller?.abort();
|
|
177
|
+
entry.controller = null;
|
|
178
|
+
entry.pending = null;
|
|
179
|
+
entry.version++;
|
|
180
|
+
entry.settledAt = Date.now();
|
|
181
|
+
this.#write(entry, { status: "ready", data, error: null });
|
|
182
|
+
this.#schedule(entry);
|
|
183
|
+
}
|
|
184
|
+
version(key) {
|
|
185
|
+
return this.#entries.get(hashKey(key))?.version ?? -1;
|
|
186
|
+
}
|
|
187
|
+
/** The tab became visible again: read whatever came due while it was hidden. */
|
|
188
|
+
resume() {
|
|
189
|
+
for (const entry of this.#entries.values()) {
|
|
190
|
+
this.#schedule(entry);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** The window regained focus. */
|
|
194
|
+
focus() {
|
|
195
|
+
for (const entry of this.#entries.values()) {
|
|
196
|
+
if (entry.focus.size > 0 && entry.listeners.size > 0) {
|
|
197
|
+
void this.#fetch(entry, false);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Stops every read and timer for good. Answers that arrive later are dropped. */
|
|
202
|
+
dispose() {
|
|
203
|
+
this.#disposed = true;
|
|
204
|
+
for (const [hash, entry] of this.#entries) {
|
|
205
|
+
this.#drop(hash, entry);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
#drop(hash, entry) {
|
|
209
|
+
entry.controller?.abort();
|
|
210
|
+
entry.controller = null;
|
|
211
|
+
entry.pending = null;
|
|
212
|
+
clearTimeout(entry.timer);
|
|
213
|
+
clearTimeout(entry.unused);
|
|
214
|
+
if (this.#entries.get(hash) === entry) {
|
|
215
|
+
this.#entries.delete(hash);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
#write(entry, state) {
|
|
219
|
+
entry.state = state;
|
|
220
|
+
for (const listener of [...entry.listeners]) {
|
|
221
|
+
listener();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
#fetch(entry, replace) {
|
|
225
|
+
if (this.#disposed) {
|
|
226
|
+
return Promise.resolve();
|
|
227
|
+
}
|
|
228
|
+
if (entry.pending && !replace) {
|
|
229
|
+
return entry.pending;
|
|
230
|
+
}
|
|
231
|
+
entry.controller?.abort();
|
|
232
|
+
clearTimeout(entry.timer);
|
|
233
|
+
const controller = new AbortController();
|
|
234
|
+
entry.controller = controller;
|
|
235
|
+
const counts = () => !this.#disposed && entry.controller === controller;
|
|
236
|
+
const pending = (async () => {
|
|
237
|
+
try {
|
|
238
|
+
const data = await Promise.resolve().then(
|
|
239
|
+
() => entry.read({ signal: controller.signal, session: this.session })
|
|
240
|
+
);
|
|
241
|
+
if (counts()) {
|
|
242
|
+
entry.version++;
|
|
243
|
+
this.#write(entry, { status: "ready", data, error: null });
|
|
244
|
+
}
|
|
245
|
+
} catch (cause) {
|
|
246
|
+
if (!counts()) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const error = relayError(cause);
|
|
250
|
+
if (error.code === RelayErrorCode.StaleSession) {
|
|
251
|
+
this.#onStaleSession();
|
|
252
|
+
}
|
|
253
|
+
const gone = error.kind === "http" && error.status === 404 || error.code === RelayErrorCode.StaleSession;
|
|
254
|
+
entry.version++;
|
|
255
|
+
this.#write(
|
|
256
|
+
entry,
|
|
257
|
+
entry.state.status === "ready" && !gone ? { status: "ready", data: entry.state.data, error } : { status: "error", data: void 0, error }
|
|
258
|
+
);
|
|
259
|
+
} finally {
|
|
260
|
+
if (counts()) {
|
|
261
|
+
entry.controller = null;
|
|
262
|
+
entry.pending = null;
|
|
263
|
+
entry.settledAt = Date.now();
|
|
264
|
+
this.#schedule(entry);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
})();
|
|
268
|
+
entry.pending = pending;
|
|
269
|
+
return pending;
|
|
270
|
+
}
|
|
271
|
+
/** Arms the poll for the shortest interval anyone on screen asked for. */
|
|
272
|
+
#schedule(entry) {
|
|
273
|
+
clearTimeout(entry.timer);
|
|
274
|
+
if (this.#disposed || entry.pending || entry.listeners.size === 0 || entry.intervals.size === 0) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const interval = Math.min(...entry.intervals.values());
|
|
278
|
+
const wait = Math.max(0, entry.settledAt + interval - Date.now());
|
|
279
|
+
entry.timer = setTimeout(() => {
|
|
280
|
+
if (visible()) {
|
|
281
|
+
void this.#fetch(entry, false);
|
|
282
|
+
}
|
|
283
|
+
}, wait);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
25
286
|
|
|
26
287
|
// src/react/context.ts
|
|
27
288
|
import { createContext, useContext } from "react";
|
|
28
|
-
var
|
|
29
|
-
function
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
289
|
+
var StoreContext = createContext(null);
|
|
290
|
+
function useStore() {
|
|
291
|
+
const store = useContext(StoreContext);
|
|
292
|
+
if (!store) {
|
|
32
293
|
throw new Error("Domino hooks require <RelayProvider>");
|
|
33
294
|
}
|
|
34
|
-
return
|
|
295
|
+
return store;
|
|
296
|
+
}
|
|
297
|
+
function useRelay() {
|
|
298
|
+
return useStore().client;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/react/activity.ts
|
|
302
|
+
function useActivity(options = {}) {
|
|
303
|
+
const store = useStore();
|
|
304
|
+
const scope = useSyncExternalStore(
|
|
305
|
+
store.subscribe,
|
|
306
|
+
() => store.scope,
|
|
307
|
+
() => null
|
|
308
|
+
);
|
|
309
|
+
const session = useSyncExternalStore(
|
|
310
|
+
store.subscribe,
|
|
311
|
+
() => store.session,
|
|
312
|
+
() => LOADING
|
|
313
|
+
);
|
|
314
|
+
const callback = useRef(options.onEvent);
|
|
315
|
+
useLayoutEffect(() => {
|
|
316
|
+
callback.current = options.onEvent;
|
|
317
|
+
}, [options.onEvent]);
|
|
318
|
+
const observer = useRef(null);
|
|
319
|
+
const [state, setState] = useState(null);
|
|
320
|
+
const { cursor, intervalMs } = options;
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
if (!scope?.member) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
let active = true;
|
|
326
|
+
const watch = store.client.me.activity.observe(
|
|
327
|
+
(event) => callback.current?.(event),
|
|
328
|
+
{
|
|
329
|
+
cursor,
|
|
330
|
+
intervalMs,
|
|
331
|
+
onState: (observation) => {
|
|
332
|
+
if (!active) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (observation.kind === "ready") {
|
|
336
|
+
setState((prior) => {
|
|
337
|
+
const items = prior?.owner === scope.key && prior.value.status === "ready" ? prior.value.data.items : [];
|
|
338
|
+
const combined = new Map(
|
|
339
|
+
[...items, ...observation.progress.items].map((event) => [
|
|
340
|
+
event.id,
|
|
341
|
+
event
|
|
342
|
+
])
|
|
343
|
+
);
|
|
344
|
+
return {
|
|
345
|
+
owner: scope.key,
|
|
346
|
+
value: {
|
|
347
|
+
status: "ready",
|
|
348
|
+
data: {
|
|
349
|
+
...observation.progress,
|
|
350
|
+
items: [...combined.values()].slice(-100)
|
|
351
|
+
},
|
|
352
|
+
error: null
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
});
|
|
356
|
+
} else if (observation.kind === "error") {
|
|
357
|
+
setState((prior) => ({
|
|
358
|
+
owner: scope.key,
|
|
359
|
+
value: prior?.owner === scope.key && prior.value.status === "ready" ? { ...prior.value, error: relayError(observation.error) } : {
|
|
360
|
+
status: "error",
|
|
361
|
+
data: void 0,
|
|
362
|
+
error: relayError(observation.error)
|
|
363
|
+
}
|
|
364
|
+
}));
|
|
365
|
+
} else if (observation.kind === "loading") {
|
|
366
|
+
setState({ owner: scope.key, value: LOADING });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
observer.current = watch;
|
|
372
|
+
return () => {
|
|
373
|
+
active = false;
|
|
374
|
+
watch.dispose();
|
|
375
|
+
if (observer.current === watch) {
|
|
376
|
+
observer.current = null;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}, [store, scope?.key, cursor, intervalMs]);
|
|
380
|
+
const refresh = useCallback(
|
|
381
|
+
() => observer.current?.refresh() ?? Promise.resolve(),
|
|
382
|
+
[]
|
|
383
|
+
);
|
|
384
|
+
const value = session.status === "error" ? session : !scope ? LOADING : !scope.member ? { status: "signed-out", data: void 0, error: null } : state?.owner === scope.key ? state.value : LOADING;
|
|
385
|
+
return { ...value, refresh };
|
|
35
386
|
}
|
|
36
387
|
|
|
388
|
+
// src/react/provider.tsx
|
|
389
|
+
import { useEffect as useEffect3, useMemo as useMemo2 } from "react";
|
|
390
|
+
|
|
37
391
|
// src/react/experience.tsx
|
|
38
392
|
import {
|
|
39
393
|
createContext as createContext2,
|
|
394
|
+
useCallback as useCallback2,
|
|
40
395
|
useContext as useContext2,
|
|
41
|
-
useEffect as useEffect2,
|
|
42
396
|
useMemo,
|
|
43
|
-
|
|
44
|
-
useState as useState2
|
|
397
|
+
useSyncExternalStore as useSyncExternalStore2
|
|
45
398
|
} from "react";
|
|
46
399
|
|
|
47
400
|
// src/react/role-delivery.ts
|
|
48
401
|
function sanitizeDeliveries(deliveries) {
|
|
49
|
-
return deliveries.map(({ quest, status }) => ({
|
|
402
|
+
return deliveries.map(({ quest, status, revokedAt, reconciliation }) => ({
|
|
403
|
+
quest,
|
|
404
|
+
status: reconciliation ? reconciliation.status === "granted" ? "delivered" : reconciliation.status : revokedAt ? "revoked" : status
|
|
405
|
+
}));
|
|
50
406
|
}
|
|
51
407
|
function completionMark(quests) {
|
|
52
408
|
return quests.filter((q) => q.completed || q.completionCount > 0).map((q) => `${q.quest}:${Math.max(q.completionCount, 1)}`).sort().join(",");
|
|
@@ -54,19 +410,37 @@ function completionMark(quests) {
|
|
|
54
410
|
function hasPending(deliveries) {
|
|
55
411
|
return deliveries.some((d) => d.status === "pending");
|
|
56
412
|
}
|
|
57
|
-
function
|
|
58
|
-
const mine = deliveries.filter((d) => d.quest === quest);
|
|
413
|
+
function roleDeliveryStatus(deliveries, quest) {
|
|
414
|
+
const mine = sanitizeDeliveries(deliveries).filter((d) => d.quest === quest);
|
|
59
415
|
if (!mine.length) {
|
|
60
416
|
return null;
|
|
61
417
|
}
|
|
62
418
|
if (hasPending(mine)) {
|
|
63
|
-
return "
|
|
419
|
+
return "pending";
|
|
420
|
+
}
|
|
421
|
+
return mine.some((d) => d.status === "failed") ? "failed" : mine.every((d) => d.status === "delivered") ? "granted" : mine.find((d) => d.status !== "delivered")?.status ?? "unknown";
|
|
422
|
+
}
|
|
423
|
+
function roleDeliveryLabel(status) {
|
|
424
|
+
switch (status) {
|
|
425
|
+
case "pending":
|
|
426
|
+
return "Role delivery pending";
|
|
427
|
+
case "failed":
|
|
428
|
+
return "Role delivery failed";
|
|
429
|
+
case "granted":
|
|
430
|
+
return "Role granted";
|
|
431
|
+
case "removed":
|
|
432
|
+
case "revoked":
|
|
433
|
+
return "Role reward revoked";
|
|
434
|
+
case "preserved":
|
|
435
|
+
return "Role managed by the organizer";
|
|
436
|
+
default:
|
|
437
|
+
return status === null ? null : "Role status unavailable";
|
|
64
438
|
}
|
|
65
|
-
return mine.some((d) => d.status === "failed") ? "Role delivery failed" : "Role granted";
|
|
66
439
|
}
|
|
67
440
|
var PENDING_POLL_MS = 5e3;
|
|
68
441
|
var RETRY_DELAYS_MS = [1e3, 4e3];
|
|
69
442
|
function watchRoleDeliveries(options) {
|
|
443
|
+
const visible2 = options.visible ?? (() => typeof document === "undefined" || document.visibilityState !== "hidden");
|
|
70
444
|
const schedule = options.schedule ?? ((run2, ms) => {
|
|
71
445
|
const timer = setTimeout(run2, ms);
|
|
72
446
|
return () => clearTimeout(timer);
|
|
@@ -82,6 +456,10 @@ function watchRoleDeliveries(options) {
|
|
|
82
456
|
if (!active || reading) {
|
|
83
457
|
return;
|
|
84
458
|
}
|
|
459
|
+
if (!visible2()) {
|
|
460
|
+
resting = true;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
85
463
|
reading = true;
|
|
86
464
|
resting = false;
|
|
87
465
|
try {
|
|
@@ -124,256 +502,656 @@ function watchRoleDeliveries(options) {
|
|
|
124
502
|
};
|
|
125
503
|
}
|
|
126
504
|
|
|
505
|
+
// src/react/use-role-deliveries.ts
|
|
506
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
507
|
+
var NONE = Object.freeze([]);
|
|
508
|
+
function useRoleDeliveries(relay, member, completions) {
|
|
509
|
+
const [state, setState] = useState2(null);
|
|
510
|
+
useEffect2(() => {
|
|
511
|
+
if (!member || !completions) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const watch = watchRoleDeliveries({
|
|
515
|
+
read: async () => (await relay.me.rewardDeliveries()).deliveries,
|
|
516
|
+
onChange: (deliveries) => setState({ member, deliveries })
|
|
517
|
+
});
|
|
518
|
+
const wake = () => {
|
|
519
|
+
if (document.visibilityState === "visible") {
|
|
520
|
+
watch.wake();
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
window.addEventListener("focus", wake);
|
|
524
|
+
document.addEventListener("visibilitychange", wake);
|
|
525
|
+
return () => {
|
|
526
|
+
watch.dispose();
|
|
527
|
+
window.removeEventListener("focus", wake);
|
|
528
|
+
document.removeEventListener("visibilitychange", wake);
|
|
529
|
+
};
|
|
530
|
+
}, [relay, member, completions]);
|
|
531
|
+
return member && completions && state?.member === member ? state.deliveries : NONE;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/react/experience.tsx
|
|
535
|
+
import { jsx } from "react/jsx-runtime";
|
|
536
|
+
var Deliveries = createContext2(Object.freeze([]));
|
|
537
|
+
function useExperienceSnapshot() {
|
|
538
|
+
const store = useStore();
|
|
539
|
+
const get = () => store.experience;
|
|
540
|
+
return useSyncExternalStore2(store.subscribe, get, get);
|
|
541
|
+
}
|
|
542
|
+
function DeliveriesProvider({ children }) {
|
|
543
|
+
const store = useStore();
|
|
544
|
+
const experience = useExperienceSnapshot();
|
|
545
|
+
const member = useSyncExternalStore2(
|
|
546
|
+
store.subscribe,
|
|
547
|
+
() => store.scope?.member ?? null,
|
|
548
|
+
() => null
|
|
549
|
+
);
|
|
550
|
+
const ready = experience.status === "ready";
|
|
551
|
+
const deliveries = useRoleDeliveries(
|
|
552
|
+
store.client,
|
|
553
|
+
ready ? member : null,
|
|
554
|
+
ready ? completionMark([
|
|
555
|
+
...experience.data.quests,
|
|
556
|
+
...experience.data.collections.flatMap((c) => c.quests)
|
|
557
|
+
]) : ""
|
|
558
|
+
);
|
|
559
|
+
return /* @__PURE__ */ jsx(Deliveries, { value: deliveries, children });
|
|
560
|
+
}
|
|
561
|
+
function useExperience() {
|
|
562
|
+
const store = useStore();
|
|
563
|
+
const experience = useExperienceSnapshot();
|
|
564
|
+
const deliveries = useContext2(Deliveries);
|
|
565
|
+
const refresh = useCallback2(() => store.refreshExperience(), [store]);
|
|
566
|
+
return useMemo(
|
|
567
|
+
() => ({ ...experience, refresh, deliveries }),
|
|
568
|
+
[experience, refresh, deliveries]
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/react/keys.ts
|
|
573
|
+
var relayKeys = {
|
|
574
|
+
/** `useExperience`: invalidating it reads the quest experience again. */
|
|
575
|
+
experience: ["experience"],
|
|
576
|
+
/** `useMe`. */
|
|
577
|
+
me: ["me"],
|
|
578
|
+
/** `useAccounts`. */
|
|
579
|
+
accounts: ["accounts"],
|
|
580
|
+
/** `useWallets`. */
|
|
581
|
+
wallets: ["wallets"],
|
|
582
|
+
/** `useMyProfile`, `useProfile`, `useProfileCompletions`, `useProfileReferral`. */
|
|
583
|
+
profiles: ["profiles"],
|
|
584
|
+
/** `useLeaderboard`, `useLeaderboardPlacement`. */
|
|
585
|
+
leaderboards: ["leaderboards"],
|
|
586
|
+
/** `useReferralSummary`. */
|
|
587
|
+
referrals: ["referrals"],
|
|
588
|
+
/** `usePointLedgers`. */
|
|
589
|
+
pointLedgers: ["point-ledgers"],
|
|
590
|
+
/** `useTieredRewards`. */
|
|
591
|
+
tieredRewards: ["tiered-rewards"],
|
|
592
|
+
/** `useSignInMethods`. */
|
|
593
|
+
signInMethods: ["sign-in-methods"],
|
|
594
|
+
/** `useEntitlements`, `useDeliveries`. */
|
|
595
|
+
rewards: ["rewards"],
|
|
596
|
+
/** `useBalances`, `usePointHistory`. */
|
|
597
|
+
balances: ["balances"],
|
|
598
|
+
/** `useQuestOutcomes`. */
|
|
599
|
+
outcomes: ["outcomes"],
|
|
600
|
+
/** `useOffers`, `useRedeemOffer`. */
|
|
601
|
+
offers: ["offers"]
|
|
602
|
+
};
|
|
603
|
+
var completionChanges = [
|
|
604
|
+
relayKeys.rewards,
|
|
605
|
+
relayKeys.balances,
|
|
606
|
+
relayKeys.leaderboards,
|
|
607
|
+
relayKeys.profiles,
|
|
608
|
+
relayKeys.referrals,
|
|
609
|
+
relayKeys.offers
|
|
610
|
+
];
|
|
611
|
+
|
|
127
612
|
// src/react/status.ts
|
|
128
613
|
function isFinished(view) {
|
|
129
|
-
return view.availability.kind === "completed" || view.completion.kind !== "keyed" && view.completed;
|
|
614
|
+
return view.availability.kind === "completed" || view.completion.kind !== "keyed" && view.completion.kind !== "progress" && view.completed;
|
|
130
615
|
}
|
|
131
616
|
function isAutomatic(view) {
|
|
132
617
|
return view.trigger.kind === "automatic";
|
|
133
618
|
}
|
|
134
619
|
function runsSilently(view) {
|
|
135
|
-
return view.trigger.kind === "automatic" && (view.completion.kind === "keyed" || Boolean(view.trigger.
|
|
620
|
+
return view.trigger.kind === "automatic" && (view.completion.kind === "keyed" || Boolean(view.trigger.tracker));
|
|
136
621
|
}
|
|
137
|
-
function
|
|
138
|
-
if (view.completion.kind !== "keyed" || view.completionCount < 1) {
|
|
622
|
+
function completionCountText(view) {
|
|
623
|
+
if (view.completion.kind !== "keyed" && view.completion.kind !== "progress" || view.completionCount < 1) {
|
|
139
624
|
return null;
|
|
140
625
|
}
|
|
141
626
|
return view.completionCount === 1 ? "Completed once" : `Completed ${view.completionCount.toLocaleString()} times`;
|
|
142
627
|
}
|
|
143
628
|
function canRepeat(view) {
|
|
144
|
-
return view.completion.kind === "keyed" && view.attempt?.status === "accepted" && canSubmit(view);
|
|
629
|
+
return (view.completion.kind === "keyed" || view.completion.kind === "progress") && view.attempt?.status === "accepted" && canSubmit(view);
|
|
145
630
|
}
|
|
146
631
|
function visibleQuests(quests) {
|
|
147
632
|
return quests.filter((q) => q.visibility !== "hidden");
|
|
148
633
|
}
|
|
149
634
|
function questStatus(view) {
|
|
635
|
+
return knownStatus(view);
|
|
636
|
+
}
|
|
637
|
+
function knownStatus(view) {
|
|
150
638
|
if (isFinished(view)) {
|
|
151
|
-
return { tone: "done",
|
|
639
|
+
return { tone: "done", code: "completed" };
|
|
152
640
|
}
|
|
153
641
|
if (view.lifecycle === "paused") {
|
|
154
|
-
return {
|
|
155
|
-
tone: "off",
|
|
156
|
-
label: "Paused",
|
|
157
|
-
detail: "This activity is paused for now."
|
|
158
|
-
};
|
|
642
|
+
return { tone: "off", code: "paused" };
|
|
159
643
|
}
|
|
160
644
|
if (view.lifecycle === "archived") {
|
|
645
|
+
return { tone: "off", code: "closed" };
|
|
646
|
+
}
|
|
647
|
+
if (view.attempt?.status === "deferred") {
|
|
161
648
|
return {
|
|
162
|
-
tone: "
|
|
163
|
-
|
|
164
|
-
|
|
649
|
+
tone: "waiting",
|
|
650
|
+
code: view.attempt.code === "evaluator-progress" ? "evaluator-progress" : "evaluator-deferred",
|
|
651
|
+
reason: view.attempt.reason ?? ""
|
|
165
652
|
};
|
|
166
653
|
}
|
|
167
654
|
const availability = view.availability;
|
|
168
|
-
if (
|
|
169
|
-
const streak = view.streak;
|
|
170
|
-
if (streak.milestone.status === "budget-exhausted") {
|
|
171
|
-
return {
|
|
172
|
-
tone: "attention",
|
|
173
|
-
label: "Milestone reached",
|
|
174
|
-
detail: "Your qualifying days are saved. This milestone's reward budget was exhausted."
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
const deadline = new Intl.DateTimeFormat("en", {
|
|
178
|
-
timeZone: streak.timeZone,
|
|
179
|
-
month: "short",
|
|
180
|
-
day: "numeric",
|
|
181
|
-
hour: "numeric",
|
|
182
|
-
minute: "2-digit",
|
|
183
|
-
timeZoneName: "short"
|
|
184
|
-
}).format(streak.nextBoundary);
|
|
655
|
+
if (availability.kind === "blocked") {
|
|
185
656
|
return {
|
|
186
|
-
tone:
|
|
187
|
-
|
|
188
|
-
detail: streak.todayQualified ? `Today counts. Your next community 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.`
|
|
657
|
+
tone: availability.reason === "paused" || availability.reason === "outside-window" ? "off" : availability.reason === "budget-exhausted" ? "attention" : "locked",
|
|
658
|
+
code: availability.reason
|
|
189
659
|
};
|
|
190
660
|
}
|
|
191
|
-
|
|
661
|
+
const ownSubmission = view.trigger.kind === "manual" && view.trigger.actor !== "staff";
|
|
662
|
+
const submissionFirst = ownSubmission && (availability.kind === "checking" || availability.kind === "needs-review" || availability.kind === "retryable");
|
|
663
|
+
if (view.counter && availability.kind !== "locked" && !submissionFirst) {
|
|
664
|
+
const currentAchievement = view.counter.latest?.window === view.counter.window.key ? view.counter.latest : null;
|
|
665
|
+
if (currentAchievement?.status === "revoked") {
|
|
666
|
+
return { tone: "attention", code: "counter-revoked" };
|
|
667
|
+
}
|
|
192
668
|
return {
|
|
193
|
-
tone: "waiting",
|
|
194
|
-
|
|
195
|
-
detail: view.completion.kind === "keyed" ? [
|
|
196
|
-
completionCountLabel(view),
|
|
197
|
-
"This counts by itself each time you qualify."
|
|
198
|
-
].filter(Boolean).join(". ") : "This updates by itself when you qualify."
|
|
669
|
+
tone: currentAchievement?.status === "budget-exhausted" ? "attention" : ownSubmission ? "open" : "waiting",
|
|
670
|
+
code: currentAchievement?.status === "budget-exhausted" ? "counter-budget-exhausted" : "counter"
|
|
199
671
|
};
|
|
200
672
|
}
|
|
201
|
-
if (
|
|
673
|
+
if (view.streak && availability.kind !== "locked" && !submissionFirst) {
|
|
674
|
+
const rejected = ownSubmission && view.attempt?.status === "rejected" ? { reason: view.attempt.reason ?? "" } : {};
|
|
675
|
+
if (view.streak.milestone.status === "budget-exhausted") {
|
|
676
|
+
return { tone: "attention", code: "streak-budget-exhausted", ...rejected };
|
|
677
|
+
}
|
|
202
678
|
return {
|
|
203
|
-
tone: "waiting",
|
|
204
|
-
|
|
205
|
-
|
|
679
|
+
tone: "reason" in rejected ? "attention" : view.streak.todayQualified ? "waiting" : "open",
|
|
680
|
+
code: "streak",
|
|
681
|
+
...rejected
|
|
206
682
|
};
|
|
207
683
|
}
|
|
684
|
+
if (runsSilently(view) && availability.kind !== "locked") {
|
|
685
|
+
return { tone: "waiting", code: "automatic" };
|
|
686
|
+
}
|
|
687
|
+
if (isAutomatic(view) && availability.kind !== "locked") {
|
|
688
|
+
return { tone: "waiting", code: "in-progress" };
|
|
689
|
+
}
|
|
208
690
|
switch (availability.kind) {
|
|
209
691
|
case "completed":
|
|
210
|
-
return { tone: "done",
|
|
692
|
+
return { tone: "done", code: "completed" };
|
|
211
693
|
case "locked":
|
|
212
|
-
return {
|
|
213
|
-
tone: "locked",
|
|
214
|
-
label: "Locked",
|
|
215
|
-
detail: `Complete ${list(availability.missing.map((m) => m.title))} first.`
|
|
216
|
-
};
|
|
694
|
+
return { tone: "locked", code: "locked" };
|
|
217
695
|
case "checking":
|
|
218
|
-
return {
|
|
219
|
-
tone: "waiting",
|
|
220
|
-
label: "Checking",
|
|
221
|
-
detail: "Your submission is being verified."
|
|
222
|
-
};
|
|
696
|
+
return { tone: "waiting", code: "checking" };
|
|
223
697
|
case "needs-review":
|
|
224
|
-
return {
|
|
225
|
-
tone: "waiting",
|
|
226
|
-
label: "In review",
|
|
227
|
-
detail: "A person is reviewing your submission."
|
|
228
|
-
};
|
|
698
|
+
return { tone: "waiting", code: "in-review" };
|
|
229
699
|
case "retryable":
|
|
230
|
-
return {
|
|
231
|
-
tone: "attention",
|
|
232
|
-
label: "Didn't go through",
|
|
233
|
-
detail: "Verification couldn't run. Try again. Nothing was lost."
|
|
234
|
-
};
|
|
700
|
+
return { tone: "attention", code: "retryable" };
|
|
235
701
|
case "staff-required":
|
|
236
|
-
return {
|
|
237
|
-
tone: "waiting",
|
|
238
|
-
label: "Waiting for staff",
|
|
239
|
-
detail: "A staff member confirms this activity for you."
|
|
240
|
-
};
|
|
702
|
+
return { tone: "waiting", code: "staff-required" };
|
|
241
703
|
case "automatic":
|
|
242
|
-
return {
|
|
243
|
-
tone: "waiting",
|
|
244
|
-
label: "In progress",
|
|
245
|
-
detail: "This updates by itself when you qualify."
|
|
246
|
-
};
|
|
704
|
+
return { tone: "waiting", code: "in-progress" };
|
|
247
705
|
case "available":
|
|
248
706
|
return view.attempt?.status === "rejected" ? {
|
|
249
707
|
tone: "attention",
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
} : { tone: "open",
|
|
708
|
+
code: "rejected",
|
|
709
|
+
reason: view.attempt.reason ?? ""
|
|
710
|
+
} : { tone: "open", code: "ready" };
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
function questStatusText(view) {
|
|
714
|
+
const status = knownStatus(view);
|
|
715
|
+
const words = (label, detail) => detail === void 0 ? { ...status, label } : { ...status, label, detail };
|
|
716
|
+
const rejection = status.reason === void 0 ? null : sentence(status.reason || "That submission wasn't accepted.");
|
|
717
|
+
switch (status.code) {
|
|
718
|
+
case "completed":
|
|
719
|
+
return words("Completed");
|
|
720
|
+
case "paused":
|
|
721
|
+
return words("Paused", "This activity is paused for now.");
|
|
722
|
+
case "closed":
|
|
723
|
+
return words("Closed", "This activity is no longer running.");
|
|
724
|
+
case "counter":
|
|
725
|
+
return words(
|
|
726
|
+
`${view.counter?.current ?? 0} / ${view.counter?.goal ?? 0} actions`,
|
|
727
|
+
"Distinct qualifying actions count toward this goal."
|
|
728
|
+
);
|
|
729
|
+
case "counter-revoked":
|
|
730
|
+
return words(
|
|
731
|
+
"Achievement revoked",
|
|
732
|
+
"The reward was corrected. Its qualifying actions remain recorded."
|
|
733
|
+
);
|
|
734
|
+
case "counter-budget-exhausted":
|
|
735
|
+
return words(
|
|
736
|
+
"Goal reached; budget exhausted",
|
|
737
|
+
"Your qualifying actions are saved. This achievement did not grant a reward."
|
|
738
|
+
);
|
|
739
|
+
case "streak-budget-exhausted":
|
|
740
|
+
return words(
|
|
741
|
+
"Milestone reached",
|
|
742
|
+
rejection ?? "Your qualifying days are saved. This milestone's reward budget was exhausted."
|
|
743
|
+
);
|
|
744
|
+
case "streak": {
|
|
745
|
+
const streak = view.streak;
|
|
746
|
+
const deadline = new Intl.DateTimeFormat("en", {
|
|
747
|
+
timeZone: streak.timeZone,
|
|
748
|
+
month: "short",
|
|
749
|
+
day: "numeric",
|
|
750
|
+
hour: "numeric",
|
|
751
|
+
minute: "2-digit",
|
|
752
|
+
timeZoneName: "short"
|
|
753
|
+
}).format(streak.nextBoundary);
|
|
754
|
+
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.`;
|
|
755
|
+
return words(
|
|
756
|
+
`${streak.current} / ${streak.goal} days`,
|
|
757
|
+
rejection ? `${rejection} ${progress}` : progress
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
case "automatic":
|
|
761
|
+
return words(
|
|
762
|
+
"Automatic",
|
|
763
|
+
view.completion.kind === "keyed" ? [
|
|
764
|
+
completionCountText(view),
|
|
765
|
+
"This counts by itself each time you qualify."
|
|
766
|
+
].filter(Boolean).join(". ") : "This updates by itself when you qualify."
|
|
767
|
+
);
|
|
768
|
+
case "in-progress":
|
|
769
|
+
return words("In progress", "This updates by itself when you qualify.");
|
|
770
|
+
case "locked":
|
|
771
|
+
return words(
|
|
772
|
+
"Locked",
|
|
773
|
+
view.availability.kind === "locked" ? sentence(
|
|
774
|
+
capitalize(list(view.availability.missing.map(prerequisiteText)))
|
|
775
|
+
) : void 0
|
|
776
|
+
);
|
|
777
|
+
case "budget-exhausted":
|
|
778
|
+
return words(
|
|
779
|
+
"Budget reached",
|
|
780
|
+
view.availability.kind === "blocked" && view.availability.resetsAt !== void 0 ? `This quest's reward budget resets at ${new Date(view.availability.resetsAt).toISOString()}.` : "This quest's reward budget has been used."
|
|
781
|
+
);
|
|
782
|
+
case "connection-required":
|
|
783
|
+
return words("Connect an account", "Connect the required account first.");
|
|
784
|
+
case "wallet-required":
|
|
785
|
+
return words("Connect a wallet", "Connect the required wallet first.");
|
|
786
|
+
case "outside-window":
|
|
787
|
+
return words(
|
|
788
|
+
"Not open",
|
|
789
|
+
view.availability.kind === "blocked" && view.availability.resetsAt !== void 0 ? `This quest opens at ${new Date(view.availability.resetsAt).toISOString()}.` : "This quest's availability window has ended."
|
|
790
|
+
);
|
|
791
|
+
case "enrollment-required":
|
|
792
|
+
return words(
|
|
793
|
+
"Enroll",
|
|
794
|
+
view.campaign ? `Enroll in ${quote(view.campaign.title)} first.` : "Enroll in this campaign first."
|
|
795
|
+
);
|
|
796
|
+
case "evaluator-deferred":
|
|
797
|
+
return words("Waiting to check again", status.reason);
|
|
798
|
+
case "evaluator-progress":
|
|
799
|
+
return words("In progress", status.reason);
|
|
800
|
+
case "checking":
|
|
801
|
+
return words("Checking", "Your submission is being verified.");
|
|
802
|
+
case "in-review":
|
|
803
|
+
return words("In review", "A person is reviewing your submission.");
|
|
804
|
+
case "retryable":
|
|
805
|
+
return words(
|
|
806
|
+
"Didn't go through",
|
|
807
|
+
"Verification couldn't run. Try again. Nothing was lost."
|
|
808
|
+
);
|
|
809
|
+
case "staff-required":
|
|
810
|
+
return words(
|
|
811
|
+
"Waiting for staff",
|
|
812
|
+
"A staff member confirms this activity for you."
|
|
813
|
+
);
|
|
814
|
+
case "rejected":
|
|
815
|
+
return words(
|
|
816
|
+
"Try again",
|
|
817
|
+
status.reason || "That submission wasn't accepted."
|
|
818
|
+
);
|
|
819
|
+
case "ready":
|
|
820
|
+
return words("Ready");
|
|
253
821
|
}
|
|
254
822
|
}
|
|
255
823
|
function canSubmit(view) {
|
|
256
|
-
return !isFinished(view) && !isAutomatic(view) && view.lifecycle === "active" && view.availability.kind === "available";
|
|
824
|
+
return !isFinished(view) && !(view.counter?.repeat === "once" && view.counter.settled > 0) && !isAutomatic(view) && view.lifecycle === "active" && view.availability.kind === "available";
|
|
257
825
|
}
|
|
258
826
|
function progressOf(quests) {
|
|
259
827
|
const counted = quests.filter(
|
|
260
|
-
(q) => q.lifecycle !== "archived" && q.completion.kind !== "keyed"
|
|
828
|
+
(q) => q.lifecycle !== "archived" && q.completion.kind !== "keyed" && q.completion.kind !== "progress"
|
|
261
829
|
);
|
|
262
830
|
return {
|
|
263
831
|
total: counted.length,
|
|
264
832
|
done: counted.filter(isFinished).length
|
|
265
833
|
};
|
|
266
834
|
}
|
|
835
|
+
function sentence(text) {
|
|
836
|
+
const trimmed = text.trim();
|
|
837
|
+
return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
|
|
838
|
+
}
|
|
267
839
|
function list(items) {
|
|
268
|
-
return new Intl.ListFormat("en", { type: "conjunction" }).format(
|
|
269
|
-
|
|
270
|
-
|
|
840
|
+
return new Intl.ListFormat("en", { type: "conjunction" }).format(items);
|
|
841
|
+
}
|
|
842
|
+
function prerequisiteText(prerequisite) {
|
|
843
|
+
switch (prerequisite.kind) {
|
|
844
|
+
case "completion":
|
|
845
|
+
return prerequisite.scope === "ever" ? `complete ${quote(prerequisite.title)} first` : `complete ${quote(prerequisite.title)} in this ${prerequisite.period}`;
|
|
846
|
+
case "balance":
|
|
847
|
+
return `hold ${prerequisite.minimum.toLocaleString()} ${prerequisite.title}`;
|
|
848
|
+
case "group":
|
|
849
|
+
return `join ${quote(prerequisite.title)}`;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function capitalize(value) {
|
|
853
|
+
return value ? value[0].toUpperCase() + value.slice(1) : value;
|
|
854
|
+
}
|
|
855
|
+
function quote(value) {
|
|
856
|
+
return `\u201C${value}\u201D`;
|
|
271
857
|
}
|
|
272
858
|
|
|
273
|
-
// src/react/
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
859
|
+
// src/react/store.ts
|
|
860
|
+
var SIGNED_OUT = Object.freeze({
|
|
861
|
+
status: "signed-out",
|
|
862
|
+
data: void 0,
|
|
863
|
+
error: null
|
|
864
|
+
});
|
|
865
|
+
function scopeKey(session) {
|
|
866
|
+
if (!session) {
|
|
867
|
+
return "visitor";
|
|
868
|
+
}
|
|
869
|
+
const principal = session.principal;
|
|
870
|
+
return JSON.stringify([
|
|
871
|
+
session.organization,
|
|
872
|
+
session.project,
|
|
873
|
+
session.environment,
|
|
874
|
+
principal.kind,
|
|
875
|
+
sessionMember(session) ?? principal.subject ?? null,
|
|
876
|
+
session.id
|
|
877
|
+
]);
|
|
878
|
+
}
|
|
879
|
+
function isVisible() {
|
|
880
|
+
return typeof document === "undefined" || document.visibilityState !== "hidden";
|
|
881
|
+
}
|
|
882
|
+
var RelayStore = class {
|
|
883
|
+
client;
|
|
884
|
+
#session = LOADING;
|
|
885
|
+
#scope = null;
|
|
886
|
+
#experience = { owner: null, state: LOADING };
|
|
887
|
+
#experienceView = LOADING;
|
|
888
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
889
|
+
#observer = null;
|
|
890
|
+
#starting = false;
|
|
891
|
+
#mounted = false;
|
|
892
|
+
/** Bumped by every session the store learns of, so an older read cannot overwrite a newer one. */
|
|
893
|
+
#sessionVersion = 0;
|
|
894
|
+
#sessionRead = null;
|
|
895
|
+
/** The completions the experience last showed, and for whom. */
|
|
896
|
+
#completions = null;
|
|
897
|
+
constructor(client) {
|
|
898
|
+
this.client = client;
|
|
899
|
+
}
|
|
900
|
+
/** `refreshSession` with one identity per store, for hooks to hand out as `refresh`. */
|
|
901
|
+
rereadSession = () => this.refreshSession();
|
|
902
|
+
subscribe = (listener) => {
|
|
903
|
+
this.#listeners.add(listener);
|
|
904
|
+
return () => {
|
|
905
|
+
this.#listeners.delete(listener);
|
|
906
|
+
};
|
|
907
|
+
};
|
|
908
|
+
get session() {
|
|
909
|
+
return this.#session;
|
|
910
|
+
}
|
|
911
|
+
get scope() {
|
|
912
|
+
return this.#scope;
|
|
913
|
+
}
|
|
914
|
+
get experience() {
|
|
915
|
+
return this.#experienceView;
|
|
916
|
+
}
|
|
917
|
+
/** Starts observing; the returned function stops every read, timer and listener. */
|
|
918
|
+
mount() {
|
|
919
|
+
this.#mounted = true;
|
|
920
|
+
if (isVisible()) {
|
|
921
|
+
this.#startObserver();
|
|
280
922
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
923
|
+
void this.refreshSession();
|
|
924
|
+
const onVisibility = () => {
|
|
925
|
+
if (isVisible()) {
|
|
926
|
+
this.#startObserver();
|
|
927
|
+
this.#scope?.cache.resume();
|
|
928
|
+
} else {
|
|
929
|
+
this.#observer?.dispose();
|
|
930
|
+
this.#observer = null;
|
|
288
931
|
}
|
|
289
932
|
};
|
|
290
|
-
|
|
291
|
-
|
|
933
|
+
const onFocus = () => this.#scope?.cache.focus();
|
|
934
|
+
if (typeof window !== "undefined") {
|
|
935
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
936
|
+
window.addEventListener("focus", onFocus);
|
|
937
|
+
}
|
|
292
938
|
return () => {
|
|
293
|
-
|
|
294
|
-
window
|
|
295
|
-
|
|
939
|
+
this.#mounted = false;
|
|
940
|
+
if (typeof window !== "undefined") {
|
|
941
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
942
|
+
window.removeEventListener("focus", onFocus);
|
|
943
|
+
}
|
|
944
|
+
this.#observer?.dispose();
|
|
945
|
+
this.#observer = null;
|
|
946
|
+
this.#sessionRead?.abort();
|
|
947
|
+
this.#sessionRead = null;
|
|
948
|
+
this.#sessionVersion++;
|
|
949
|
+
this.#scope?.cache.dispose();
|
|
950
|
+
this.#scope = null;
|
|
951
|
+
this.#session = LOADING;
|
|
952
|
+
this.#experience = { owner: null, state: LOADING };
|
|
953
|
+
this.#emit();
|
|
296
954
|
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
955
|
+
}
|
|
956
|
+
/** Reads the session again now, without waiting for the experience observer. */
|
|
957
|
+
async refreshSession() {
|
|
958
|
+
if (!this.#mounted) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const version = ++this.#sessionVersion;
|
|
962
|
+
this.#sessionRead?.abort();
|
|
963
|
+
const controller = new AbortController();
|
|
964
|
+
this.#sessionRead = controller;
|
|
965
|
+
try {
|
|
966
|
+
const session = await this.client.auth.session({
|
|
967
|
+
signal: controller.signal
|
|
968
|
+
});
|
|
969
|
+
if (version === this.#sessionVersion && this.#mounted) {
|
|
970
|
+
const before = this.#scope?.key;
|
|
971
|
+
this.#setSession({ status: "ready", data: session, error: null });
|
|
972
|
+
this.#emit();
|
|
973
|
+
if (before !== void 0 && this.#scope?.key !== before) {
|
|
974
|
+
void this.#observer?.refresh();
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
} catch (cause) {
|
|
978
|
+
if (version === this.#sessionVersion && this.#mounted) {
|
|
979
|
+
this.#failSession(relayError(cause));
|
|
980
|
+
this.#emit();
|
|
319
981
|
}
|
|
320
|
-
|
|
321
|
-
|
|
982
|
+
} finally {
|
|
983
|
+
if (this.#sessionRead === controller) {
|
|
984
|
+
this.#sessionRead = null;
|
|
322
985
|
}
|
|
323
|
-
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Signs the participant out through the client, and drops their scope as soon as Relay confirms:
|
|
990
|
+
* the previous member's values never wait on the next session read, which may fail. Only the
|
|
991
|
+
* scope that signed out is dropped: someone who signed in while the request was in flight stays.
|
|
992
|
+
*/
|
|
993
|
+
async signOut() {
|
|
994
|
+
const scope = this.#scope;
|
|
995
|
+
await this.client.auth.signOut();
|
|
996
|
+
if (!this.#mounted || this.#scope !== scope) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
this.#sessionVersion++;
|
|
1000
|
+
this.#sessionRead?.abort();
|
|
1001
|
+
this.#setSession({ status: "ready", data: null, error: null });
|
|
1002
|
+
this.#emit();
|
|
1003
|
+
}
|
|
1004
|
+
refreshExperience() {
|
|
1005
|
+
return this.#observer?.refresh() ?? Promise.resolve();
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Marks queries out of date (see `QueryCache.invalidate`). The `["experience"]` key, or no keys at
|
|
1009
|
+
* all, also reads the quest experience again.
|
|
1010
|
+
*/
|
|
1011
|
+
invalidate(keys) {
|
|
1012
|
+
const reads = [];
|
|
1013
|
+
if (keys.length === 0 || keys.some((key) => key[0] === "experience")) {
|
|
1014
|
+
reads.push(this.refreshExperience());
|
|
1015
|
+
}
|
|
1016
|
+
reads.push(this.#scope?.cache.invalidate(keys) ?? Promise.resolve());
|
|
1017
|
+
return Promise.all(reads).then(() => {
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
#startObserver() {
|
|
1021
|
+
if (this.#observer || !this.#mounted) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
this.#starting = true;
|
|
1025
|
+
try {
|
|
1026
|
+
this.#observer = this.client.quests.observe((observation) => {
|
|
324
1027
|
switch (observation.kind) {
|
|
325
1028
|
case "loading":
|
|
326
|
-
|
|
1029
|
+
if (this.#starting) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
this.#sessionVersion++;
|
|
1033
|
+
this.#sessionRead?.abort();
|
|
1034
|
+
return;
|
|
327
1035
|
case "session":
|
|
328
|
-
|
|
1036
|
+
this.#sessionVersion++;
|
|
1037
|
+
this.#setSession({
|
|
1038
|
+
status: "ready",
|
|
1039
|
+
data: observation.session,
|
|
1040
|
+
error: null
|
|
1041
|
+
});
|
|
1042
|
+
break;
|
|
329
1043
|
case "ready":
|
|
330
|
-
|
|
1044
|
+
this.#sessionVersion++;
|
|
1045
|
+
this.#setSession({
|
|
331
1046
|
status: "ready",
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
1047
|
+
data: observation.session,
|
|
1048
|
+
error: null
|
|
1049
|
+
});
|
|
1050
|
+
this.#completed(observation.progress);
|
|
1051
|
+
this.#experience = {
|
|
1052
|
+
owner: observation.progress.member,
|
|
1053
|
+
state: {
|
|
1054
|
+
status: "ready",
|
|
1055
|
+
// Hidden quests never reach the UI, on any poll.
|
|
1056
|
+
data: {
|
|
1057
|
+
entitlements: observation.progress.entitlements,
|
|
1058
|
+
quests: visibleQuests(observation.progress.quests),
|
|
1059
|
+
collections: observation.progress.collections.map(
|
|
1060
|
+
(collection) => ({
|
|
1061
|
+
...collection,
|
|
1062
|
+
quests: visibleQuests(collection.quests)
|
|
1063
|
+
})
|
|
1064
|
+
)
|
|
1065
|
+
},
|
|
1066
|
+
error: null
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
break;
|
|
1070
|
+
case "error": {
|
|
1071
|
+
const error = relayError(observation.error);
|
|
1072
|
+
const previous = this.#experience;
|
|
1073
|
+
this.#experience = previous.state.status === "ready" ? {
|
|
1074
|
+
owner: previous.owner,
|
|
1075
|
+
state: { ...previous.state, error }
|
|
1076
|
+
} : {
|
|
1077
|
+
owner: this.#scope?.member ?? null,
|
|
1078
|
+
state: { status: "error", data: void 0, error }
|
|
341
1079
|
};
|
|
342
|
-
|
|
343
|
-
|
|
1080
|
+
if (this.#session.status === "loading") {
|
|
1081
|
+
void this.refreshSession();
|
|
1082
|
+
}
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
344
1085
|
}
|
|
1086
|
+
this.#emit();
|
|
345
1087
|
});
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}, [relay]);
|
|
350
|
-
const mine = experience.status === "ready" && member !== null && member === owner;
|
|
351
|
-
const deliveries = useRoleDeliveries(
|
|
352
|
-
relay,
|
|
353
|
-
mine ? member : null,
|
|
354
|
-
mine ? completionMark([
|
|
355
|
-
...experience.quests,
|
|
356
|
-
...experience.collections.flatMap((c) => c.quests)
|
|
357
|
-
]) : ""
|
|
358
|
-
);
|
|
359
|
-
const value = useMemo(
|
|
360
|
-
() => ({
|
|
361
|
-
experience,
|
|
362
|
-
member: mine ? member : null,
|
|
363
|
-
deliveries,
|
|
364
|
-
refresh: async () => observer.current?.refresh()
|
|
365
|
-
}),
|
|
366
|
-
[experience, mine, member, deliveries]
|
|
367
|
-
);
|
|
368
|
-
return /* @__PURE__ */ jsx(Context, { value, children });
|
|
369
|
-
}
|
|
370
|
-
function useExperience() {
|
|
371
|
-
const value = useContext2(Context);
|
|
372
|
-
if (!value) {
|
|
373
|
-
throw new Error("useExperience requires <RelayProvider>");
|
|
1088
|
+
} finally {
|
|
1089
|
+
this.#starting = false;
|
|
1090
|
+
}
|
|
374
1091
|
}
|
|
375
|
-
|
|
376
|
-
|
|
1092
|
+
/**
|
|
1093
|
+
* A new completion changes points, scores, profile completions and referral progress, whether
|
|
1094
|
+
* the participant submitted it or Domino completed it by itself: read them again.
|
|
1095
|
+
*/
|
|
1096
|
+
#completed(progress) {
|
|
1097
|
+
const mark = completionMark([
|
|
1098
|
+
...progress.quests,
|
|
1099
|
+
...progress.collections.flatMap((collection) => collection.quests)
|
|
1100
|
+
]);
|
|
1101
|
+
const previous = this.#completions;
|
|
1102
|
+
this.#completions = { member: progress.member, mark };
|
|
1103
|
+
if (previous?.member === progress.member && previous.mark !== mark) {
|
|
1104
|
+
void this.#scope?.cache.invalidate(completionChanges);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
#failSession(error) {
|
|
1108
|
+
this.#setSession(
|
|
1109
|
+
this.#session.status === "ready" ? { ...this.#session, error } : { status: "error", data: void 0, error }
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
#setSession(next) {
|
|
1113
|
+
this.#session = next;
|
|
1114
|
+
const key = next.status === "loading" ? null : next.status === "error" ? "unknown" : scopeKey(next.data);
|
|
1115
|
+
if (key === (this.#scope?.key ?? null)) {
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
this.#scope?.cache.dispose();
|
|
1119
|
+
if (key === null) {
|
|
1120
|
+
this.#scope = null;
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const session = next.status === "ready" ? next.data : null;
|
|
1124
|
+
this.#scope = {
|
|
1125
|
+
key,
|
|
1126
|
+
session,
|
|
1127
|
+
member: sessionMember(session),
|
|
1128
|
+
cache: new QueryCache(session?.id, () => void this.refreshSession())
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
#deriveExperience() {
|
|
1132
|
+
const session = this.#session;
|
|
1133
|
+
if (session.status === "loading") {
|
|
1134
|
+
return LOADING;
|
|
1135
|
+
}
|
|
1136
|
+
if (session.status === "error") {
|
|
1137
|
+
return session;
|
|
1138
|
+
}
|
|
1139
|
+
const member = this.#scope?.member ?? null;
|
|
1140
|
+
if (!member) {
|
|
1141
|
+
return SIGNED_OUT;
|
|
1142
|
+
}
|
|
1143
|
+
return this.#experience.owner === member ? this.#experience.state : LOADING;
|
|
1144
|
+
}
|
|
1145
|
+
#emit() {
|
|
1146
|
+
const experience = this.#deriveExperience();
|
|
1147
|
+
if (experience !== this.#experienceView) {
|
|
1148
|
+
this.#experienceView = experience;
|
|
1149
|
+
}
|
|
1150
|
+
for (const listener of [...this.#listeners]) {
|
|
1151
|
+
listener();
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
377
1155
|
|
|
378
1156
|
// src/react/provider.tsx
|
|
379
1157
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
@@ -382,10 +1160,252 @@ function RelayProvider({
|
|
|
382
1160
|
baseUrl = "/relay",
|
|
383
1161
|
children
|
|
384
1162
|
}) {
|
|
385
|
-
const
|
|
386
|
-
|
|
1163
|
+
const origin = client ? null : baseUrl;
|
|
1164
|
+
const store = useMemo2(
|
|
1165
|
+
() => new RelayStore(
|
|
1166
|
+
client ?? createBrowserRelayClient({ baseUrl: origin ?? baseUrl })
|
|
1167
|
+
),
|
|
1168
|
+
// A given client ignores `baseUrl`.
|
|
1169
|
+
[client, origin]
|
|
1170
|
+
);
|
|
1171
|
+
useEffect3(() => store.mount(), [store]);
|
|
1172
|
+
return /* @__PURE__ */ jsx2(StoreContext, { value: store, children: /* @__PURE__ */ jsx2(DeliveriesProvider, { children }) });
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/react/query.ts
|
|
1176
|
+
import {
|
|
1177
|
+
useCallback as useCallback3,
|
|
1178
|
+
useEffect as useEffect4,
|
|
1179
|
+
useLayoutEffect as useLayoutEffect2,
|
|
1180
|
+
useMemo as useMemo3,
|
|
1181
|
+
useRef as useRef2,
|
|
1182
|
+
useState as useState3,
|
|
1183
|
+
useSyncExternalStore as useSyncExternalStore3
|
|
1184
|
+
} from "react";
|
|
1185
|
+
function useLatest(value) {
|
|
1186
|
+
const ref = useRef2(value);
|
|
1187
|
+
useLayoutEffect2(() => {
|
|
1188
|
+
ref.current = value;
|
|
1189
|
+
});
|
|
1190
|
+
return ref;
|
|
1191
|
+
}
|
|
1192
|
+
function useSessionSnapshot(store) {
|
|
1193
|
+
const get = () => store.session;
|
|
1194
|
+
return useSyncExternalStore3(store.subscribe, get, get);
|
|
1195
|
+
}
|
|
1196
|
+
function useScope(store) {
|
|
1197
|
+
const get = () => store.scope;
|
|
1198
|
+
return useSyncExternalStore3(store.subscribe, get, get);
|
|
1199
|
+
}
|
|
1200
|
+
function useCached(cache, key, read, options) {
|
|
1201
|
+
const hash = key ? hashKey(key) : null;
|
|
1202
|
+
const { intervalMs, refetchOnFocus } = options;
|
|
1203
|
+
const latest = useLatest({ key, read });
|
|
1204
|
+
const subscribe = useCallback3(
|
|
1205
|
+
(listener) => {
|
|
1206
|
+
const { key: current } = latest.current;
|
|
1207
|
+
if (!cache || !current) {
|
|
1208
|
+
return () => {
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
return cache.subscribe(
|
|
1212
|
+
current,
|
|
1213
|
+
(context) => latest.current.read(context),
|
|
1214
|
+
{ intervalMs, refetchOnFocus },
|
|
1215
|
+
listener
|
|
1216
|
+
);
|
|
1217
|
+
},
|
|
1218
|
+
// `hash` stands for the key.
|
|
1219
|
+
[cache, hash, intervalMs, refetchOnFocus]
|
|
1220
|
+
);
|
|
1221
|
+
const get = () => cache && key ? cache.snapshot(key) : LOADING;
|
|
1222
|
+
const snapshot = useSyncExternalStore3(subscribe, get, get);
|
|
1223
|
+
const refresh = useCallback3(
|
|
1224
|
+
() => cache && latest.current.key ? cache.refresh(latest.current.key) : Promise.resolve(),
|
|
1225
|
+
[cache, hash]
|
|
1226
|
+
);
|
|
1227
|
+
return useMemo3(() => ({ ...snapshot, refresh }), [snapshot, refresh]);
|
|
1228
|
+
}
|
|
1229
|
+
function useRelayQuery(key, read, options = {}) {
|
|
1230
|
+
const store = useStore();
|
|
1231
|
+
const scope = useScope(store);
|
|
1232
|
+
return useCached(scope?.cache ?? null, key, read, options);
|
|
1233
|
+
}
|
|
1234
|
+
function useMemberQuery(key, read, options = {}) {
|
|
1235
|
+
const store = useStore();
|
|
1236
|
+
const scope = useScope(store);
|
|
1237
|
+
const member = scope?.member ?? null;
|
|
1238
|
+
const query = useCached(
|
|
1239
|
+
member ? scope.cache : null,
|
|
1240
|
+
member ? key : null,
|
|
1241
|
+
read,
|
|
1242
|
+
options
|
|
1243
|
+
);
|
|
1244
|
+
return useMemberGate(store, member, query);
|
|
1245
|
+
}
|
|
1246
|
+
function useMemberGate(store, member, query) {
|
|
1247
|
+
const session = useSessionSnapshot(store);
|
|
1248
|
+
return useMemo3(() => {
|
|
1249
|
+
if (session.status === "loading") {
|
|
1250
|
+
return { ...LOADING, refresh: query.refresh };
|
|
1251
|
+
}
|
|
1252
|
+
if (session.status === "error") {
|
|
1253
|
+
return {
|
|
1254
|
+
status: "error",
|
|
1255
|
+
data: void 0,
|
|
1256
|
+
error: session.error,
|
|
1257
|
+
refresh: store.rereadSession
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
return member ? query : { ...SIGNED_OUT, refresh: query.refresh };
|
|
1261
|
+
}, [session, member, query, store]);
|
|
1262
|
+
}
|
|
1263
|
+
function usePages(cache, key, read, options) {
|
|
1264
|
+
const hash = key ? hashKey(key) : null;
|
|
1265
|
+
const query = useCached(
|
|
1266
|
+
cache,
|
|
1267
|
+
key,
|
|
1268
|
+
(context) => read(context),
|
|
1269
|
+
options
|
|
1270
|
+
);
|
|
1271
|
+
const [more, setMore] = useState3(null);
|
|
1272
|
+
const busy = useRef2(null);
|
|
1273
|
+
const latest = useLatest({ key, read, query });
|
|
1274
|
+
useEffect4(
|
|
1275
|
+
() => () => {
|
|
1276
|
+
busy.current?.abort();
|
|
1277
|
+
busy.current = null;
|
|
1278
|
+
},
|
|
1279
|
+
[cache, hash]
|
|
1280
|
+
);
|
|
1281
|
+
const loadMore = useCallback3(async () => {
|
|
1282
|
+
const { key: current, read: reader, query: shown } = latest.current;
|
|
1283
|
+
if (!cache || !current || busy.current || shown.status !== "ready" || shown.data.nextCursor === null) {
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const owner = { cache, hash };
|
|
1287
|
+
const version = cache.version(current);
|
|
1288
|
+
const controller = new AbortController();
|
|
1289
|
+
busy.current = controller;
|
|
1290
|
+
setMore({ owner, loading: true, error: null });
|
|
1291
|
+
try {
|
|
1292
|
+
const page = await reader({
|
|
1293
|
+
signal: controller.signal,
|
|
1294
|
+
session: cache.session,
|
|
1295
|
+
cursor: shown.data.nextCursor
|
|
1296
|
+
});
|
|
1297
|
+
if (busy.current === controller) {
|
|
1298
|
+
cache.update(current, version, (data) => ({
|
|
1299
|
+
items: [...data.items, ...page.items],
|
|
1300
|
+
nextCursor: page.nextCursor
|
|
1301
|
+
}));
|
|
1302
|
+
}
|
|
1303
|
+
} catch (cause) {
|
|
1304
|
+
if (busy.current === controller) {
|
|
1305
|
+
setMore({ owner, loading: false, error: relayError(cause) });
|
|
1306
|
+
}
|
|
1307
|
+
} finally {
|
|
1308
|
+
if (busy.current === controller) {
|
|
1309
|
+
busy.current = null;
|
|
1310
|
+
setMore(
|
|
1311
|
+
(value) => value?.owner === owner ? { ...value, loading: false } : value
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}, [cache, hash]);
|
|
1316
|
+
const mine = more && more.owner.cache === cache && more.owner.hash === hash ? more : null;
|
|
1317
|
+
return useMemo3(
|
|
1318
|
+
() => ({
|
|
1319
|
+
...query,
|
|
1320
|
+
loadMore,
|
|
1321
|
+
loadingMore: mine?.loading ?? false,
|
|
1322
|
+
loadMoreError: mine?.error ?? null
|
|
1323
|
+
}),
|
|
1324
|
+
[query, loadMore, mine]
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
function useRelayPages(key, read, options = {}) {
|
|
1328
|
+
const store = useStore();
|
|
1329
|
+
const scope = useScope(store);
|
|
1330
|
+
return usePages(scope?.cache ?? null, key, read, options);
|
|
1331
|
+
}
|
|
1332
|
+
function useMemberPages(key, read, options = {}) {
|
|
1333
|
+
const store = useStore();
|
|
1334
|
+
const scope = useScope(store);
|
|
1335
|
+
const member = scope?.member ?? null;
|
|
1336
|
+
const pages = usePages(
|
|
1337
|
+
member ? scope.cache : null,
|
|
1338
|
+
member ? key : null,
|
|
1339
|
+
read,
|
|
1340
|
+
options
|
|
1341
|
+
);
|
|
1342
|
+
const gate = useMemberGate(store, member, pages);
|
|
1343
|
+
return useMemo3(
|
|
1344
|
+
() => gate === pages ? pages : {
|
|
1345
|
+
...gate,
|
|
1346
|
+
loadMore: pages.loadMore,
|
|
1347
|
+
loadingMore: false,
|
|
1348
|
+
loadMoreError: null
|
|
1349
|
+
},
|
|
1350
|
+
[gate, pages]
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
function useInvalidate() {
|
|
1354
|
+
const store = useStore();
|
|
1355
|
+
return useCallback3((keys = []) => store.invalidate(keys), [store]);
|
|
1356
|
+
}
|
|
1357
|
+
function useSession() {
|
|
1358
|
+
const store = useStore();
|
|
1359
|
+
const session = useSessionSnapshot(store);
|
|
1360
|
+
const refresh = store.rereadSession;
|
|
1361
|
+
return useMemo3(
|
|
1362
|
+
() => ({
|
|
1363
|
+
...session,
|
|
1364
|
+
refresh,
|
|
1365
|
+
member: session.status === "ready" ? sessionMember(session.data) : null
|
|
1366
|
+
}),
|
|
1367
|
+
[session, refresh]
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
function useAction() {
|
|
1371
|
+
const [busy, setBusy] = useState3(false);
|
|
1372
|
+
const [actionError, setActionError] = useState3(null);
|
|
1373
|
+
const run = useCallback3(async (task) => {
|
|
1374
|
+
setBusy(true);
|
|
1375
|
+
setActionError(null);
|
|
1376
|
+
try {
|
|
1377
|
+
await task();
|
|
1378
|
+
return true;
|
|
1379
|
+
} catch (cause) {
|
|
1380
|
+
setActionError(relayError(cause));
|
|
1381
|
+
return false;
|
|
1382
|
+
} finally {
|
|
1383
|
+
setBusy(false);
|
|
1384
|
+
}
|
|
1385
|
+
}, []);
|
|
1386
|
+
return { busy, actionError, run };
|
|
1387
|
+
}
|
|
1388
|
+
function useScopeSession() {
|
|
1389
|
+
const store = useStore();
|
|
1390
|
+
return useScope(store)?.session?.id;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// src/react/rewards.ts
|
|
1394
|
+
function useEntitlements(options = {}) {
|
|
1395
|
+
const relay = useRelay();
|
|
1396
|
+
return useMemberQuery(
|
|
1397
|
+
[...relayKeys.rewards, "entitlements"],
|
|
1398
|
+
(context) => relay.rewards.entitlements(context),
|
|
1399
|
+
{ intervalMs: 5e3, ...options }
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
function useDeliveries(options = {}) {
|
|
1403
|
+
const relay = useRelay();
|
|
1404
|
+
return useMemberQuery(
|
|
1405
|
+
[...relayKeys.rewards, "deliveries"],
|
|
1406
|
+
(context) => relay.rewards.deliveries(context),
|
|
1407
|
+
{ intervalMs: 5e3, ...options }
|
|
387
1408
|
);
|
|
388
|
-
return /* @__PURE__ */ jsx2(ClientContext, { value, children: /* @__PURE__ */ jsx2(ExperienceProvider, { children }) });
|
|
389
1409
|
}
|
|
390
1410
|
|
|
391
1411
|
// src/react/quest-groups.ts
|
|
@@ -409,10 +1429,17 @@ function questGroups(experience, order = []) {
|
|
|
409
1429
|
}
|
|
410
1430
|
|
|
411
1431
|
// src/react/use-quest.ts
|
|
412
|
-
import {
|
|
1432
|
+
import {
|
|
1433
|
+
useCallback as useCallback4,
|
|
1434
|
+
useEffect as useEffect5,
|
|
1435
|
+
useLayoutEffect as useLayoutEffect3,
|
|
1436
|
+
useMemo as useMemo4,
|
|
1437
|
+
useRef as useRef3,
|
|
1438
|
+
useState as useState4
|
|
1439
|
+
} from "react";
|
|
413
1440
|
function useQuest(observed) {
|
|
414
|
-
const
|
|
415
|
-
const
|
|
1441
|
+
const store = useStore();
|
|
1442
|
+
const relay = store.client;
|
|
416
1443
|
const [latest, setLatest] = useState4(null);
|
|
417
1444
|
const [draft, setDraft] = useState4(null);
|
|
418
1445
|
const [notice, setNotice] = useState4(null);
|
|
@@ -421,12 +1448,12 @@ function useQuest(observed) {
|
|
|
421
1448
|
const [discardedAttempt, setDiscardedAttempt] = useState4(null);
|
|
422
1449
|
const [saved, setSaved] = useState4("none");
|
|
423
1450
|
const [receipt, setReceipt] = useState4(null);
|
|
424
|
-
|
|
1451
|
+
useEffect5(() => setLatest(null), [observed.release]);
|
|
425
1452
|
const view = latest ?? observed;
|
|
426
1453
|
const { quest, release } = view;
|
|
427
1454
|
const completed = isFinished(view) || isAutomatic(view);
|
|
428
1455
|
const attempt = view.attempt?.id;
|
|
429
|
-
|
|
1456
|
+
useEffect5(() => {
|
|
430
1457
|
let active = true;
|
|
431
1458
|
relay.quests.open(view).then((controller) => controller.status()).then((status) => {
|
|
432
1459
|
if (!active) {
|
|
@@ -441,7 +1468,7 @@ function useQuest(observed) {
|
|
|
441
1468
|
active = false;
|
|
442
1469
|
};
|
|
443
1470
|
}, [relay, quest, release, completed, attempt, busy]);
|
|
444
|
-
const run =
|
|
1471
|
+
const run = useCallback4(
|
|
445
1472
|
async (action) => {
|
|
446
1473
|
setBusy(true);
|
|
447
1474
|
setError(null);
|
|
@@ -457,158 +1484,140 @@ function useQuest(observed) {
|
|
|
457
1484
|
setReceipt(result.attempt.id);
|
|
458
1485
|
setNotice("received");
|
|
459
1486
|
}
|
|
460
|
-
await
|
|
1487
|
+
await store.refreshExperience();
|
|
461
1488
|
} catch (cause) {
|
|
462
|
-
setError(
|
|
463
|
-
cause instanceof Error ? cause.message : "Something went wrong."
|
|
464
|
-
);
|
|
1489
|
+
setError(asRelayError(cause, "Something went wrong."));
|
|
465
1490
|
} finally {
|
|
466
1491
|
setBusy(false);
|
|
467
1492
|
}
|
|
468
1493
|
},
|
|
469
|
-
[
|
|
1494
|
+
[store]
|
|
470
1495
|
);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
view
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
setNotice(null);
|
|
492
|
-
} catch (cause) {
|
|
493
|
-
setError(
|
|
494
|
-
cause instanceof Error ? cause.message : "Could not start a new submission."
|
|
495
|
-
);
|
|
496
|
-
} finally {
|
|
497
|
-
setBusy(false);
|
|
498
|
-
}
|
|
499
|
-
},
|
|
500
|
-
submit: (input) => run(async () => {
|
|
501
|
-
if (view.connections.length) {
|
|
502
|
-
const accounts = await relay.auth.accounts();
|
|
503
|
-
const missing = view.connections.find(
|
|
504
|
-
(provider) => !accounts.some(
|
|
505
|
-
(account) => account.provider === provider && account.configured && account.status === "connected"
|
|
506
|
-
)
|
|
507
|
-
);
|
|
508
|
-
if (missing) {
|
|
509
|
-
throw new Error(`Connect ${missing} before verifying.`);
|
|
1496
|
+
const shown = useRef3(view);
|
|
1497
|
+
useLayoutEffect3(() => {
|
|
1498
|
+
shown.current = view;
|
|
1499
|
+
});
|
|
1500
|
+
const actions = useMemo4(
|
|
1501
|
+
() => ({
|
|
1502
|
+
restart: async () => {
|
|
1503
|
+
const current = shown.current;
|
|
1504
|
+
setBusy(true);
|
|
1505
|
+
setError(null);
|
|
1506
|
+
try {
|
|
1507
|
+
await (await relay.quests.open(current)).discard();
|
|
1508
|
+
setDiscardedAttempt(current.attempt?.id ?? null);
|
|
1509
|
+
setSaved("none");
|
|
1510
|
+
setDraft(null);
|
|
1511
|
+
setNotice(null);
|
|
1512
|
+
} catch (cause) {
|
|
1513
|
+
setError(asRelayError(cause, "Could not start a new submission."));
|
|
1514
|
+
} finally {
|
|
1515
|
+
setBusy(false);
|
|
510
1516
|
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
1517
|
+
},
|
|
1518
|
+
submit: (input) => run(async () => {
|
|
1519
|
+
const current = shown.current;
|
|
1520
|
+
if (current.connections.length) {
|
|
1521
|
+
const accounts = await relay.auth.accounts();
|
|
1522
|
+
const missing = current.connections.find(
|
|
1523
|
+
(provider) => !accounts.some(
|
|
1524
|
+
(account) => account.provider === provider && account.configured && account.status === "connected"
|
|
1525
|
+
)
|
|
1526
|
+
);
|
|
1527
|
+
if (missing) {
|
|
1528
|
+
throw new Error(`Connect ${missing} before verifying.`);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
const controller = await relay.quests.open(current);
|
|
1532
|
+
if ((controller.kind === "text" || controller.kind === "url") && input.kind === controller.kind) {
|
|
1533
|
+
return controller.submit(input.value);
|
|
1534
|
+
}
|
|
1535
|
+
if (controller.kind === "claim" && input.kind === "claim") {
|
|
1536
|
+
return controller.submit();
|
|
1537
|
+
}
|
|
1538
|
+
if (controller.kind === "quiz" && input.kind === "quiz") {
|
|
1539
|
+
return controller.submit(input.answers);
|
|
1540
|
+
}
|
|
1541
|
+
if (controller.kind === "photo" && input.kind === "photo") {
|
|
1542
|
+
return controller.submit(input.file);
|
|
1543
|
+
}
|
|
1544
|
+
throw new Error("This activity changed. Reload and try again.");
|
|
1545
|
+
}),
|
|
1546
|
+
/** Finish an interrupted submission, or reopen an outdated draft. */
|
|
1547
|
+
resume: () => run(async () => (await relay.quests.open(shown.current)).resume()),
|
|
1548
|
+
/**
|
|
1549
|
+
* Re-run verification that failed to run. Never creates a new attempt.
|
|
1550
|
+
* Domino refuses this for automatic quests, so never offer it for them.
|
|
1551
|
+
*/
|
|
1552
|
+
retry: () => run(async () => {
|
|
1553
|
+
const current = shown.current;
|
|
1554
|
+
if (isAutomatic(current)) {
|
|
1555
|
+
throw new Error("This activity updates by itself.");
|
|
1556
|
+
}
|
|
1557
|
+
return {
|
|
1558
|
+
kind: "submitted",
|
|
1559
|
+
attempt: await relay.quests.retry(current)
|
|
1560
|
+
};
|
|
1561
|
+
})
|
|
1562
|
+
}),
|
|
1563
|
+
[relay, run]
|
|
1564
|
+
);
|
|
1565
|
+
const startingFresh = view.attempt?.id === discardedAttempt && view.attempt?.status === "failed";
|
|
1566
|
+
return useMemo4(
|
|
1567
|
+
() => ({
|
|
1568
|
+
/** The newest known version of the quest. Render this, not the prop. */
|
|
1569
|
+
view,
|
|
1570
|
+
/** Input recovered after the quest changed. Prefill the form with it. */
|
|
1571
|
+
draft,
|
|
1572
|
+
notice,
|
|
1573
|
+
error,
|
|
1574
|
+
busy,
|
|
1575
|
+
saved,
|
|
1576
|
+
/** The attempt the last "received" notice is about. */
|
|
1577
|
+
receipt,
|
|
1578
|
+
startingFresh,
|
|
1579
|
+
...actions
|
|
526
1580
|
}),
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Re-run verification that failed to run. Never creates a new attempt.
|
|
531
|
-
* Domino refuses this for automatic quests, so never offer it for them.
|
|
532
|
-
*/
|
|
533
|
-
retry: () => run(async () => {
|
|
534
|
-
if (isAutomatic(view)) {
|
|
535
|
-
throw new Error("This activity updates by itself.");
|
|
536
|
-
}
|
|
537
|
-
return {
|
|
538
|
-
kind: "submitted",
|
|
539
|
-
attempt: await relay.quests.retry(view)
|
|
540
|
-
};
|
|
541
|
-
})
|
|
542
|
-
};
|
|
1581
|
+
[view, draft, notice, error, busy, saved, receipt, startingFresh, actions]
|
|
1582
|
+
);
|
|
543
1583
|
}
|
|
544
1584
|
|
|
545
1585
|
// src/react/auth.ts
|
|
546
|
-
import { useCallback as
|
|
1586
|
+
import { useCallback as useCallback5, useMemo as useMemo6, useRef as useRef4, useState as useState6 } from "react";
|
|
547
1587
|
|
|
548
1588
|
// src/react/wallets.ts
|
|
549
|
-
import {
|
|
550
|
-
var WALLETS_CHANGED = "relay-wallets-changed";
|
|
1589
|
+
import { useEffect as useEffect6, useMemo as useMemo5, useState as useState5 } from "react";
|
|
551
1590
|
function useWallets() {
|
|
552
1591
|
const relay = useRelay();
|
|
553
|
-
const
|
|
554
|
-
const
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
return () => {
|
|
577
|
-
window.removeEventListener("focus", update);
|
|
578
|
-
window.removeEventListener(WALLETS_CHANGED, update);
|
|
1592
|
+
const invalidate = useInvalidate();
|
|
1593
|
+
const session = useScopeSession();
|
|
1594
|
+
const query = useMemberQuery(
|
|
1595
|
+
relayKeys.wallets,
|
|
1596
|
+
(options) => relay.wallets.list(options),
|
|
1597
|
+
{ refetchOnFocus: true }
|
|
1598
|
+
);
|
|
1599
|
+
const { busy, actionError, run } = useAction();
|
|
1600
|
+
const actions = useMemo5(() => {
|
|
1601
|
+
const changed = () => void invalidate([relayKeys.wallets, relayKeys.experience]);
|
|
1602
|
+
return {
|
|
1603
|
+
/** Links the wallet to the member the list was read for; another session is refused. */
|
|
1604
|
+
link: (signer) => run(async () => {
|
|
1605
|
+
await relay.wallets.link(signer, { session });
|
|
1606
|
+
changed();
|
|
1607
|
+
}),
|
|
1608
|
+
unlink: (wallet) => run(async () => {
|
|
1609
|
+
await relay.wallets.unlink(
|
|
1610
|
+
{ chain: wallet.chain, address: wallet.address },
|
|
1611
|
+
{ session }
|
|
1612
|
+
);
|
|
1613
|
+
changed();
|
|
1614
|
+
})
|
|
579
1615
|
};
|
|
580
|
-
}, [
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
await task();
|
|
586
|
-
} catch (cause) {
|
|
587
|
-
setError(
|
|
588
|
-
cause instanceof Error && cause.message ? cause.message : fallback
|
|
589
|
-
);
|
|
590
|
-
} finally {
|
|
591
|
-
setBusy(false);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
return {
|
|
595
|
-
/** Null until read, and while nobody is signed in. */
|
|
596
|
-
wallets,
|
|
597
|
-
error,
|
|
598
|
-
busy,
|
|
599
|
-
refresh,
|
|
600
|
-
link: (signer) => run(async () => {
|
|
601
|
-
await relay.wallets.link(signer);
|
|
602
|
-
window.dispatchEvent(new Event(WALLETS_CHANGED));
|
|
603
|
-
}, "Couldn't link this wallet. Try again."),
|
|
604
|
-
unlink: (wallet) => run(async () => {
|
|
605
|
-
await relay.wallets.unlink({
|
|
606
|
-
chain: wallet.chain,
|
|
607
|
-
address: wallet.address
|
|
608
|
-
});
|
|
609
|
-
window.dispatchEvent(new Event(WALLETS_CHANGED));
|
|
610
|
-
}, "Couldn't unlink this wallet. Try again.")
|
|
611
|
-
};
|
|
1616
|
+
}, [relay, invalidate, session, run]);
|
|
1617
|
+
return useMemo5(
|
|
1618
|
+
() => ({ ...query, busy, actionError, ...actions }),
|
|
1619
|
+
[query, busy, actionError, actions]
|
|
1620
|
+
);
|
|
612
1621
|
}
|
|
613
1622
|
function discoverChains(key) {
|
|
614
1623
|
return JSON.parse(key).flatMap(
|
|
@@ -618,7 +1627,7 @@ function discoverChains(key) {
|
|
|
618
1627
|
function useWalletDiscovery(options = {}) {
|
|
619
1628
|
const chains = JSON.stringify(cosmosChainList(options.cosmos));
|
|
620
1629
|
const [found, setFound] = useState5({ evm: [], solana: [], cosmos: [] });
|
|
621
|
-
|
|
1630
|
+
useEffect6(() => {
|
|
622
1631
|
let active = true;
|
|
623
1632
|
const solana = discoverSolanaWallets();
|
|
624
1633
|
const cosmos = discoverChains(chains);
|
|
@@ -641,7 +1650,7 @@ function useSignInWallets(networks, options = {}) {
|
|
|
641
1650
|
const key = networks ? `${networks.join(" ")}
|
|
642
1651
|
${chains}` : null;
|
|
643
1652
|
const [found, setFound] = useState5(null);
|
|
644
|
-
|
|
1653
|
+
useEffect6(() => {
|
|
645
1654
|
if (key === null) {
|
|
646
1655
|
return;
|
|
647
1656
|
}
|
|
@@ -663,73 +1672,47 @@ ${chains}` : null;
|
|
|
663
1672
|
}
|
|
664
1673
|
|
|
665
1674
|
// src/react/auth.ts
|
|
1675
|
+
var NONE_UNAVAILABLE = [];
|
|
666
1676
|
function useSignInMethods() {
|
|
667
1677
|
const relay = useRelay();
|
|
668
|
-
|
|
669
|
-
useEffect5(() => {
|
|
670
|
-
let active = true;
|
|
671
|
-
void readSignInMethods(relay).then((result) => {
|
|
672
|
-
if (active) {
|
|
673
|
-
setMethods(result);
|
|
674
|
-
}
|
|
675
|
-
});
|
|
676
|
-
return () => {
|
|
677
|
-
active = false;
|
|
678
|
-
};
|
|
679
|
-
}, [relay]);
|
|
680
|
-
return methods;
|
|
1678
|
+
return useRelayQuery(relayKeys.signInMethods, () => readSignInMethods(relay));
|
|
681
1679
|
}
|
|
682
1680
|
function useMe() {
|
|
683
1681
|
const relay = useRelay();
|
|
684
|
-
|
|
685
|
-
const [me, setMe] = useState6(null);
|
|
686
|
-
useEffect5(() => {
|
|
687
|
-
if (!member) {
|
|
688
|
-
setMe(null);
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
let active = true;
|
|
692
|
-
void relay.me.get().then((value) => active && setMe(value)).catch(() => active && setMe(null));
|
|
693
|
-
return () => {
|
|
694
|
-
active = false;
|
|
695
|
-
};
|
|
696
|
-
}, [relay, member]);
|
|
697
|
-
return me;
|
|
1682
|
+
return useMemberQuery(relayKeys.me, (options) => relay.me.get(options));
|
|
698
1683
|
}
|
|
699
1684
|
function useSignIn() {
|
|
700
1685
|
const relay = useRelay();
|
|
701
1686
|
const [challenge, setChallengeState] = useState6(null);
|
|
702
|
-
const latest =
|
|
1687
|
+
const latest = useRef4(null);
|
|
703
1688
|
const [busy, setBusy] = useState6(false);
|
|
704
1689
|
const [error, setError] = useState6(null);
|
|
705
|
-
const setChallenge =
|
|
1690
|
+
const setChallenge = useCallback5((value) => {
|
|
706
1691
|
latest.current = value;
|
|
707
1692
|
setChallengeState(value);
|
|
708
1693
|
}, []);
|
|
709
|
-
const run =
|
|
1694
|
+
const run = useCallback5(
|
|
710
1695
|
async (task, fallback) => {
|
|
711
1696
|
setBusy(true);
|
|
712
1697
|
setError(null);
|
|
713
1698
|
try {
|
|
714
1699
|
await task();
|
|
715
1700
|
} catch (cause) {
|
|
716
|
-
setError(
|
|
717
|
-
cause instanceof Error && cause.message ? cause.message : fallback
|
|
718
|
-
);
|
|
1701
|
+
setError(asRelayError(cause, fallback));
|
|
719
1702
|
} finally {
|
|
720
1703
|
setBusy(false);
|
|
721
1704
|
}
|
|
722
1705
|
},
|
|
723
1706
|
[]
|
|
724
1707
|
);
|
|
725
|
-
const sendCode =
|
|
1708
|
+
const sendCode = useCallback5(
|
|
726
1709
|
(email) => run(async () => {
|
|
727
1710
|
setChallenge(null);
|
|
728
1711
|
setChallenge((await relay.auth.emailStart(email)).challenge);
|
|
729
1712
|
}, "Couldn't send a code. Check the address and try again."),
|
|
730
1713
|
[relay, run, setChallenge]
|
|
731
1714
|
);
|
|
732
|
-
const verifyCode =
|
|
1715
|
+
const verifyCode = useCallback5(
|
|
733
1716
|
(code) => run(async () => {
|
|
734
1717
|
const sent = latest.current;
|
|
735
1718
|
if (!sent) {
|
|
@@ -740,59 +1723,70 @@ function useSignIn() {
|
|
|
740
1723
|
}, "That code didn't work. Check it, or request a new one."),
|
|
741
1724
|
[relay, run, setChallenge]
|
|
742
1725
|
);
|
|
743
|
-
const startOver =
|
|
1726
|
+
const startOver = useCallback5(() => {
|
|
744
1727
|
setChallenge(null);
|
|
745
1728
|
setError(null);
|
|
746
1729
|
}, [setChallenge]);
|
|
747
|
-
const withProvider =
|
|
1730
|
+
const withProvider = useCallback5(
|
|
748
1731
|
(provider) => run(async () => {
|
|
749
1732
|
const { url } = await relay.auth.start(provider, "sign-in");
|
|
750
1733
|
window.location.assign(url);
|
|
751
1734
|
}, "Couldn't start sign-in. Try again."),
|
|
752
1735
|
[relay, run]
|
|
753
1736
|
);
|
|
754
|
-
const withWallet =
|
|
1737
|
+
const withWallet = useCallback5(
|
|
755
1738
|
(signer) => run(async () => {
|
|
756
1739
|
await relay.auth.wallet.signIn(signer);
|
|
757
1740
|
setChallenge(null);
|
|
758
1741
|
}, "Couldn't sign in with this wallet. Try again."),
|
|
759
1742
|
[relay, run, setChallenge]
|
|
760
1743
|
);
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1744
|
+
const codeSent = challenge !== null;
|
|
1745
|
+
return useMemo6(
|
|
1746
|
+
() => ({
|
|
1747
|
+
busy,
|
|
1748
|
+
error,
|
|
1749
|
+
/** True once a code was sent and the form should ask for it. */
|
|
1750
|
+
codeSent,
|
|
1751
|
+
sendCode,
|
|
1752
|
+
verifyCode,
|
|
1753
|
+
startOver,
|
|
1754
|
+
withProvider,
|
|
1755
|
+
/** Signs in, or signs up, with a wallet; the wallet asks the participant to sign a message. */
|
|
1756
|
+
withWallet
|
|
1757
|
+
}),
|
|
1758
|
+
[
|
|
1759
|
+
busy,
|
|
1760
|
+
error,
|
|
1761
|
+
codeSent,
|
|
1762
|
+
sendCode,
|
|
1763
|
+
verifyCode,
|
|
1764
|
+
startOver,
|
|
1765
|
+
withProvider,
|
|
1766
|
+
withWallet
|
|
1767
|
+
]
|
|
1768
|
+
);
|
|
773
1769
|
}
|
|
774
1770
|
function useSignOut() {
|
|
775
|
-
const
|
|
1771
|
+
const store = useStore();
|
|
776
1772
|
const [busy, setBusy] = useState6(false);
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
} finally {
|
|
784
|
-
setBusy(false);
|
|
785
|
-
}
|
|
1773
|
+
const signOut = useCallback5(async () => {
|
|
1774
|
+
setBusy(true);
|
|
1775
|
+
try {
|
|
1776
|
+
await store.signOut();
|
|
1777
|
+
} finally {
|
|
1778
|
+
setBusy(false);
|
|
786
1779
|
}
|
|
787
|
-
};
|
|
1780
|
+
}, [store]);
|
|
1781
|
+
return useMemo6(() => ({ busy, signOut }), [busy, signOut]);
|
|
788
1782
|
}
|
|
789
1783
|
function useSignInOptions(input = {}) {
|
|
790
1784
|
const chains = JSON.stringify(cosmosChainList(input.cosmos));
|
|
791
|
-
const cosmos =
|
|
1785
|
+
const cosmos = useMemo6(
|
|
792
1786
|
() => JSON.parse(chains),
|
|
793
1787
|
[chains]
|
|
794
1788
|
);
|
|
795
|
-
const methods = useSignInMethods();
|
|
1789
|
+
const methods = useSignInMethods().data ?? null;
|
|
796
1790
|
const wallets = useSignInWallets(methods?.wallets, { cosmos });
|
|
797
1791
|
const {
|
|
798
1792
|
busy,
|
|
@@ -804,7 +1798,7 @@ function useSignInOptions(input = {}) {
|
|
|
804
1798
|
withProvider,
|
|
805
1799
|
withWallet
|
|
806
1800
|
} = useSignIn();
|
|
807
|
-
const built =
|
|
1801
|
+
const built = useMemo6(() => {
|
|
808
1802
|
if (!methods) {
|
|
809
1803
|
return null;
|
|
810
1804
|
}
|
|
@@ -822,20 +1816,23 @@ function useSignInOptions(input = {}) {
|
|
|
822
1816
|
}
|
|
823
1817
|
});
|
|
824
1818
|
}, [methods, wallets, cosmos, withProvider, sendCode, verifyCode, withWallet]);
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1819
|
+
const methodsError = methods?.error ?? null;
|
|
1820
|
+
return useMemo6(
|
|
1821
|
+
() => ({
|
|
1822
|
+
options: built?.options ?? null,
|
|
1823
|
+
unavailable: built?.unavailable ?? NONE_UNAVAILABLE,
|
|
1824
|
+
methodsError,
|
|
1825
|
+
busy,
|
|
1826
|
+
error,
|
|
1827
|
+
codeSent,
|
|
1828
|
+
startOver
|
|
1829
|
+
}),
|
|
1830
|
+
[built, methodsError, busy, error, codeSent, startOver]
|
|
1831
|
+
);
|
|
834
1832
|
}
|
|
835
1833
|
|
|
836
1834
|
// src/react/accounts.ts
|
|
837
|
-
import {
|
|
838
|
-
var CHANGED = "relay-accounts-changed";
|
|
1835
|
+
import { useMemo as useMemo7 } from "react";
|
|
839
1836
|
function isConnected(accounts, provider) {
|
|
840
1837
|
return Boolean(
|
|
841
1838
|
accounts?.some(
|
|
@@ -845,304 +1842,124 @@ function isConnected(accounts, provider) {
|
|
|
845
1842
|
}
|
|
846
1843
|
function useAccounts() {
|
|
847
1844
|
const relay = useRelay();
|
|
848
|
-
const
|
|
849
|
-
const
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
setBusy(false);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
return {
|
|
890
|
-
/** Null until read, and while nobody is signed in. */
|
|
891
|
-
accounts,
|
|
892
|
-
error,
|
|
893
|
-
busy,
|
|
894
|
-
refresh,
|
|
895
|
-
isConnected: (provider) => isConnected(accounts, provider),
|
|
896
|
-
connect: (provider) => run(async () => {
|
|
897
|
-
const { url } = await relay.auth.start(provider, "connect");
|
|
898
|
-
window.location.assign(url);
|
|
899
|
-
}, "Couldn't start connecting. Try again."),
|
|
900
|
-
disconnect: (provider) => run(async () => {
|
|
901
|
-
await relay.auth.disconnect(provider);
|
|
902
|
-
window.dispatchEvent(new Event(CHANGED));
|
|
903
|
-
}, "Couldn't disconnect. Try again.")
|
|
904
|
-
};
|
|
1845
|
+
const invalidate = useInvalidate();
|
|
1846
|
+
const session = useScopeSession();
|
|
1847
|
+
const query = useMemberQuery(
|
|
1848
|
+
relayKeys.accounts,
|
|
1849
|
+
(options) => relay.auth.accounts(options),
|
|
1850
|
+
{ refetchOnFocus: true }
|
|
1851
|
+
);
|
|
1852
|
+
const { busy, actionError, run } = useAction();
|
|
1853
|
+
const actions = useMemo7(
|
|
1854
|
+
() => ({
|
|
1855
|
+
connect: (provider) => run(async () => {
|
|
1856
|
+
const { url } = await relay.auth.start(provider, "connect", {
|
|
1857
|
+
session
|
|
1858
|
+
});
|
|
1859
|
+
window.location.assign(url);
|
|
1860
|
+
}),
|
|
1861
|
+
disconnect: (provider) => run(async () => {
|
|
1862
|
+
await relay.auth.disconnect(provider, { session });
|
|
1863
|
+
void invalidate([
|
|
1864
|
+
relayKeys.accounts,
|
|
1865
|
+
relayKeys.experience,
|
|
1866
|
+
relayKeys.profiles
|
|
1867
|
+
]);
|
|
1868
|
+
})
|
|
1869
|
+
}),
|
|
1870
|
+
[relay, invalidate, session, run]
|
|
1871
|
+
);
|
|
1872
|
+
const accounts = query.data;
|
|
1873
|
+
return useMemo7(
|
|
1874
|
+
() => ({
|
|
1875
|
+
...query,
|
|
1876
|
+
busy,
|
|
1877
|
+
actionError,
|
|
1878
|
+
isConnected: (provider) => isConnected(accounts, provider),
|
|
1879
|
+
...actions
|
|
1880
|
+
}),
|
|
1881
|
+
[query, busy, actionError, accounts, actions]
|
|
1882
|
+
);
|
|
905
1883
|
}
|
|
906
1884
|
|
|
907
1885
|
// src/react/profiles.ts
|
|
908
|
-
import {
|
|
909
|
-
var
|
|
1886
|
+
import { useMemo as useMemo8 } from "react";
|
|
1887
|
+
var MINE = [...relayKeys.profiles, "mine"];
|
|
910
1888
|
function useMyProfile() {
|
|
911
1889
|
const relay = useRelay();
|
|
912
|
-
const
|
|
913
|
-
const
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1890
|
+
const store = useStore();
|
|
1891
|
+
const session = useScopeSession();
|
|
1892
|
+
const query = useMemberQuery(
|
|
1893
|
+
MINE,
|
|
1894
|
+
(options) => relay.profiles.mine(options),
|
|
1895
|
+
{ refetchOnFocus: true }
|
|
1896
|
+
);
|
|
1897
|
+
const { busy, actionError, run } = useAction();
|
|
1898
|
+
const actions = useMemo8(() => {
|
|
1899
|
+
const change = (task) => run(async () => {
|
|
1900
|
+
const cache = store.scope?.cache;
|
|
1901
|
+
const saved = await task();
|
|
1902
|
+
cache?.set(MINE, saved);
|
|
1903
|
+
void cache?.invalidate([
|
|
1904
|
+
[...relayKeys.profiles, "get"],
|
|
1905
|
+
relayKeys.leaderboards
|
|
922
1906
|
]);
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
if (!member) {
|
|
934
|
-
setProfile(null);
|
|
935
|
-
session.current = null;
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
const update = () => void refresh();
|
|
939
|
-
update();
|
|
940
|
-
window.addEventListener("focus", update);
|
|
941
|
-
window.addEventListener(PROFILE_CHANGED, update);
|
|
942
|
-
return () => {
|
|
943
|
-
window.removeEventListener("focus", update);
|
|
944
|
-
window.removeEventListener(PROFILE_CHANGED, update);
|
|
945
|
-
};
|
|
946
|
-
}, [member, refresh]);
|
|
947
|
-
async function run(task, fallback) {
|
|
948
|
-
setBusy(true);
|
|
949
|
-
setError(null);
|
|
950
|
-
try {
|
|
951
|
-
const value = await task(session.current ?? void 0);
|
|
952
|
-
setProfile(value);
|
|
953
|
-
window.dispatchEvent(new Event(PROFILE_CHANGED));
|
|
954
|
-
return true;
|
|
955
|
-
} catch (cause) {
|
|
956
|
-
setError(
|
|
957
|
-
cause instanceof Error && cause.message ? cause.message : fallback
|
|
958
|
-
);
|
|
959
|
-
return false;
|
|
960
|
-
} finally {
|
|
961
|
-
setBusy(false);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
return {
|
|
965
|
-
/** Null until read, and while nobody is signed in. */
|
|
966
|
-
profile,
|
|
967
|
-
error,
|
|
968
|
-
busy,
|
|
969
|
-
refresh,
|
|
970
|
-
/** Resolves true once saved; a failure sets `error`. */
|
|
971
|
-
update: (input) => run(
|
|
972
|
-
(value) => relay.profiles.update(input, { session: value }),
|
|
973
|
-
"Couldn't save your profile. Try again."
|
|
974
|
-
),
|
|
975
|
-
/** Uploads a JPEG, PNG or WebP of at most 5 MB as the avatar; resolves true once saved. */
|
|
976
|
-
uploadAvatar: (file) => run(
|
|
977
|
-
(value) => relay.profiles.uploadAvatar(file, {
|
|
978
|
-
uploadId: crypto.randomUUID(),
|
|
979
|
-
session: value
|
|
980
|
-
}),
|
|
981
|
-
"Couldn't upload this picture. Try again."
|
|
982
|
-
)
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
|
-
function useProfileRead(read, intervalMs, fallback) {
|
|
986
|
-
const { member } = useExperience();
|
|
987
|
-
const [state, setState] = useState8({
|
|
988
|
-
status: "loading"
|
|
989
|
-
});
|
|
990
|
-
const refresh = useCallback5(async () => {
|
|
991
|
-
try {
|
|
992
|
-
const value = await read();
|
|
993
|
-
setState({ status: "ready", value, stale: null });
|
|
994
|
-
} catch (cause) {
|
|
995
|
-
if (cause instanceof RelayError && cause.status === 404) {
|
|
996
|
-
setState({ status: "missing" });
|
|
997
|
-
return;
|
|
998
|
-
}
|
|
999
|
-
const message = cause instanceof Error && cause.message ? cause.message : fallback;
|
|
1000
|
-
setState(
|
|
1001
|
-
(previous) => previous.status === "ready" ? { ...previous, stale: message } : { status: "error", message }
|
|
1002
|
-
);
|
|
1003
|
-
}
|
|
1004
|
-
}, [read, fallback]);
|
|
1005
|
-
useEffect7(() => {
|
|
1006
|
-
setState({ status: "loading" });
|
|
1007
|
-
void refresh();
|
|
1008
|
-
const timer = setInterval(() => {
|
|
1009
|
-
if (document.visibilityState === "visible") {
|
|
1010
|
-
void refresh();
|
|
1011
|
-
}
|
|
1012
|
-
}, intervalMs);
|
|
1013
|
-
const changed = () => void refresh();
|
|
1014
|
-
window.addEventListener(PROFILE_CHANGED, changed);
|
|
1015
|
-
return () => {
|
|
1016
|
-
clearInterval(timer);
|
|
1017
|
-
window.removeEventListener(PROFILE_CHANGED, changed);
|
|
1907
|
+
});
|
|
1908
|
+
return {
|
|
1909
|
+
update: (input) => change(() => relay.profiles.update(input, { session })),
|
|
1910
|
+
/** Uploads a JPEG, PNG or WebP of at most 5 MB as the avatar. */
|
|
1911
|
+
uploadAvatar: (file) => change(
|
|
1912
|
+
() => relay.profiles.uploadAvatar(file, {
|
|
1913
|
+
uploadId: crypto.randomUUID(),
|
|
1914
|
+
session
|
|
1915
|
+
})
|
|
1916
|
+
)
|
|
1018
1917
|
};
|
|
1019
|
-
}, [
|
|
1020
|
-
return
|
|
1918
|
+
}, [relay, store, session, run]);
|
|
1919
|
+
return useMemo8(
|
|
1920
|
+
() => ({ ...query, busy, actionError, ...actions }),
|
|
1921
|
+
[query, busy, actionError, actions]
|
|
1922
|
+
);
|
|
1021
1923
|
}
|
|
1022
1924
|
function useProfile(id, options = {}) {
|
|
1023
1925
|
const relay = useRelay();
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
options.intervalMs ?? 6e4
|
|
1028
|
-
"Could not load the profile."
|
|
1926
|
+
return useRelayQuery(
|
|
1927
|
+
[...relayKeys.profiles, "get", id],
|
|
1928
|
+
(request) => relay.profiles.get(id, request),
|
|
1929
|
+
{ intervalMs: options.intervalMs ?? 6e4 }
|
|
1029
1930
|
);
|
|
1030
|
-
const value = state.status === "ready" ? { status: "ready", profile: state.value, stale: state.stale } : state;
|
|
1031
|
-
return { ...value, refresh };
|
|
1032
1931
|
}
|
|
1033
1932
|
function useLeaderboardPlacement(board, profile, options = {}) {
|
|
1034
1933
|
const relay = useRelay();
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1934
|
+
return useRelayQuery(
|
|
1935
|
+
[...relayKeys.leaderboards, board, "placement", profile],
|
|
1936
|
+
(request) => relay.leaderboards.placement(board, profile, request),
|
|
1937
|
+
{ intervalMs: options.intervalMs ?? 3e4 }
|
|
1038
1938
|
);
|
|
1039
|
-
const { state, refresh } = useProfileRead(
|
|
1040
|
-
read,
|
|
1041
|
-
options.intervalMs ?? 3e4,
|
|
1042
|
-
"Could not load the leaderboard placement."
|
|
1043
|
-
);
|
|
1044
|
-
return { ...state, refresh };
|
|
1045
1939
|
}
|
|
1046
1940
|
function useProfileReferral(profile, program, options = {}) {
|
|
1047
1941
|
const relay = useRelay();
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
const { state, refresh } = useProfileRead(
|
|
1053
|
-
read,
|
|
1054
|
-
options.intervalMs ?? 6e4,
|
|
1055
|
-
"Could not load referral progress."
|
|
1942
|
+
return useMemberQuery(
|
|
1943
|
+
[...relayKeys.profiles, "referral", profile, program],
|
|
1944
|
+
(request) => relay.profiles.referral(profile, program, request),
|
|
1945
|
+
{ intervalMs: options.intervalMs ?? 6e4 }
|
|
1056
1946
|
);
|
|
1057
|
-
return { ...state, refresh };
|
|
1058
1947
|
}
|
|
1059
1948
|
function useProfileCompletions(profile, options = {}) {
|
|
1060
1949
|
const relay = useRelay();
|
|
1061
|
-
const { member } = useExperience();
|
|
1062
1950
|
const { limit = 20 } = options;
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
async (first) => {
|
|
1071
|
-
if (first) {
|
|
1072
|
-
generation.current++;
|
|
1073
|
-
paging.current = { ready: false, busy: false };
|
|
1074
|
-
} else {
|
|
1075
|
-
if (!paging.current.ready || paging.current.busy || cursor.current === null) {
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
paging.current.busy = true;
|
|
1079
|
-
setState(
|
|
1080
|
-
(previous) => previous.status === "ready" ? { ...previous, loading: true, error: null } : previous
|
|
1081
|
-
);
|
|
1082
|
-
}
|
|
1083
|
-
const current = generation.current;
|
|
1084
|
-
const own = paging.current;
|
|
1085
|
-
try {
|
|
1086
|
-
const page = await relay.profiles.completions(profile, {
|
|
1087
|
-
limit,
|
|
1088
|
-
...!first && cursor.current ? { cursor: cursor.current } : {}
|
|
1089
|
-
});
|
|
1090
|
-
if (current !== generation.current) {
|
|
1091
|
-
return;
|
|
1092
|
-
}
|
|
1093
|
-
cursor.current = page.nextCursor;
|
|
1094
|
-
own.ready = true;
|
|
1095
|
-
setState((previous) => ({
|
|
1096
|
-
status: "ready",
|
|
1097
|
-
items: !first && previous.status === "ready" ? [...previous.items, ...page.items] : page.items,
|
|
1098
|
-
more: page.nextCursor !== null,
|
|
1099
|
-
loading: false,
|
|
1100
|
-
error: null
|
|
1101
|
-
}));
|
|
1102
|
-
} catch (cause) {
|
|
1103
|
-
if (current !== generation.current) {
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
if (first && cause instanceof RelayError && cause.status === 404) {
|
|
1107
|
-
setState({ status: "missing" });
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
const message = cause instanceof Error && cause.message ? cause.message : "Could not load completed quests.";
|
|
1111
|
-
setState(
|
|
1112
|
-
(previous) => !first && previous.status === "ready" ? { ...previous, loading: false, error: message } : { status: "error", message }
|
|
1113
|
-
);
|
|
1114
|
-
} finally {
|
|
1115
|
-
if (!first) {
|
|
1116
|
-
own.busy = false;
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
},
|
|
1120
|
-
[relay, profile, limit]
|
|
1951
|
+
return useRelayPages(
|
|
1952
|
+
[...relayKeys.profiles, "completions", profile, limit],
|
|
1953
|
+
({ cursor, ...request }) => relay.profiles.completions(
|
|
1954
|
+
profile,
|
|
1955
|
+
cursor ? { limit, cursor } : { limit },
|
|
1956
|
+
request
|
|
1957
|
+
)
|
|
1121
1958
|
);
|
|
1122
|
-
useEffect7(() => {
|
|
1123
|
-
setState({ status: "loading" });
|
|
1124
|
-
cursor.current = null;
|
|
1125
|
-
void load(true);
|
|
1126
|
-
const changed = () => void load(true);
|
|
1127
|
-
window.addEventListener(PROFILE_CHANGED, changed);
|
|
1128
|
-
return () => {
|
|
1129
|
-
generation.current++;
|
|
1130
|
-
window.removeEventListener(PROFILE_CHANGED, changed);
|
|
1131
|
-
};
|
|
1132
|
-
}, [load, member]);
|
|
1133
|
-
return {
|
|
1134
|
-
...state,
|
|
1135
|
-
refresh: () => load(true),
|
|
1136
|
-
/**
|
|
1137
|
-
* Appends the next page; does nothing while one loads, while the first page reloads, or when
|
|
1138
|
-
* there is none.
|
|
1139
|
-
*/
|
|
1140
|
-
loadMore: () => load(false)
|
|
1141
|
-
};
|
|
1142
1959
|
}
|
|
1143
1960
|
|
|
1144
1961
|
// src/react/referrals.ts
|
|
1145
|
-
import { useCallback as useCallback6, useEffect as
|
|
1962
|
+
import { useCallback as useCallback6, useEffect as useEffect7, useMemo as useMemo9, useRef as useRef5, useState as useState7 } from "react";
|
|
1146
1963
|
var PENDING = "domino:pending-referral";
|
|
1147
1964
|
var CODE = /^[a-zA-Z0-9_-]{1,100}$/;
|
|
1148
1965
|
function captureReferralCode(param = "ref") {
|
|
@@ -1167,12 +1984,13 @@ function forgetReferralCode() {
|
|
|
1167
1984
|
}
|
|
1168
1985
|
function useInvitation(param = "ref") {
|
|
1169
1986
|
const relay = useRelay();
|
|
1170
|
-
const
|
|
1171
|
-
const
|
|
1987
|
+
const invalidate = useInvalidate();
|
|
1988
|
+
const { member } = useSession();
|
|
1989
|
+
const [state, setState] = useState7(() => {
|
|
1172
1990
|
const code = captureReferralCode(param);
|
|
1173
1991
|
return code ? { status: "pending", code } : { status: "none" };
|
|
1174
1992
|
});
|
|
1175
|
-
const started =
|
|
1993
|
+
const started = useRef5(null);
|
|
1176
1994
|
const accept = useCallback6(
|
|
1177
1995
|
async (code) => {
|
|
1178
1996
|
setState({ status: "accepting", code });
|
|
@@ -1180,152 +1998,189 @@ function useInvitation(param = "ref") {
|
|
|
1180
1998
|
await relay.referrals.accept({ code, actionId: `accept-${code}` });
|
|
1181
1999
|
forgetReferralCode();
|
|
1182
2000
|
setState({ status: "accepted", code });
|
|
2001
|
+
void invalidate([relayKeys.referrals, relayKeys.profiles]);
|
|
1183
2002
|
} catch (cause) {
|
|
1184
|
-
const refused = cause
|
|
2003
|
+
const refused = isRelayError(cause) && (cause.kind === "http" && cause.status !== void 0 && cause.status >= 400 && cause.status < 500 && cause.status !== 429 || cause.code === RelayErrorCode.InvalidInput);
|
|
1185
2004
|
if (refused) {
|
|
1186
2005
|
forgetReferralCode();
|
|
1187
2006
|
}
|
|
1188
2007
|
setState({
|
|
1189
2008
|
status: refused ? "rejected" : "failed",
|
|
1190
2009
|
code,
|
|
1191
|
-
|
|
2010
|
+
error: asRelayError(cause, "That invitation code couldn't be used.")
|
|
1192
2011
|
});
|
|
1193
2012
|
}
|
|
1194
2013
|
},
|
|
1195
|
-
[relay]
|
|
2014
|
+
[relay, invalidate]
|
|
1196
2015
|
);
|
|
1197
|
-
|
|
2016
|
+
useEffect7(() => {
|
|
1198
2017
|
if (member && state.status === "pending" && started.current !== `${member}:${state.code}`) {
|
|
1199
2018
|
started.current = `${member}:${state.code}`;
|
|
1200
2019
|
void accept(state.code);
|
|
1201
2020
|
}
|
|
1202
2021
|
}, [member, state, accept]);
|
|
1203
|
-
return { invitation: state, accept };
|
|
2022
|
+
return useMemo9(() => ({ invitation: state, accept }), [state, accept]);
|
|
1204
2023
|
}
|
|
1205
2024
|
function useReferralSummary(options = {}) {
|
|
1206
2025
|
const relay = useRelay();
|
|
1207
|
-
const {
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
);
|
|
1218
|
-
|
|
1219
|
-
}, [relay]);
|
|
1220
|
-
useEffect8(() => {
|
|
1221
|
-
setSummary(null);
|
|
1222
|
-
if (member) {
|
|
1223
|
-
void refresh();
|
|
2026
|
+
const { path = "/", param = "ref", intervalMs = 6e4 } = options;
|
|
2027
|
+
const query = useMemberQuery(
|
|
2028
|
+
[...relayKeys.referrals, "summary"],
|
|
2029
|
+
(request) => relay.referrals.summary(request),
|
|
2030
|
+
{ intervalMs }
|
|
2031
|
+
);
|
|
2032
|
+
return useMemo9(() => {
|
|
2033
|
+
let link = null;
|
|
2034
|
+
if (query.data && typeof window !== "undefined") {
|
|
2035
|
+
const url = new URL(path, window.location.origin);
|
|
2036
|
+
url.searchParams.set(param, query.data.code);
|
|
2037
|
+
link = url.toString();
|
|
1224
2038
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
if (summary && typeof window !== "undefined") {
|
|
1228
|
-
const url = new URL(options.path ?? "/", window.location.origin);
|
|
1229
|
-
url.searchParams.set(options.param ?? "ref", summary.code);
|
|
1230
|
-
link = url.toString();
|
|
1231
|
-
}
|
|
1232
|
-
return { summary, link, error, refresh };
|
|
2039
|
+
return { ...query, link };
|
|
2040
|
+
}, [query, path, param]);
|
|
1233
2041
|
}
|
|
1234
2042
|
|
|
1235
2043
|
// src/react/leaderboards.ts
|
|
1236
|
-
import { useCallback as useCallback7, useEffect as useEffect9, useState as useState10 } from "react";
|
|
1237
2044
|
function useLeaderboard(board, options = {}) {
|
|
1238
2045
|
const relay = useRelay();
|
|
1239
|
-
const { member } = useExperience();
|
|
1240
2046
|
const { limit = 25, intervalMs = 3e4 } = options;
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
2047
|
+
return useRelayQuery(
|
|
2048
|
+
[...relayKeys.leaderboards, board, "standings", limit],
|
|
2049
|
+
(request) => relay.leaderboards.standings(board, { limit }, request),
|
|
2050
|
+
{ intervalMs }
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// src/react/balances.ts
|
|
2055
|
+
function useBalances() {
|
|
2056
|
+
const relay = useRelay();
|
|
2057
|
+
return useMemberQuery(
|
|
2058
|
+
relayKeys.balances,
|
|
2059
|
+
(options) => relay.me.balances(options)
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
function usePointHistory(ledger) {
|
|
2063
|
+
const relay = useRelay();
|
|
2064
|
+
return useMemberPages(
|
|
2065
|
+
[...relayKeys.balances, "history", ledger],
|
|
2066
|
+
({ cursor, ...options }) => relay.me.pointHistory(cursor ? { ledger, cursor } : { ledger }, options)
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// src/react/outcomes.ts
|
|
2071
|
+
function useQuestOutcomes(query = {}) {
|
|
2072
|
+
const relay = useRelay();
|
|
2073
|
+
const { quest, state, code } = query;
|
|
2074
|
+
return useMemberPages(
|
|
2075
|
+
[...relayKeys.outcomes, quest ?? "", state ?? "", code ?? ""],
|
|
2076
|
+
({ cursor, ...options }) => relay.quests.outcomes(
|
|
2077
|
+
{
|
|
2078
|
+
...quest ? { quest } : {},
|
|
2079
|
+
...state ? { state } : {},
|
|
2080
|
+
...code ? { code } : {},
|
|
2081
|
+
...cursor ? { cursor } : {}
|
|
2082
|
+
},
|
|
2083
|
+
options
|
|
2084
|
+
)
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// src/react/offers.ts
|
|
2089
|
+
import { useCallback as useCallback7, useState as useState8 } from "react";
|
|
2090
|
+
function useOffers() {
|
|
2091
|
+
const relay = useRelay();
|
|
2092
|
+
return useRelayQuery(
|
|
2093
|
+
relayKeys.offers,
|
|
2094
|
+
(options) => relay.offers.list(options)
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
function useRedeemOffer() {
|
|
2098
|
+
const relay = useRelay();
|
|
2099
|
+
const invalidate = useInvalidate();
|
|
2100
|
+
const [busy, setBusy] = useState8(false);
|
|
2101
|
+
const [error, setError] = useState8(null);
|
|
2102
|
+
const redeem = useCallback7(
|
|
2103
|
+
async (offer) => {
|
|
2104
|
+
if (busy) {
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
setBusy(true);
|
|
2108
|
+
setError(null);
|
|
2109
|
+
try {
|
|
2110
|
+
const redemption = await relay.offers.redeem(offer);
|
|
2111
|
+
await invalidate([relayKeys.offers, relayKeys.balances]);
|
|
2112
|
+
return redemption;
|
|
2113
|
+
} catch (cause) {
|
|
2114
|
+
setError(asRelayError(cause, "This offer could not be redeemed."));
|
|
2115
|
+
return null;
|
|
2116
|
+
} finally {
|
|
2117
|
+
setBusy(false);
|
|
1258
2118
|
}
|
|
1259
|
-
},
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
return {
|
|
2119
|
+
},
|
|
2120
|
+
[busy, invalidate, relay]
|
|
2121
|
+
);
|
|
2122
|
+
return { redeem, busy, error };
|
|
1263
2123
|
}
|
|
1264
2124
|
|
|
1265
2125
|
// src/react/references.ts
|
|
1266
|
-
import { useCallback as useCallback8, useEffect as useEffect10, useState as useState11 } from "react";
|
|
1267
|
-
function useReferences(read, failure) {
|
|
1268
|
-
const relay = useRelay();
|
|
1269
|
-
const [state, setState] = useState11({ status: "loading" });
|
|
1270
|
-
const refresh = useCallback8(async () => {
|
|
1271
|
-
try {
|
|
1272
|
-
setState({ status: "ready", items: await read(relay) });
|
|
1273
|
-
} catch (cause) {
|
|
1274
|
-
setState({
|
|
1275
|
-
status: "error",
|
|
1276
|
-
message: cause instanceof Error && cause.message ? cause.message : failure
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
}, [relay, read, failure]);
|
|
1280
|
-
useEffect10(() => {
|
|
1281
|
-
void refresh();
|
|
1282
|
-
}, [refresh]);
|
|
1283
|
-
return { ...state, refresh };
|
|
1284
|
-
}
|
|
1285
|
-
var readPointLedgers = (relay) => relay.pointLedgers.list();
|
|
1286
|
-
var readTieredRewards = (relay) => relay.tieredRewards.list();
|
|
1287
2126
|
function usePointLedgers() {
|
|
1288
|
-
|
|
2127
|
+
const relay = useRelay();
|
|
2128
|
+
return useRelayQuery(
|
|
2129
|
+
relayKeys.pointLedgers,
|
|
2130
|
+
(options) => relay.pointLedgers.list(options)
|
|
2131
|
+
);
|
|
1289
2132
|
}
|
|
1290
2133
|
function useTieredRewards() {
|
|
1291
|
-
|
|
2134
|
+
const relay = useRelay();
|
|
2135
|
+
return useRelayQuery(
|
|
2136
|
+
relayKeys.tieredRewards,
|
|
2137
|
+
(options) => relay.tieredRewards.list(options)
|
|
2138
|
+
);
|
|
1292
2139
|
}
|
|
1293
2140
|
export {
|
|
1294
|
-
PENDING_POLL_MS,
|
|
1295
|
-
PROFILE_CHANGED,
|
|
1296
|
-
RETRY_DELAYS_MS,
|
|
1297
2141
|
RelayProvider,
|
|
1298
|
-
avatarSrc,
|
|
1299
2142
|
canRepeat,
|
|
1300
2143
|
canSubmit,
|
|
1301
2144
|
captureReferralCode,
|
|
1302
|
-
|
|
1303
|
-
completionMark,
|
|
1304
|
-
defaultAvatar,
|
|
1305
|
-
hasPending,
|
|
2145
|
+
completionCountText,
|
|
1306
2146
|
isAutomatic,
|
|
1307
2147
|
isConnected,
|
|
1308
2148
|
isFinished,
|
|
1309
2149
|
progressOf,
|
|
1310
2150
|
questGroups,
|
|
1311
2151
|
questStatus,
|
|
2152
|
+
questStatusText,
|
|
2153
|
+
relayKeys,
|
|
1312
2154
|
roleDeliveryLabel,
|
|
1313
|
-
|
|
1314
|
-
sanitizeDeliveries,
|
|
2155
|
+
roleDeliveryStatus,
|
|
1315
2156
|
useAccounts,
|
|
2157
|
+
useActivity,
|
|
2158
|
+
useBalances,
|
|
2159
|
+
useDeliveries,
|
|
2160
|
+
useEntitlements,
|
|
1316
2161
|
useExperience,
|
|
2162
|
+
useInvalidate,
|
|
1317
2163
|
useInvitation,
|
|
1318
2164
|
useLeaderboard,
|
|
1319
2165
|
useLeaderboardPlacement,
|
|
1320
2166
|
useMe,
|
|
2167
|
+
useMemberPages,
|
|
2168
|
+
useMemberQuery,
|
|
1321
2169
|
useMyProfile,
|
|
2170
|
+
useOffers,
|
|
2171
|
+
usePointHistory,
|
|
1322
2172
|
usePointLedgers,
|
|
1323
2173
|
useProfile,
|
|
1324
2174
|
useProfileCompletions,
|
|
1325
2175
|
useProfileReferral,
|
|
1326
2176
|
useQuest,
|
|
2177
|
+
useQuestOutcomes,
|
|
2178
|
+
useRedeemOffer,
|
|
1327
2179
|
useReferralSummary,
|
|
1328
2180
|
useRelay,
|
|
2181
|
+
useRelayPages,
|
|
2182
|
+
useRelayQuery,
|
|
2183
|
+
useSession,
|
|
1329
2184
|
useSignIn,
|
|
1330
2185
|
useSignInMethods,
|
|
1331
2186
|
useSignInOptions,
|
|
@@ -1333,8 +2188,5 @@ export {
|
|
|
1333
2188
|
useSignOut,
|
|
1334
2189
|
useTieredRewards,
|
|
1335
2190
|
useWalletDiscovery,
|
|
1336
|
-
useWallets
|
|
1337
|
-
visibleQuests,
|
|
1338
|
-
walletAvatarSeed,
|
|
1339
|
-
watchRoleDeliveries
|
|
2191
|
+
useWallets
|
|
1340
2192
|
};
|