@nano133/nanoterra 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +62 -12
- package/ai/SPEC.md +7 -1
- package/dist/admin/amount.d.ts +4 -0
- package/dist/admin/amount.js +4 -0
- package/dist/admin/panel/dom.d.ts +1 -1
- package/dist/admin/panel/dom.js +3 -2
- package/dist/admin/panel/shelf.js +1 -1
- package/dist/admin/panel/shelfData.d.ts +1 -1
- package/dist/admin/panel/shelfData.js +6 -6
- package/dist/admin/summary.d.ts +1 -1
- package/dist/admin/summary.js +8 -8
- package/dist/looks/index.d.ts +1 -1
- package/dist/looks/index.js +1 -1
- package/dist/looks/ladder.d.ts +1 -1
- package/dist/looks/ladder.js +7 -7
- package/dist/looks/look.d.ts +7 -2
- package/dist/looks/look.js +10 -3
- package/dist/player-signin/panel/index.d.ts +2 -2
- package/dist/player-signin/panel/index.js +3 -2
- package/dist/player-signin/panel/mount.d.ts +1 -1
- package/dist/player-signin/panel/mount.js +5 -3
- package/dist/player-signin/panel/view.d.ts +3 -0
- package/dist/player-signin/panel/view.js +49 -46
- package/dist/player-signin/panel/words.d.ts +96 -1
- package/dist/player-signin/panel/words.js +109 -24
- package/dist/shop/handler.js +2 -2
- package/dist/shop/panel/index.d.ts +2 -0
- package/dist/shop/panel/index.js +3 -0
- package/dist/shop/panel/looks.d.ts +3 -0
- package/dist/shop/panel/looks.js +11 -11
- package/dist/shop/panel/mount.d.ts +18 -2
- package/dist/shop/panel/mount.js +48 -21
- package/dist/shop/panel/pay.d.ts +3 -0
- package/dist/shop/panel/pay.js +38 -25
- package/dist/shop/panel/words.d.ts +89 -0
- package/dist/shop/panel/words.js +79 -0
- package/package.json +1 -1
- package/src/admin/amount.ts +6 -0
- package/src/admin/panel/dom.ts +4 -2
- package/src/admin/panel/shelf.ts +1 -1
- package/src/admin/panel/shelfData.ts +6 -6
- package/src/admin/summary.ts +8 -8
- package/src/looks/index.ts +1 -1
- package/src/looks/ladder.ts +7 -6
- package/src/looks/look.ts +12 -3
- package/src/player-signin/panel/index.ts +4 -3
- package/src/player-signin/panel/mount.ts +6 -4
- package/src/player-signin/panel/style.css +22 -17
- package/src/player-signin/panel/view.ts +60 -55
- package/src/player-signin/panel/words.ts +202 -24
- package/src/shop/handler.ts +2 -2
- package/src/shop/panel/index.ts +4 -0
- package/src/shop/panel/looks.ts +14 -14
- package/src/shop/panel/mount.ts +60 -21
- package/src/shop/panel/pay.ts +51 -30
- package/src/shop/panel/style.css +24 -20
- package/src/shop/panel/words.ts +172 -0
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
//
|
|
12
12
|
// The seed lives in local variables of one sign-in only: never in storage, a log, an error text or an address. The page remembers only the passkeys'
|
|
13
13
|
// ids and the last way (./memo.ts).
|
|
14
|
+
//
|
|
15
|
+
// `words` changes any word of the sheet (./words.ts: PLAYER_SIGNIN_WORDS are the defaults), for a game's own words or language.
|
|
14
16
|
import { firstUse, makePasskey, openPasskey, PasskeyError, passkeyFault, passkeySupport } from "@nano133/signin/passkey";
|
|
15
17
|
import { ApiError, createApi } from "../../admin/panel/api.js";
|
|
16
18
|
import { h, shortAddress } from "../../admin/panel/dom.js";
|
|
@@ -19,7 +21,7 @@ import { checkScheme } from "../scheme-check.js";
|
|
|
19
21
|
import { previewHost } from "../host.js";
|
|
20
22
|
import { WAYS } from "../types.js";
|
|
21
23
|
import { readMemo, rememberLast, rememberMade, rememberOpened } from "./memo.js";
|
|
22
|
-
import { faultWords } from "./words.js";
|
|
24
|
+
import { faultWords, playerSigninWords } from "./words.js";
|
|
23
25
|
/** The wallets, with letters, not logos: no wallet's licence clearly allows its logo. */
|
|
24
26
|
const WALLETS = {
|
|
25
27
|
berry: { name: "Berry Wallet", mark: "B" },
|
|
@@ -59,6 +61,7 @@ export function signInView(options) {
|
|
|
59
61
|
const passkeyOn = options.passkey ?? !previewHost(rpId);
|
|
60
62
|
const scheme = passkeyOn ? checkScheme(options.scheme) : null;
|
|
61
63
|
const touch = () => options.touch ?? (typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches);
|
|
64
|
+
const w = playerSigninWords(options.words);
|
|
62
65
|
let stopped = false;
|
|
63
66
|
let timer;
|
|
64
67
|
let prompt = null;
|
|
@@ -77,7 +80,7 @@ export function signInView(options) {
|
|
|
77
80
|
node.toggleAttribute("aria-busy", on);
|
|
78
81
|
};
|
|
79
82
|
const key = (text, onclick, kind = "plain") => h("button", { class: `n133s-btn n133s-${kind}`, type: "button", onclick }, text);
|
|
80
|
-
const backKey = () => h("button", { class: "n133s-back", type: "button", onclick: () => showWays() }, icon("back", 18), h("span", {},
|
|
83
|
+
const backKey = () => h("button", { class: "n133s-back", type: "button", onclick: () => showWays() }, icon("back", 18), h("span", {}, w.back));
|
|
81
84
|
const title = (text) => h("h3", { class: "n133s-step", tabindex: "-1" }, text);
|
|
82
85
|
const focusFirst = () => (area.querySelector("h3, button:not([disabled])") ?? area).focus?.({ preventScroll: true });
|
|
83
86
|
const setup = () => ({ rpId, rpName: options.name, scheme: scheme, userName: passkeyName(options.name), exclude: readMemo(rpId).ids });
|
|
@@ -94,7 +97,7 @@ export function signInView(options) {
|
|
|
94
97
|
function done(player) {
|
|
95
98
|
stopWaiting();
|
|
96
99
|
current = null;
|
|
97
|
-
area.replaceChildren(h("p", { class: "n133s-done" }, icon("check", 22), h("span", {},
|
|
100
|
+
area.replaceChildren(h("p", { class: "n133s-done" }, icon("check", 22), h("span", {}, w.signedInAs(shortAddress(player.address)))));
|
|
98
101
|
say("");
|
|
99
102
|
options.onSignedIn?.(player);
|
|
100
103
|
}
|
|
@@ -102,29 +105,29 @@ export function signInView(options) {
|
|
|
102
105
|
function passkeyTile() {
|
|
103
106
|
const off = support === "no";
|
|
104
107
|
const last = readMemo(rpId).last === "passkey";
|
|
105
|
-
return h("button", { class: `n133s-pk${off ? " is-off" : ""}`, type: "button", onclick: () => showPasskey() }, h("span", { class: "n133s-pk-mark" }, keyIcon()), h("span", { class: "n133s-pk-words" }, h("strong", {},
|
|
108
|
+
return h("button", { class: `n133s-pk${off ? " is-off" : ""}`, type: "button", onclick: () => showPasskey() }, h("span", { class: "n133s-pk-mark" }, keyIcon()), h("span", { class: "n133s-pk-words" }, h("strong", {}, w.usePasskey), h("small", {}, off ? w.passkeyOff : w.passkeyHint)), last ? h("span", { class: "n133s-last" }, w.lastUsed) : null);
|
|
106
109
|
}
|
|
107
110
|
function walletTiles() {
|
|
108
111
|
const last = readMemo(rpId).last;
|
|
109
|
-
return h("div", { class: "n133s-tiles" }, ...ways.map((
|
|
112
|
+
return h("div", { class: "n133s-tiles" }, ...ways.map((way) => h("button", { class: "n133s-tile", type: "button", onclick: () => choose(way) }, h("span", { class: "n133s-mark", "aria-hidden": "true" }, WALLETS[way].mark), h("span", { class: "n133s-name" }, WALLETS[way].name), last === way ? h("span", { class: "n133s-last" }, w.lastUsed) : null)));
|
|
110
113
|
}
|
|
111
114
|
function showWays() {
|
|
112
115
|
stopWaiting();
|
|
113
116
|
busy(false);
|
|
114
117
|
const pk = passkeyOn ? passkeyTile() : null;
|
|
115
118
|
const first = pk && support !== "no";
|
|
116
|
-
const walletsPart = ways.length ? [h("p", { class: "n133s-or" }, pk ?
|
|
117
|
-
fill(h("p", { class: "n133s-lead" },
|
|
119
|
+
const walletsPart = ways.length ? [h("p", { class: "n133s-or" }, pk ? w.orWallet : w.chooseWallet), walletTiles()] : [];
|
|
120
|
+
fill(h("p", { class: "n133s-lead" }, w.lead), first ? pk : null, ...walletsPart, !first && pk ? h("p", { class: "n133s-or" }, w.orPasskey) : null, !first ? pk : null);
|
|
118
121
|
say("");
|
|
119
122
|
}
|
|
120
123
|
// ---- the passkey ----
|
|
121
124
|
function showFault(e) {
|
|
122
125
|
busy(false);
|
|
123
126
|
const f = passkeyFault(e);
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
return showPasskey(
|
|
127
|
-
fill(backKey(), title(
|
|
127
|
+
const fw = faultWords(f.code, rpId, w);
|
|
128
|
+
if (fw.next === "stay")
|
|
129
|
+
return showPasskey(fw.text);
|
|
130
|
+
fill(backKey(), title(fw.title), h("p", { class: "n133s-text n133s-fault", "data-fault": f.code }, fw.text), ways.length ? h("p", { class: "n133s-or" }, w.faultWallets) : null, ways.length ? walletTiles() : null);
|
|
128
131
|
say("");
|
|
129
132
|
focusFirst();
|
|
130
133
|
}
|
|
@@ -140,15 +143,15 @@ export function signInView(options) {
|
|
|
140
143
|
if (!hostFits(location.hostname, rpId))
|
|
141
144
|
return showFault({ name: "SecurityError" });
|
|
142
145
|
const known = readMemo(rpId).ids.length > 0;
|
|
143
|
-
const use = key(
|
|
144
|
-
const make = key(
|
|
145
|
-
area.replaceChildren(backKey(), title(
|
|
146
|
+
const use = key(w.useMine, () => void usePasskey(), known ? "primary" : "plain");
|
|
147
|
+
const make = key(w.makeOne, () => void makeNew(), known ? "plain" : "primary");
|
|
148
|
+
area.replaceChildren(backKey(), title(w.passkeyTitle), h("p", { class: "n133s-text" }, w.passkeyText), h("div", { class: "n133s-keys" }, known ? use : make, known ? make : use));
|
|
146
149
|
say(note ?? "");
|
|
147
150
|
focusFirst();
|
|
148
151
|
}
|
|
149
152
|
async function usePasskey() {
|
|
150
153
|
busy(true);
|
|
151
|
-
say(
|
|
154
|
+
say(w.waitingPasskey);
|
|
152
155
|
prompt = new AbortController();
|
|
153
156
|
try {
|
|
154
157
|
const p = await openPasskey(setup(), prompt.signal);
|
|
@@ -163,7 +166,7 @@ export function signInView(options) {
|
|
|
163
166
|
}
|
|
164
167
|
async function makeNew() {
|
|
165
168
|
busy(true);
|
|
166
|
-
say(
|
|
169
|
+
say(w.makingPasskey);
|
|
167
170
|
prompt = new AbortController();
|
|
168
171
|
let made;
|
|
169
172
|
try {
|
|
@@ -183,7 +186,7 @@ export function signInView(options) {
|
|
|
183
186
|
/** The first use of a passkey just made. Where it gives nothing (a browser that allows one prompt per press, a closed sheet), one key finishes it. */
|
|
184
187
|
async function firstOpen(made, pick) {
|
|
185
188
|
busy(true);
|
|
186
|
-
say(
|
|
189
|
+
say(w.openingPasskey);
|
|
187
190
|
prompt = new AbortController();
|
|
188
191
|
try {
|
|
189
192
|
const p = await firstUse(setup(), made, prompt.signal, pick);
|
|
@@ -198,7 +201,7 @@ export function signInView(options) {
|
|
|
198
201
|
if (f.code !== "cancelled" && f.code !== "failed")
|
|
199
202
|
return showFault(f);
|
|
200
203
|
busy(false);
|
|
201
|
-
area.replaceChildren(title(
|
|
204
|
+
area.replaceChildren(title(w.madeTitle), h("p", { class: "n133s-text" }, w.madeText), h("div", { class: "n133s-keys" }, key(w.openMine, () => void firstOpen(made, true), "primary")));
|
|
202
205
|
say("");
|
|
203
206
|
focusFirst();
|
|
204
207
|
}
|
|
@@ -210,25 +213,25 @@ export function signInView(options) {
|
|
|
210
213
|
const code = h("p", { class: "n133s-code", translate: "no" });
|
|
211
214
|
const codeBox = h("div", { class: "n133s-codebox" }, code);
|
|
212
215
|
codeBox.hidden = true;
|
|
213
|
-
const go = key(
|
|
214
|
-
const copyBtn = key(
|
|
216
|
+
const go = key(w.later, () => resolve(), "quiet");
|
|
217
|
+
const copyBtn = key(w.copyCode, async () => {
|
|
215
218
|
try {
|
|
216
219
|
await navigator.clipboard.writeText(seed);
|
|
217
|
-
copyBtn.textContent =
|
|
220
|
+
copyBtn.textContent = w.copied;
|
|
218
221
|
}
|
|
219
222
|
catch {
|
|
220
|
-
copyBtn.textContent =
|
|
223
|
+
copyBtn.textContent = w.copyFailed;
|
|
221
224
|
}
|
|
222
225
|
});
|
|
223
226
|
codeBox.append(copyBtn);
|
|
224
|
-
const show = key(
|
|
227
|
+
const show = key(w.showCode, () => {
|
|
225
228
|
code.textContent = seed.match(/.{1,16}/g).join("\n");
|
|
226
229
|
codeBox.hidden = false;
|
|
227
230
|
show.remove();
|
|
228
|
-
go.textContent =
|
|
231
|
+
go.textContent = w.continue;
|
|
229
232
|
go.className = "n133s-btn n133s-primary";
|
|
230
233
|
}, "primary");
|
|
231
|
-
area.replaceChildren(title(
|
|
234
|
+
area.replaceChildren(title(w.spareTitle), h("p", { class: "n133s-text" }, w.spareText), codeBox, h("p", { class: "n133s-fine" }, w.spareFine), h("div", { class: "n133s-keys" }, show, go));
|
|
232
235
|
say("");
|
|
233
236
|
focusFirst();
|
|
234
237
|
});
|
|
@@ -240,7 +243,7 @@ export function signInView(options) {
|
|
|
240
243
|
if (stopped)
|
|
241
244
|
return;
|
|
242
245
|
busy(true);
|
|
243
|
-
say(
|
|
246
|
+
say(w.signingIn);
|
|
244
247
|
try {
|
|
245
248
|
const { signinBody } = await import("@nano133/signin/purse");
|
|
246
249
|
const turnstile = await personToken();
|
|
@@ -251,7 +254,7 @@ export function signInView(options) {
|
|
|
251
254
|
}
|
|
252
255
|
catch (e) {
|
|
253
256
|
if (!stopped)
|
|
254
|
-
showPasskey(e instanceof ApiError ? e.message
|
|
257
|
+
showPasskey(e instanceof ApiError ? w.refusal(e.message, e.status) : w.signInFailed);
|
|
255
258
|
}
|
|
256
259
|
}
|
|
257
260
|
// ---- a wallet app ----
|
|
@@ -261,7 +264,7 @@ export function signInView(options) {
|
|
|
261
264
|
stopWaiting();
|
|
262
265
|
current = null;
|
|
263
266
|
busy(true);
|
|
264
|
-
say(
|
|
267
|
+
say(w.starting);
|
|
265
268
|
try {
|
|
266
269
|
const turnstile = await personToken();
|
|
267
270
|
const s = await api.post(`${base}/start`, { way, ...(turnstile ? { turnstile } : {}) });
|
|
@@ -275,13 +278,13 @@ export function signInView(options) {
|
|
|
275
278
|
catch (e) {
|
|
276
279
|
busy(false);
|
|
277
280
|
showWays();
|
|
278
|
-
say(e instanceof
|
|
281
|
+
say(e instanceof ApiError ? w.refusal(e.message, e.status) : e instanceof Error && e.message ? w.refusal(e.message, 0) : w.startFailed);
|
|
279
282
|
}
|
|
280
283
|
}
|
|
281
284
|
function showWallet(s) {
|
|
282
285
|
const name = WALLETS[s.way].name;
|
|
283
286
|
area.replaceChildren(backKey(), title(name), ...walletSteps(s, name));
|
|
284
|
-
say(
|
|
287
|
+
say(w.waitingFor(name));
|
|
285
288
|
focusFirst();
|
|
286
289
|
}
|
|
287
290
|
/** What the player does with the wallet: on a touch screen, one key; with a mouse, a QR code for the phone. */
|
|
@@ -289,36 +292,36 @@ export function signInView(options) {
|
|
|
289
292
|
const open = (text, href, newTab = false) => h("a", { class: "n133s-btn n133s-primary", href, target: newTab ? "_blank" : undefined, rel: newTab ? "noopener" : undefined }, text);
|
|
290
293
|
const qr = (text) => {
|
|
291
294
|
if (!options.qr)
|
|
292
|
-
return h("p", { class: "n133s-fine" },
|
|
295
|
+
return h("p", { class: "n133s-fine" }, w.openOnPhone(name));
|
|
293
296
|
const c = options.qr(text);
|
|
294
|
-
c.setAttribute("aria-label",
|
|
297
|
+
c.setAttribute("aria-label", w.qrLabel(name));
|
|
295
298
|
return h("div", { class: "n133s-qr" }, c);
|
|
296
299
|
};
|
|
297
|
-
const only = h("p", { class: "n133s-fine" },
|
|
298
|
-
const comeBack = h("p", { class: "n133s-fine" },
|
|
300
|
+
const only = h("p", { class: "n133s-fine" }, w.onlyYours(location.host));
|
|
301
|
+
const comeBack = h("p", { class: "n133s-fine" }, w.comeBack(s.way === "berry" ? w.yourWallet : name));
|
|
299
302
|
if (s.way === "nautilus") {
|
|
300
|
-
const code = h("p", { class: "n133s-text" },
|
|
301
|
-
return touch() ? [code, open(
|
|
303
|
+
const code = h("p", { class: "n133s-text" }, w.checkCode, h("strong", { class: "n133s-signcode" }, s.code ?? ""));
|
|
304
|
+
return touch() ? [code, open(w.openApp(name), s.link), comeBack] : [qr(s.link), h("p", { class: "n133s-text" }, w.scanWith(name)), code, only];
|
|
302
305
|
}
|
|
303
306
|
if (s.way === "nanchat") {
|
|
304
|
-
const here = h("p", { class: "n133s-fine" },
|
|
305
|
-
return touch() ? [open(
|
|
307
|
+
const here = h("p", { class: "n133s-fine" }, w.onComputer, h("a", { href: s.link, target: "_blank", rel: "noopener" }, w.openLink(name)));
|
|
308
|
+
return touch() ? [open(w.openApp(name), s.link, true), comeBack, only] : [qr(s.link), h("p", { class: "n133s-text" }, w.scanCamera(name)), only, here];
|
|
306
309
|
}
|
|
307
|
-
const other = h("a", { class: "n133s-btn n133s-plain", href: s.walletLink ?? s.link },
|
|
308
|
-
const here = h("p", { class: "n133s-fine" },
|
|
309
|
-
return touch() ? [open(
|
|
310
|
+
const other = h("a", { class: "n133s-btn n133s-plain", href: s.walletLink ?? s.link }, w.anotherWallet);
|
|
311
|
+
const here = h("p", { class: "n133s-fine" }, w.onComputer, h("a", { href: s.walletLink ?? s.link }, w.openLink(w.aWallet)));
|
|
312
|
+
return touch() ? [open(w.openApp(name), s.link), other, comeBack] : [qr(s.link), h("p", { class: "n133s-text" }, w.scanWith(name)), only, here];
|
|
310
313
|
}
|
|
311
314
|
/** The request ended without a sign-in: say why, and offer a new one. */
|
|
312
315
|
function again(s, words) {
|
|
313
316
|
current = null;
|
|
314
|
-
area.replaceChildren(backKey(), title(WALLETS[s.way].name), h("div", { class: "n133s-keys" }, key(
|
|
317
|
+
area.replaceChildren(backKey(), title(WALLETS[s.way].name), h("div", { class: "n133s-keys" }, key(w.tryAgain, () => void choose(s.way), "primary"), key(w.otherWay, () => showWays())));
|
|
315
318
|
say(words);
|
|
316
319
|
}
|
|
317
320
|
async function poll(s) {
|
|
318
321
|
if (stopped || current !== s)
|
|
319
322
|
return;
|
|
320
323
|
if (Date.now() > s.expiresAt + 5000)
|
|
321
|
-
return again(s,
|
|
324
|
+
return again(s, w.expired);
|
|
322
325
|
try {
|
|
323
326
|
const r = await api.post(`${base}/check`, { id: s.id, way: s.way });
|
|
324
327
|
if (stopped || current !== s)
|
|
@@ -326,13 +329,13 @@ export function signInView(options) {
|
|
|
326
329
|
if (r.status === "signedin" && r.address)
|
|
327
330
|
return done({ address: r.address, via: "wallet" });
|
|
328
331
|
if (r.status === "elsewhere" || r.status === "used")
|
|
329
|
-
return again(s,
|
|
332
|
+
return again(s, w.usedElsewhere);
|
|
330
333
|
if (r.status === "expired")
|
|
331
|
-
return again(s,
|
|
334
|
+
return again(s, w.expired);
|
|
332
335
|
}
|
|
333
336
|
catch (e) {
|
|
334
337
|
if (e instanceof ApiError && e.status === 429)
|
|
335
|
-
say(
|
|
338
|
+
say(w.tooManyChecks);
|
|
336
339
|
}
|
|
337
340
|
timer = setTimeout(() => void poll(s), 2000);
|
|
338
341
|
}
|
|
@@ -5,4 +5,99 @@ export type FaultWords = {
|
|
|
5
5
|
text: string;
|
|
6
6
|
next: "stay" | "wallets";
|
|
7
7
|
};
|
|
8
|
-
|
|
8
|
+
/** A fault's words: its title, and its text (a function when it names the game's domain, `rpId`). */
|
|
9
|
+
export type FaultText = {
|
|
10
|
+
title: string;
|
|
11
|
+
text: string | ((rpId: string) => string);
|
|
12
|
+
};
|
|
13
|
+
export type PlayerSigninWords = {
|
|
14
|
+
/** The sheet's title. The option `title` wins over it. */
|
|
15
|
+
title: string;
|
|
16
|
+
/** The close key's name for a screen reader. */
|
|
17
|
+
close: string;
|
|
18
|
+
/** The first line of the ways. */
|
|
19
|
+
lead: string;
|
|
20
|
+
usePasskey: string;
|
|
21
|
+
/** Under "Use a passkey". */
|
|
22
|
+
passkeyHint: string;
|
|
23
|
+
/** Under "Use a passkey" where the browser reports no passkey PRF. */
|
|
24
|
+
passkeyOff: string;
|
|
25
|
+
lastUsed: string;
|
|
26
|
+
/** Before the wallet tiles, after the passkey. */
|
|
27
|
+
orWallet: string;
|
|
28
|
+
/** Before the wallet tiles, with no passkey before them. */
|
|
29
|
+
chooseWallet: string;
|
|
30
|
+
/** Before the passkey, when it comes last. */
|
|
31
|
+
orPasskey: string;
|
|
32
|
+
back: string;
|
|
33
|
+
passkeyTitle: string;
|
|
34
|
+
passkeyText: string;
|
|
35
|
+
useMine: string;
|
|
36
|
+
makeOne: string;
|
|
37
|
+
waitingPasskey: string;
|
|
38
|
+
makingPasskey: string;
|
|
39
|
+
openingPasskey: string;
|
|
40
|
+
/** A passkey just made, whose first use gave nothing: one key opens it. */
|
|
41
|
+
madeTitle: string;
|
|
42
|
+
madeText: string;
|
|
43
|
+
openMine: string;
|
|
44
|
+
spareTitle: string;
|
|
45
|
+
spareText: string;
|
|
46
|
+
/** Under the code: keep it secret; how to use it. */
|
|
47
|
+
spareFine: string;
|
|
48
|
+
showCode: string;
|
|
49
|
+
copyCode: string;
|
|
50
|
+
copied: string;
|
|
51
|
+
copyFailed: string;
|
|
52
|
+
later: string;
|
|
53
|
+
continue: string;
|
|
54
|
+
signingIn: string;
|
|
55
|
+
signInFailed: string;
|
|
56
|
+
/** After a fault that offers the wallet apps. */
|
|
57
|
+
faultWallets: string;
|
|
58
|
+
faults: Record<PasskeyFault, FaultText>;
|
|
59
|
+
starting: string;
|
|
60
|
+
startFailed: string;
|
|
61
|
+
/** The words of a refusal: the server's own (`text`, in English) and its HTTP status (0: no answer). Default: the server's words. */
|
|
62
|
+
refusal: (text: string, status: number) => string;
|
|
63
|
+
/** The status while the page waits for a wallet ("Berry Wallet"). */
|
|
64
|
+
waitingFor: (wallet: string) => string;
|
|
65
|
+
/** After a sign-in, with the player's short address. */
|
|
66
|
+
signedInAs: (address: string) => string;
|
|
67
|
+
/** The key that opens a wallet app on a touch screen. */
|
|
68
|
+
openApp: (wallet: string) => string;
|
|
69
|
+
anotherWallet: string;
|
|
70
|
+
/** Before a link that opens the wallet on this computer. */
|
|
71
|
+
onComputer: string;
|
|
72
|
+
/** The link's words: open a wallet ("NanChat", or `aWallet`). */
|
|
73
|
+
openLink: (wallet: string) => string;
|
|
74
|
+
aWallet: string;
|
|
75
|
+
/** With no QR hook, on a screen with a mouse. */
|
|
76
|
+
openOnPhone: (wallet: string) => string;
|
|
77
|
+
qrLabel: (wallet: string) => string;
|
|
78
|
+
/** Only approve a sign-in the player started, on this host. */
|
|
79
|
+
onlyYours: (host: string) => string;
|
|
80
|
+
/** After the key that opens the app (`yourWallet` for Berry Wallet, which opens any wallet). */
|
|
81
|
+
comeBack: (wallet: string) => string;
|
|
82
|
+
yourWallet: string;
|
|
83
|
+
/** Under a QR code: scan it with this wallet. */
|
|
84
|
+
scanWith: (wallet: string) => string;
|
|
85
|
+
/** Under NanChat's QR code: the phone's camera opens it. */
|
|
86
|
+
scanCamera: (wallet: string) => string;
|
|
87
|
+
/** Before Nautilus's code (the code follows, bold). */
|
|
88
|
+
checkCode: string;
|
|
89
|
+
tryAgain: string;
|
|
90
|
+
otherWay: string;
|
|
91
|
+
expired: string;
|
|
92
|
+
usedElsewhere: string;
|
|
93
|
+
tooManyChecks: string;
|
|
94
|
+
};
|
|
95
|
+
/** `words` as a game gives it: any of the keys, and any of the faults. */
|
|
96
|
+
export type PlayerSigninWordsOption = Partial<Omit<PlayerSigninWords, "faults">> & {
|
|
97
|
+
faults?: Partial<Record<PasskeyFault, FaultText>>;
|
|
98
|
+
};
|
|
99
|
+
export declare const PLAYER_SIGNIN_WORDS: PlayerSigninWords;
|
|
100
|
+
/** The game's words over the defaults; its faults over the default faults. A key it leaves out, or gives as undefined, keeps its default. */
|
|
101
|
+
export declare function playerSigninWords(over?: PlayerSigninWordsOption): PlayerSigninWords;
|
|
102
|
+
/** A fault's title, text and next step, in the game's words (default: PLAYER_SIGNIN_WORDS). An unknown code gets the words of "failed". */
|
|
103
|
+
export declare function faultWords(code: PasskeyFault, rpId: string, words?: PlayerSigninWordsOption): FaultWords;
|
|
@@ -1,26 +1,111 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
// Every word of the sign-in sheet, and its words for each passkey fault, in plain English. A game gives `words` to `mountPlayerSignin` (or to
|
|
2
|
+
// `signInView`) with the keys it wants to change, in its own words or language; the rest stay these. A text with a value in it is a function of that
|
|
3
|
+
// value. The wallets' names (Berry Wallet, NanChat, Nautilus) are names, not words: they stay.
|
|
4
|
+
//
|
|
5
|
+
// The fault codes are @nano133/signin's (PasskeyFault). A fault never names a seed or a key, and never tells the player to delete a passkey. No
|
|
6
|
+
// sentence has more than 10 words.
|
|
7
|
+
export const PLAYER_SIGNIN_WORDS = {
|
|
8
|
+
title: "Sign in",
|
|
9
|
+
close: "Close",
|
|
10
|
+
lead: "No password and no payment.",
|
|
11
|
+
usePasskey: "Use a passkey",
|
|
12
|
+
passkeyHint: "Face ID, Touch ID or your device PIN",
|
|
13
|
+
passkeyOff: "Not on this browser",
|
|
14
|
+
lastUsed: "Last used",
|
|
15
|
+
orWallet: "Or a wallet app",
|
|
16
|
+
chooseWallet: "Choose your wallet",
|
|
17
|
+
orPasskey: "Or a passkey",
|
|
18
|
+
back: "Back",
|
|
19
|
+
passkeyTitle: "Passkey",
|
|
20
|
+
passkeyText: "Face ID, Touch ID or your device PIN. The passkey stays on your device. The game never sees it.",
|
|
21
|
+
useMine: "Use my passkey",
|
|
22
|
+
makeOne: "Make a passkey",
|
|
23
|
+
waitingPasskey: "Waiting for your passkey…",
|
|
24
|
+
makingPasskey: "Making your passkey…",
|
|
25
|
+
openingPasskey: "Opening your passkey…",
|
|
26
|
+
madeTitle: "Your passkey is made",
|
|
27
|
+
madeText: "Press the key to open it.",
|
|
28
|
+
openMine: "Open my passkey",
|
|
29
|
+
spareTitle: "Keep a spare key",
|
|
30
|
+
spareText: "Your looks belong to this passkey. If you lose it, this code brings them back.",
|
|
31
|
+
spareFine: "Keep it secret. Anyone with it can sign in as you. Import it as a seed in Nautilus or another wallet. Then sign in with that wallet.",
|
|
32
|
+
showCode: "Show my code",
|
|
33
|
+
copyCode: "Copy the code",
|
|
34
|
+
copied: "Copied",
|
|
35
|
+
copyFailed: "Copy failed: write it down",
|
|
36
|
+
later: "Later",
|
|
37
|
+
continue: "Continue",
|
|
38
|
+
signingIn: "Signing in…",
|
|
39
|
+
signInFailed: "Sign-in did not work. Try again.",
|
|
40
|
+
faultWallets: "Sign in with a wallet app",
|
|
41
|
+
faults: {
|
|
42
|
+
cancelled: { title: "No passkey was used", text: "No passkey was used." },
|
|
43
|
+
"no-prf": { title: "Not on this device yet", text: "This device's passkeys cannot sign in to games yet. Use a wallet app, or a passkey on another device." },
|
|
44
|
+
unsupported: { title: "No passkeys here", text: "This browser has no passkeys. Use a wallet app." },
|
|
45
|
+
"other-form": { title: "Not this password manager", text: "This game cannot read your password manager's answer. Choose iCloud Keychain or Google Password Manager instead. Or use a wallet app." },
|
|
46
|
+
"not-a-purse": { title: "Not this game's passkey", text: "That passkey is not one of this game's. Choose another one, or make one." },
|
|
47
|
+
"wrong-host": { title: "Wrong address", text: (rpId) => `Passkeys for this game work only on ${rpId}.` },
|
|
48
|
+
exists: { title: "You have a passkey", text: "This device has your passkey already. Use it." },
|
|
49
|
+
unstable: { title: "Not on this device", text: "This device gave two answers for one passkey. It cannot sign in here. Use a wallet app." },
|
|
50
|
+
"other-passkey": { title: "Another passkey", text: "That is another passkey. Choose the one you just made. Its name has today's date." },
|
|
51
|
+
failed: { title: "That did not work", text: "The passkey did not work. Try again." },
|
|
52
|
+
},
|
|
53
|
+
starting: "Starting…",
|
|
54
|
+
startFailed: "Sign-in did not start. Try again.",
|
|
55
|
+
refusal: (text) => text,
|
|
56
|
+
waitingFor: (wallet) => `Waiting for ${wallet}…`,
|
|
57
|
+
signedInAs: (address) => `Signed in as ${address}`,
|
|
58
|
+
openApp: (wallet) => `Open ${wallet}`,
|
|
59
|
+
anotherWallet: "Another wallet",
|
|
60
|
+
onComputer: "On this computer: ",
|
|
61
|
+
openLink: (wallet) => `open ${wallet}`,
|
|
62
|
+
aWallet: "a wallet",
|
|
63
|
+
openOnPhone: (wallet) => `Open this page on your phone to use ${wallet}.`,
|
|
64
|
+
qrLabel: (wallet) => `QR code to sign in with ${wallet}`,
|
|
65
|
+
onlyYours: (host) => `Only approve a sign-in you started yourself, on ${host}.`,
|
|
66
|
+
comeBack: (wallet) => `Approve the sign-in in ${wallet}. Then come back to this page.`,
|
|
67
|
+
yourWallet: "your wallet",
|
|
68
|
+
scanWith: (wallet) => `Scan this with ${wallet} on your phone. Then approve the sign-in.`,
|
|
69
|
+
scanCamera: (wallet) => `Scan this with your phone's camera to open ${wallet}. Then approve the sign-in.`,
|
|
70
|
+
checkCode: "Check that Nautilus shows this code: ",
|
|
71
|
+
tryAgain: "Try again",
|
|
72
|
+
otherWay: "Choose another way",
|
|
73
|
+
expired: "This sign-in expired.",
|
|
74
|
+
usedElsewhere: "This sign-in was used in another browser. Start again.",
|
|
75
|
+
tooManyChecks: "Too many checks. Waiting a moment…",
|
|
76
|
+
};
|
|
77
|
+
/** The game's words over the defaults; its faults over the default faults. A key it leaves out, or gives as undefined, keeps its default. */
|
|
78
|
+
export function playerSigninWords(over) {
|
|
79
|
+
const out = { ...PLAYER_SIGNIN_WORDS, faults: { ...PLAYER_SIGNIN_WORDS.faults } };
|
|
80
|
+
for (const [k, v] of Object.entries(over ?? {})) {
|
|
81
|
+
if (v === undefined || !(k in PLAYER_SIGNIN_WORDS))
|
|
82
|
+
continue;
|
|
83
|
+
if (k === "faults") {
|
|
84
|
+
for (const [code, f] of Object.entries(v))
|
|
85
|
+
if (f && code in out.faults)
|
|
86
|
+
out.faults[code] = f;
|
|
87
|
+
}
|
|
88
|
+
else
|
|
89
|
+
out[k] = v;
|
|
25
90
|
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
/** What happens after each fault: the passkey step again, or the wallet apps. Not words: the sheet's steps. */
|
|
94
|
+
const NEXT = {
|
|
95
|
+
cancelled: "stay",
|
|
96
|
+
"no-prf": "wallets",
|
|
97
|
+
unsupported: "wallets",
|
|
98
|
+
"other-form": "wallets",
|
|
99
|
+
"not-a-purse": "stay",
|
|
100
|
+
"wrong-host": "wallets",
|
|
101
|
+
exists: "stay",
|
|
102
|
+
unstable: "wallets",
|
|
103
|
+
"other-passkey": "stay",
|
|
104
|
+
failed: "stay",
|
|
105
|
+
};
|
|
106
|
+
/** A fault's title, text and next step, in the game's words (default: PLAYER_SIGNIN_WORDS). An unknown code gets the words of "failed". */
|
|
107
|
+
export function faultWords(code, rpId, words = PLAYER_SIGNIN_WORDS) {
|
|
108
|
+
const known = code in NEXT ? code : "failed";
|
|
109
|
+
const f = words.faults?.[known] ?? PLAYER_SIGNIN_WORDS.faults[known];
|
|
110
|
+
return { title: f.title, text: typeof f.text === "function" ? f.text(rpId) : f.text, next: NEXT[known] };
|
|
26
111
|
}
|
package/dist/shop/handler.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// TEST MODE: with no payee (none, the example address, or a wrong checksum) or `testMode: true`, the answer carries NO address and NO link, and the page
|
|
15
15
|
// shows "TEST MODE: DO NOT PAY". A payment to an address nobody holds the key for is lost for good.
|
|
16
16
|
import { fail, json, memoryLimiter, readLimited, sameOrigin } from "../admin/http.js";
|
|
17
|
-
import { checkLooks } from "../looks/look.js";
|
|
17
|
+
import { checkLooks, usdDecimals } from "../looks/look.js";
|
|
18
18
|
import { payeeOf } from "./address.js";
|
|
19
19
|
import { nanoUri, priceRaw, xnoText } from "./amount.js";
|
|
20
20
|
import { PUBLIC_NODES } from "./nodes.js";
|
|
@@ -101,7 +101,7 @@ export function createShopHandler(options) {
|
|
|
101
101
|
if (!price)
|
|
102
102
|
return fail(503, "Prices are not available now. Try again later.");
|
|
103
103
|
try {
|
|
104
|
-
const order = await reserveOrder(store, { player, lookId: look.id, usd: look.priceUsd
|
|
104
|
+
const order = await reserveOrder(store, { player, lookId: look.id, usd: usdDecimals(look.priceUsd), rate: rate.usd, price, visitor, now: at });
|
|
105
105
|
return json(payView(order, at), 201);
|
|
106
106
|
}
|
|
107
107
|
catch (e) {
|
|
@@ -5,3 +5,5 @@ export type { LooksViewOptions, SlotWords } from "./looks.js";
|
|
|
5
5
|
export { PAY_TIMING, isTestOrder, payPanel } from "./pay.js";
|
|
6
6
|
export type { PayOptions, PayPanel } from "./pay.js";
|
|
7
7
|
export { qrModules, qrSvg, qrVersionFor } from "./qr.js";
|
|
8
|
+
export { COINGECKO_CREDIT, SHOP_WORDS, priceLine, shopWords } from "./words.js";
|
|
9
|
+
export type { ShopWords } from "./words.js";
|
package/dist/shop/panel/index.js
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
// import "@nano133/nanoterra/shop/style.css";
|
|
5
5
|
// const shop = mountShop(document.getElementById("shop")!, { looks, owned: () => me.owned, signedIn: () => !!me.address, signIn, worn, wear, onOwned });
|
|
6
6
|
//
|
|
7
|
+
// Every word of the two sheets can be the game's own: `mountShop(el, { ..., words: { sendExactly: "Pay exactly" } })` (ShopWords; SHOP_WORDS are the defaults).
|
|
8
|
+
//
|
|
7
9
|
// The server part is "@nano133/nanoterra/shop" (the handler and the stores): it must not load this one.
|
|
8
10
|
export { mountShop } from "./mount.js";
|
|
9
11
|
export { looksView, slotsOf } from "./looks.js";
|
|
10
12
|
export { PAY_TIMING, isTestOrder, payPanel } from "./pay.js";
|
|
11
13
|
export { qrModules, qrSvg, qrVersionFor } from "./qr.js";
|
|
14
|
+
export { COINGECKO_CREDIT, SHOP_WORDS, priceLine, shopWords } from "./words.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Look } from "../../looks/look.js";
|
|
2
|
+
import { type ShopWords } from "./words.js";
|
|
2
3
|
/** A tab's words: its key's word, and its plain choice's name (an item slot's: "Plain shovel", "No charm"). */
|
|
3
4
|
export type SlotWords = {
|
|
4
5
|
tab: string;
|
|
@@ -24,6 +25,8 @@ export type LooksViewOptions = {
|
|
|
24
25
|
tab?: string;
|
|
25
26
|
/** A tab was chosen: the sheet draws again with it. */
|
|
26
27
|
onTab?: (slot: string) => void;
|
|
28
|
+
/** The game's words (./words.ts): the keys it leaves out keep the defaults. */
|
|
29
|
+
words?: Partial<ShopWords>;
|
|
27
30
|
};
|
|
28
31
|
/** The wear slots of a list, in the order they first show (the outfits first). */
|
|
29
32
|
export declare function slotsOf(looks: readonly Look[]): string[];
|
package/dist/shop/panel/looks.js
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
// with `takeOff`, each item tab starts with its plain choice (free, "No charm").
|
|
6
6
|
import { h } from "../../admin/panel/dom.js";
|
|
7
7
|
import { OUTFIT, isOutfit, usdText, wearOf } from "../../looks/look.js";
|
|
8
|
-
|
|
9
|
-
const titleCase = (s) => s.charAt(0).toUpperCase() + s.slice(1).replace(/-/g, " ");
|
|
8
|
+
import { shopWords } from "./words.js";
|
|
10
9
|
/** The wear slots of a list, in the order they first show (the outfits first). */
|
|
11
10
|
export function slotsOf(looks) {
|
|
12
11
|
const out = [OUTFIT];
|
|
@@ -34,13 +33,14 @@ function swatchOf(look) {
|
|
|
34
33
|
export function looksView(p) {
|
|
35
34
|
const slots = slotsOf(p.looks);
|
|
36
35
|
const tab = p.tab && slots.includes(p.tab) ? p.tab : slots[0];
|
|
37
|
-
const
|
|
36
|
+
const w = shopWords(p.words);
|
|
37
|
+
const words = (slot) => p.slots?.[slot] ?? (slot === OUTFIT ? { tab: w.outfits } : { tab: w.slotTab(slot), plain: w.slotPlain(slot) });
|
|
38
38
|
const list = h("ul", { class: "n133s-looks" });
|
|
39
39
|
const row = (pic, name, line, action, worn) => h("li", { class: `n133s-look${worn ? " n133s-look-worn" : ""}` }, pic, h("span", { class: "n133s-look-text" }, h("strong", {}, name), h("small", {}, line)), action);
|
|
40
40
|
if (tab !== OUTFIT && p.takeOff) {
|
|
41
41
|
const plain = p.plainWorn ? p.plainWorn(tab) : !p.looks.some((l) => wearOf(l) === tab && p.worn(l));
|
|
42
|
-
const action = h("button", { class: `n133s-btn${plain ? "" : " n133s-primary"}`, type: "button", disabled: plain, onclick: () => p.takeOff(tab) }, plain ?
|
|
43
|
-
list.append(row(h("span", { class: "n133s-swatch n133s-swatch-plain", "aria-hidden": "true" }), words(tab).plain ??
|
|
42
|
+
const action = h("button", { class: `n133s-btn${plain ? "" : " n133s-primary"}`, type: "button", disabled: plain, onclick: () => p.takeOff(tab) }, plain ? w.wearing : w.wear);
|
|
43
|
+
list.append(row(h("span", { class: "n133s-swatch n133s-swatch-plain", "aria-hidden": "true" }), words(tab).plain ?? w.slotPlain(tab), w.free, action, plain));
|
|
44
44
|
}
|
|
45
45
|
for (const look of p.looks) {
|
|
46
46
|
if (wearOf(look) !== tab)
|
|
@@ -48,14 +48,14 @@ export function looksView(p) {
|
|
|
48
48
|
const mine = look.priceUsd === 0 || p.owned.has(look.id);
|
|
49
49
|
const worn = p.worn(look);
|
|
50
50
|
const action = mine
|
|
51
|
-
? h("button", { class: `n133s-btn${worn ? "" : " n133s-primary"}`, type: "button", disabled: worn, onclick: () => p.wear(look) }, worn ?
|
|
52
|
-
: h("button", { class: "n133s-btn n133s-buy", type: "button", onclick: () => p.buy(look) },
|
|
53
|
-
const what = isOutfit(look) ? (look.change.hat
|
|
54
|
-
const state = mine ?
|
|
51
|
+
? h("button", { class: `n133s-btn${worn ? "" : " n133s-primary"}`, type: "button", disabled: worn, onclick: () => p.wear(look) }, worn ? w.wearing : w.wear)
|
|
52
|
+
: h("button", { class: "n133s-btn n133s-buy", type: "button", onclick: () => p.buy(look) }, w.buy(usdText(look.priceUsd)));
|
|
53
|
+
const what = isOutfit(look) ? w.outfitLine(look.change.hat ?? null) : w.itemLine(tab);
|
|
54
|
+
const state = mine ? ` · ${look.priceUsd === 0 ? w.free : w.yours}` : "";
|
|
55
55
|
list.append(row(p.picture?.(look) ?? swatchOf(look), look.name, `${what}${state}`, action, worn));
|
|
56
56
|
}
|
|
57
57
|
const tabs = slots.length > 1
|
|
58
|
-
? h("div", { class: "n133s-tabs", role: "tablist", "aria-label":
|
|
58
|
+
? h("div", { class: "n133s-tabs", role: "tablist", "aria-label": w.kinds }, ...slots.map((s) => h("button", { class: "n133s-tab", type: "button", role: "tab", "aria-selected": s === tab ? "true" : "false", onclick: () => p.onTab?.(s) }, words(s).tab)))
|
|
59
59
|
: null;
|
|
60
|
-
return h("div", { class: "n133s-looks-view" }, tabs, list, h("p", { class: "n133s-fine" },
|
|
60
|
+
return h("div", { class: "n133s-looks-view" }, tabs, list, h("p", { class: "n133s-fine" }, w.looksNote), p.signedIn ? null : h("p", { class: "n133s-fine" }, w.signInFirst));
|
|
61
61
|
}
|