pi-roundtable-web 0.25.0 → 0.26.1
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 +54 -0
- package/README.md +165 -12
- package/dist/account-zk4efb87.css +102 -0
- package/dist/chunk-account-ghq105t4.js +1 -0
- package/dist/chunk-action-menu-me0g9qsc.js +1 -0
- package/dist/{chunk-agent-0pm9p3nn.js → chunk-agent-rdv7ekrt.js} +1 -1
- package/dist/chunk-arranged-rooms-81rqhx9x.js +1 -0
- package/dist/chunk-character-creator-bd426yjw.js +1 -0
- package/dist/chunk-codeeditor-ajhgd33t.js +2 -0
- package/dist/chunk-confirm-p3mts8mx.js +1 -0
- package/dist/{chunk-connectors-anhghy3k.js → chunk-connectors-bkv3xn83.js} +1 -1
- package/dist/chunk-conversations-0e0vbxwr.js +1 -0
- package/dist/{chunk-drag-q4fx9b22.js → chunk-drag-peyb8t0k.js} +1 -1
- package/dist/{chunk-entry-he1embgp.js → chunk-entry-mv7q8br0.js} +1 -1
- package/dist/chunk-folder-list-era3tksh.js +1 -0
- package/dist/chunk-folds-hvws0bd5.js +1 -0
- package/dist/chunk-gate-hc7dd0fx.js +1 -0
- package/dist/chunk-group-scene-dkbpmbh5.js +1 -0
- package/dist/chunk-group-table-fqmxgdfw.js +1 -0
- package/dist/chunk-host-icons-4xde9ntk.js +1 -0
- package/dist/chunk-host-picture-014yzk20.js +1 -0
- package/dist/chunk-host-state-t56e5sdy.js +1 -0
- package/dist/chunk-host-ui-p1zy9nmp.js +1 -0
- package/dist/chunk-index-0fmmvgee.js +2 -0
- package/dist/chunk-index-0q15ysrb.js +1 -0
- package/dist/chunk-index-0xh24y25.js +2 -0
- package/dist/chunk-index-2yebd9tb.js +426 -0
- package/dist/chunk-index-4t0r3t3b.js +2 -0
- package/dist/chunk-index-5yzfrf7b.js +138 -0
- package/dist/{chunk-index-d5abvq7v.js → chunk-index-6twb7ccd.js} +2 -2
- package/dist/chunk-index-73gh7kgr.js +920 -0
- package/dist/chunk-index-7xffskfa.js +8 -0
- package/dist/chunk-index-aqw7cy5j.js +1 -0
- package/dist/chunk-index-b6ca02e8.js +2 -0
- package/dist/chunk-index-b8nvhcx5.js +2 -0
- package/dist/{chunk-index-bvfqjh77.js → chunk-index-cvbdf2ca.js} +2 -2
- package/dist/chunk-index-czdketze.js +2 -0
- package/dist/{chunk-index-9jqcskb4.js → chunk-index-dt9775ka.js} +5 -5
- package/dist/chunk-index-eg55mqqe.js +1 -0
- package/dist/chunk-index-gr6e0hse.js +2 -0
- package/dist/chunk-index-gyary7np.js +13 -0
- package/dist/chunk-index-j214tczc.js +1 -0
- package/dist/chunk-index-jd6632ef.js +2 -0
- package/dist/chunk-index-jw7dtshr.js +2 -0
- package/dist/{chunk-index-krtk6nn2.js → chunk-index-k7t14rwa.js} +2 -2
- package/dist/chunk-index-kwcx62wb.js +4 -0
- package/dist/chunk-index-m7fvw92f.js +2 -0
- package/dist/chunk-index-mmg788a1.js +2 -0
- package/dist/chunk-index-mq0dacwr.js +2 -0
- package/dist/chunk-index-p53ak25v.js +2 -0
- package/dist/{chunk-index-27qca4mr.js → chunk-index-pk1yxkv4.js} +2 -2
- package/dist/{chunk-index-9b1kt7qw.css → chunk-index-rbcz9jd4.css} +1 -1
- package/dist/{chunk-index-s681nny9.js → chunk-index-rmxbdmn2.js} +2 -2
- package/dist/chunk-index-rnxj3z8k.js +2 -0
- package/dist/chunk-index-snzzfhk3.js +2 -0
- package/dist/chunk-index-v3bmr0nw.js +2 -0
- package/dist/chunk-index-vcyzwtk2.js +2 -0
- package/dist/{chunk-index-zfd00hwb.js → chunk-index-w2j3njb6.js} +2 -2
- package/dist/chunk-index-w31c4yjv.js +2 -0
- package/dist/chunk-index-wrm8w165.js +2 -0
- package/dist/chunk-index-xh0mg9cw.js +2 -0
- package/dist/chunk-index-ybtvvx85.js +2 -0
- package/dist/chunk-index-yzsgsvnp.js +2 -0
- package/dist/{chunk-index-18gxy79n.js → chunk-index-z9x4cnzf.js} +2 -2
- package/dist/chunk-index-zrvg23jd.js +2 -0
- package/dist/chunk-index-zynq0b0w.js +50 -0
- package/dist/chunk-lang-css-88v3hp3d.js +1 -0
- package/dist/{chunk-lang-dockerfile-d5mzxs60.js → chunk-lang-dockerfile-k728ewsf.js} +1 -1
- package/dist/{chunk-lang-go-830fmhba.js → chunk-lang-go-wjwgay4v.js} +3 -3
- package/dist/chunk-lang-html-07d47es4.js +1 -0
- package/dist/chunk-lang-javascript-fmhhgzwd.js +1 -0
- package/dist/{chunk-lang-json-5eaarz7j.js → chunk-lang-json-7gd89mvs.js} +1 -1
- package/dist/chunk-lang-markdown-b6qbe8bt.js +6 -0
- package/dist/{chunk-lang-python-fjw84w10.js → chunk-lang-python-3v56y31b.js} +2 -2
- package/dist/{chunk-lang-rust-5rbp2zdk.js → chunk-lang-rust-kh0z74d1.js} +1 -1
- package/dist/{chunk-lang-shell-vzagpym3.js → chunk-lang-shell-9zv0zvbx.js} +1 -1
- package/dist/{chunk-lang-sql-0c2w9c2y.js → chunk-lang-sql-yw6bf3t7.js} +2 -2
- package/dist/{chunk-lang-toml-yr9fv6eb.js → chunk-lang-toml-tg1r96nq.js} +1 -1
- package/dist/chunk-lang-typescript-etjjvr2g.js +1 -0
- package/dist/{chunk-lang-xml-eze08p42.js → chunk-lang-xml-ev1mndd4.js} +1 -1
- package/dist/{chunk-lang-yaml-c4b6ft3y.js → chunk-lang-yaml-fz5qpb3r.js} +1 -1
- package/dist/chunk-layout-actions-48ecgtkg.js +1 -0
- package/dist/{chunk-menu-items-jrwxycr1.js → chunk-menu-items-1epy3h0t.js} +2 -2
- package/dist/chunk-mode-toggle-fxmfxsa4.js +1 -0
- package/dist/chunk-note-row-kd0fy7ee.js +75 -0
- package/dist/chunk-notes-yb8f057c.js +1 -0
- package/dist/chunk-notice-bell-wr4f1rph.js +1 -0
- package/dist/chunk-notices-x07da5bb.js +1 -0
- package/dist/chunk-onboarding-csrkbh9z.js +1 -0
- package/dist/chunk-overview-avc2d30t.js +1 -0
- package/dist/chunk-palette-9nhp95bz.js +1 -0
- package/dist/chunk-placement-items-q694dpgk.js +1 -0
- package/dist/chunk-profile-6qemf3m7.js +1 -0
- package/dist/chunk-prompt-history-7enzp2p3.js +1 -0
- package/dist/chunk-prompts-nv6mrn0q.js +1 -0
- package/dist/chunk-room-confirms-zz7n61y7.js +1 -0
- package/dist/chunk-room-menu-rr6nzap1.js +1 -0
- package/dist/chunk-rooms-menu-button-zbtwj47d.js +1 -0
- package/dist/chunk-schedules-rw57nr0m.js +1 -0
- package/dist/{chunk-seat-menu-jv4s7hmj.js → chunk-seat-menu-ys5v9abz.js} +1 -1
- package/dist/chunk-sheet-hosts-jhvaacst.js +1 -0
- package/dist/chunk-skill-editor-sz8an8qr.js +8 -0
- package/dist/chunk-skills-tnr1h16h.js +1 -0
- package/dist/chunk-switcher-list-17enhrse.js +1 -0
- package/dist/chunk-tab-dot-r6brdbmw.js +1 -0
- package/dist/chunk-table-r12cja6p.js +1 -0
- package/dist/chunk-table-seats-g3t392zz.js +1 -0
- package/dist/{chunk-table2d-4sje1fr0.js → chunk-table2d-p3ezsdcj.js} +1 -1
- package/dist/{chunk-table3d-h64skazp.js → chunk-table3d-4x2n2b00.js} +23 -23
- package/dist/chunk-thread-actions-qc6ezkha.js +1 -0
- package/dist/chunk-transcript-n2s6dher.js +1 -0
- package/dist/chunk-where-f3scdydc.js +1 -0
- package/dist/chunk-workspace-xs023bxm.js +1 -0
- package/dist/chunk-zh-tw-efvmk3fd.js +1 -0
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +5 -4
- package/src/api-types.ts +356 -4
- package/src/app/app.ts +201 -32
- package/src/app/desk.ts +47 -22
- package/src/app/files.ts +73 -7
- package/src/app/presence.ts +2 -2
- package/src/app/protocol.ts +63 -1
- package/src/app/room-log.ts +153 -14
- package/src/app/rooms.ts +39 -0
- package/src/app/routes.ts +168 -28
- package/src/app/socket.ts +349 -81
- package/src/app/sockets.ts +108 -20
- package/src/app/surface.ts +72 -36
- package/src/app/threads.ts +31 -2
- package/src/app/uploads.ts +33 -7
- package/src/auth/authenticator.ts +77 -8
- package/src/auth/login-page.ts +4 -2
- package/src/auth/next.ts +52 -0
- package/src/auth/oidc-pages.ts +62 -0
- package/src/auth/oidc.ts +649 -0
- package/src/auth/sessions.ts +193 -0
- package/src/authz.ts +173 -0
- package/src/console-api.ts +122 -25
- package/src/console-server.ts +102 -12
- package/src/gate.ts +90 -0
- package/src/host/access.ts +161 -0
- package/src/host/cards.ts +112 -0
- package/src/host/context.ts +188 -0
- package/src/host/forms.ts +240 -0
- package/src/host/frames.ts +349 -0
- package/src/host/host-app.ts +178 -0
- package/src/host/host-context.ts +49 -0
- package/src/host/host-routes.ts +396 -0
- package/src/host/pages.ts +266 -0
- package/src/host/reads.ts +155 -0
- package/src/host/retention.ts +41 -0
- package/src/host/room-views.ts +137 -0
- package/src/host/rooms-service.ts +429 -0
- package/src/host/stored-json.ts +13 -0
- package/src/host/views.ts +106 -0
- package/src/i18n/zh-tw.ts +139 -0
- package/src/index.ts +15 -1
- package/src/notices/api.ts +43 -0
- package/src/notices/mount.ts +292 -0
- package/src/notices/service.ts +294 -0
- package/src/notices/store.ts +298 -0
- package/src/notices/urls.ts +38 -0
- package/src/onboarding/api.ts +32 -0
- package/src/onboarding/service.ts +149 -0
- package/src/onboarding/steps.ts +75 -0
- package/src/onboarding/waivers.ts +40 -0
- package/src/options.ts +534 -34
- package/src/page-presentation.ts +47 -2
- package/src/profile/api.ts +175 -0
- package/src/profile/links.ts +162 -0
- package/src/push/api.ts +189 -52
- package/src/push/dispatch.ts +156 -0
- package/src/push/drain.ts +139 -0
- package/src/push/outbox.ts +218 -0
- package/src/push/prefs.ts +118 -0
- package/src/push/sender.ts +7 -2
- package/src/push/triggers.ts +56 -5
- package/src/push/vapid.ts +19 -0
- package/src/push/web-push-transport.ts +106 -0
- package/src/push-sender/index.ts +14 -0
- package/src/push-sender/run.ts +118 -0
- package/src/push-sender/session.ts +54 -0
- package/src/pwa.ts +32 -4
- package/src/visitors.ts +94 -6
- package/src/web-app.ts +718 -0
- package/src/web-plugin.ts +279 -26
- package/dist/chunk-action-menu-qe839g4b.js +0 -1
- package/dist/chunk-arranged-rooms-enpfswmg.js +0 -1
- package/dist/chunk-character-creator-6yf29xwx.js +0 -1
- package/dist/chunk-codeeditor-gdbdg7rp.js +0 -2
- package/dist/chunk-confirm-xrw7d732.js +0 -1
- package/dist/chunk-conversations-ry228aes.js +0 -1
- package/dist/chunk-folder-list-5cp8stna.js +0 -1
- package/dist/chunk-folds-hff104tk.js +0 -1
- package/dist/chunk-group-scene-h228jdat.js +0 -1
- package/dist/chunk-index-0p5h2kdz.js +0 -2
- package/dist/chunk-index-0pt08yd9.js +0 -1
- package/dist/chunk-index-12bw9yj8.js +0 -2
- package/dist/chunk-index-2ad17eh4.js +0 -2
- package/dist/chunk-index-31wh5twj.js +0 -52
- package/dist/chunk-index-4dy94pbz.js +0 -2
- package/dist/chunk-index-4ecjtdy8.js +0 -13
- package/dist/chunk-index-4v1b7ks9.js +0 -2
- package/dist/chunk-index-5d8yjj4m.js +0 -2
- package/dist/chunk-index-89ajmxh7.js +0 -2
- package/dist/chunk-index-b9ny4623.js +0 -2
- package/dist/chunk-index-e1dr6g10.js +0 -2
- package/dist/chunk-index-gpkzhq1g.js +0 -2
- package/dist/chunk-index-hk3xyy20.js +0 -2
- package/dist/chunk-index-jm23vn03.js +0 -4
- package/dist/chunk-index-k7gh0jvz.js +0 -2
- package/dist/chunk-index-ky42ev0b.js +0 -2
- package/dist/chunk-index-pa2yyqh0.js +0 -2
- package/dist/chunk-index-pcvt668q.js +0 -2
- package/dist/chunk-index-pqw27eye.js +0 -1
- package/dist/chunk-index-wmbmmrd4.js +0 -302
- package/dist/chunk-index-ymbjrsp6.js +0 -2
- package/dist/chunk-lang-css-t85r8rz0.js +0 -1
- package/dist/chunk-lang-html-n6jparge.js +0 -1
- package/dist/chunk-lang-javascript-xj00cp97.js +0 -1
- package/dist/chunk-lang-markdown-zxx8eke8.js +0 -6
- package/dist/chunk-lang-typescript-8f6yywt3.js +0 -1
- package/dist/chunk-layout-actions-bv0chj41.js +0 -1
- package/dist/chunk-notes-9b7pgqfe.js +0 -1
- package/dist/chunk-overview-8yw71esz.js +0 -1
- package/dist/chunk-palette-frms7axe.js +0 -1
- package/dist/chunk-placement-items-fvfy9m0y.js +0 -1
- package/dist/chunk-prompt-history-yrqehads.js +0 -1
- package/dist/chunk-prompts-g6609tqx.js +0 -1
- package/dist/chunk-room-confirms-0a2y0925.js +0 -1
- package/dist/chunk-room-menu-g11q97br.js +0 -1
- package/dist/chunk-rooms-menu-button-h9qge48n.js +0 -1
- package/dist/chunk-schedules-ezmacrbj.js +0 -1
- package/dist/chunk-sheet-hosts-b0azyfhb.js +0 -1
- package/dist/chunk-skill-editor-h6k6whwg.js +0 -8
- package/dist/chunk-skills-gqcfhq56.js +0 -1
- package/dist/chunk-switcher-list-mcpamqnj.js +0 -1
- package/dist/chunk-table-s8cde1xz.js +0 -1
- package/dist/chunk-table-seats-wqkjn8bc.js +0 -1
- package/dist/chunk-transcript-c59c6mpm.js +0 -1
- package/dist/chunk-workspace-0cyspkk4.js +0 -1
- package/dist/chunk-zh-tw-nc93jm10.js +0 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { IdentityService, Logger, PersonProfiles } from "pi-roundtable";
|
|
2
|
+
import type { OnboardingStepView, OnboardingView } from "../api-types.ts";
|
|
3
|
+
import type { Gate } from "../gate.ts";
|
|
4
|
+
import type {
|
|
5
|
+
OnboardingStep,
|
|
6
|
+
StepStatus,
|
|
7
|
+
Viewer,
|
|
8
|
+
WebApp,
|
|
9
|
+
WebAppRegistry,
|
|
10
|
+
} from "../web-app.ts";
|
|
11
|
+
import type { OnboardingWaivers } from "./waivers.ts";
|
|
12
|
+
|
|
13
|
+
type OnboardingPort = WebApp["onboarding"];
|
|
14
|
+
|
|
15
|
+
/** The id of the notifications step. */
|
|
16
|
+
export const PUSH_STEP = "push";
|
|
17
|
+
/** The id of the step that links a provider's account. */
|
|
18
|
+
export const linkStep = (provider: string): string => `link:${provider}`;
|
|
19
|
+
|
|
20
|
+
export interface OnboardingServiceOptions {
|
|
21
|
+
registry: Pick<WebAppRegistry, "steps" | "step" | "prover">;
|
|
22
|
+
waivers: Pick<OnboardingWaivers, "waived" | "waive">;
|
|
23
|
+
gate: Pick<Gate, "changed">;
|
|
24
|
+
identity: Pick<IdentityService, "speakerFor" | "principal">;
|
|
25
|
+
profiles?: Pick<PersonProfiles, "of">;
|
|
26
|
+
push: WebApp["push"];
|
|
27
|
+
logger: Logger;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What a person owes before the app opens, and how they settle it. The steps are the host's
|
|
32
|
+
* (`registerStep`) and the app's own (`link:<provider>`, `push`); a step is undone until its
|
|
33
|
+
* `done` says it is, and a required one may be waived only when it offers a waiver.
|
|
34
|
+
*/
|
|
35
|
+
export class OnboardingService implements OnboardingPort {
|
|
36
|
+
readonly #options: OnboardingServiceOptions;
|
|
37
|
+
|
|
38
|
+
constructor(options: OnboardingServiceOptions) {
|
|
39
|
+
this.#options = options;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The person as a step's callbacks see them, built from their id alone. */
|
|
43
|
+
async viewerFor(principalId: string): Promise<Viewer> {
|
|
44
|
+
const { identity, profiles } = this.#options;
|
|
45
|
+
let tier: Viewer["tier"] = "member";
|
|
46
|
+
try {
|
|
47
|
+
tier = (await identity.speakerFor(principalId)).tier;
|
|
48
|
+
} catch {
|
|
49
|
+
// No tier: they are a member as far as a step is concerned.
|
|
50
|
+
}
|
|
51
|
+
const name =
|
|
52
|
+
(await profiles?.of(principalId))?.name ??
|
|
53
|
+
(await identity.principal(principalId))?.displayName ??
|
|
54
|
+
principalId;
|
|
55
|
+
return { principalId, speakerId: principalId, tier, roles: [], name };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async status(principalId: string): Promise<StepStatus[]> {
|
|
59
|
+
return this.statuses(await this.viewerFor(principalId));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Every step that applies to the viewer, with whether it is done or waived. */
|
|
63
|
+
async statuses(viewer: Viewer): Promise<StepStatus[]> {
|
|
64
|
+
const found: StepStatus[] = [];
|
|
65
|
+
for (const step of this.#options.registry.steps()) {
|
|
66
|
+
try {
|
|
67
|
+
if (step.applies && !(await step.applies(viewer))) continue;
|
|
68
|
+
const done = await step.done(viewer);
|
|
69
|
+
const waived =
|
|
70
|
+
!done && step.required
|
|
71
|
+
? await this.#options.waivers.waived(viewer.principalId, step.id)
|
|
72
|
+
: false;
|
|
73
|
+
found.push(statusOf(step, done, waived));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// A step that cannot be read is undone.
|
|
76
|
+
this.#options.logger.warn(
|
|
77
|
+
{ step: step.id, err: error },
|
|
78
|
+
"an onboarding step was not read",
|
|
79
|
+
);
|
|
80
|
+
found.push(statusOf(step, false, false));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return found;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
changed(principalId: string): void {
|
|
87
|
+
this.#options.gate.changed(principalId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The steps with the push state, as the onboarding page shows them. */
|
|
91
|
+
async view(viewer: Viewer): Promise<OnboardingView> {
|
|
92
|
+
const [steps, push] = await Promise.all([
|
|
93
|
+
this.statuses(viewer),
|
|
94
|
+
this.#options.push.status(viewer.principalId),
|
|
95
|
+
]);
|
|
96
|
+
return {
|
|
97
|
+
steps: steps.map(
|
|
98
|
+
(step): OnboardingStepView => ({
|
|
99
|
+
id: step.id,
|
|
100
|
+
label: step.label,
|
|
101
|
+
...(step.help ? { help: step.help } : {}),
|
|
102
|
+
...(step.builtin ? { builtin: step.builtin } : {}),
|
|
103
|
+
required: step.required,
|
|
104
|
+
open: step.open,
|
|
105
|
+
...(step.waiver ? { waiver: step.waiver } : {}),
|
|
106
|
+
done: step.done,
|
|
107
|
+
waived: step.waived,
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
110
|
+
push: {
|
|
111
|
+
devices: push.devices,
|
|
112
|
+
...(push.lastReceiptAt
|
|
113
|
+
? { lastReceiptAt: push.lastReceiptAt.toISOString() }
|
|
114
|
+
: {}),
|
|
115
|
+
waived: push.waived,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Waives a step for the person. Only a required step that offers a waiver can be; any other
|
|
122
|
+
* answers false. Recorded with who and when, then the gate is asked again.
|
|
123
|
+
*/
|
|
124
|
+
async waive(principalId: string, stepId: string): Promise<boolean> {
|
|
125
|
+
const step = this.#options.registry.step(stepId);
|
|
126
|
+
if (!step?.required || !step.waiver) return false;
|
|
127
|
+
await this.#options.waivers.waive(principalId, stepId);
|
|
128
|
+
this.changed(principalId);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function statusOf(
|
|
134
|
+
step: OnboardingStep,
|
|
135
|
+
done: boolean,
|
|
136
|
+
waived: boolean,
|
|
137
|
+
): StepStatus {
|
|
138
|
+
return {
|
|
139
|
+
id: step.id,
|
|
140
|
+
label: step.label,
|
|
141
|
+
...(step.help ? { help: step.help } : {}),
|
|
142
|
+
...(step.builtin ? { builtin: step.builtin } : {}),
|
|
143
|
+
required: step.required,
|
|
144
|
+
done,
|
|
145
|
+
waived,
|
|
146
|
+
open: step.open,
|
|
147
|
+
...(step.required && step.waiver ? { waiver: step.waiver } : {}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { IdentityService, Tier } from "pi-roundtable";
|
|
2
|
+
import type { PushOutbox } from "../push/outbox.ts";
|
|
3
|
+
import type { LinkProver, OnboardingStep, Viewer } from "../web-app.ts";
|
|
4
|
+
import { linkStep, PUSH_STEP } from "./service.ts";
|
|
5
|
+
|
|
6
|
+
export interface BuiltInStepsOptions {
|
|
7
|
+
/** `onboarding.links`: provider → required or nudge. */
|
|
8
|
+
links: Readonly<Record<string, "required" | "nudge">>;
|
|
9
|
+
/** `onboarding.push` and `onboarding.pushWaiver`. */
|
|
10
|
+
push: { mode: "required" | "nudge" | "off"; waiver: boolean };
|
|
11
|
+
/** The tiers whose people may receive push; the push step applies to these only. */
|
|
12
|
+
pushTiers: readonly Tier[];
|
|
13
|
+
prover(
|
|
14
|
+
provider: string,
|
|
15
|
+
): Pick<LinkProver, "provider" | "label" | "status"> | undefined;
|
|
16
|
+
identity: Pick<IdentityService, "identities">;
|
|
17
|
+
outbox: Pick<PushOutbox, "lastReceiptAt">;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The steps `onboarding` asks for. `link:<provider>` is done when the prover says the account is
|
|
22
|
+
* linked, or when the person has an identity of that provider from any source (a configuration, CLI
|
|
23
|
+
* or plugin link covers an owner, who never proves what the host already knows). `push` is done
|
|
24
|
+
* when a device of the person reported back within 30 days: a test push, or any notice.
|
|
25
|
+
*/
|
|
26
|
+
export function builtInSteps(options: BuiltInStepsOptions): OnboardingStep[] {
|
|
27
|
+
const steps: OnboardingStep[] = [];
|
|
28
|
+
for (const [provider, mode] of Object.entries(options.links)) {
|
|
29
|
+
steps.push({
|
|
30
|
+
id: linkStep(provider),
|
|
31
|
+
// Read when asked: a host registers its provers after the app has made its steps.
|
|
32
|
+
get label() {
|
|
33
|
+
return options.prover(provider)?.label ?? provider;
|
|
34
|
+
},
|
|
35
|
+
required: mode === "required",
|
|
36
|
+
builtin: "link",
|
|
37
|
+
open: { page: "profile" },
|
|
38
|
+
done: (viewer) => linked(options, provider, viewer),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (options.push.mode !== "off") {
|
|
42
|
+
const required = options.push.mode === "required";
|
|
43
|
+
steps.push({
|
|
44
|
+
id: PUSH_STEP,
|
|
45
|
+
label: "Turn on notifications",
|
|
46
|
+
required,
|
|
47
|
+
builtin: "push",
|
|
48
|
+
open: { page: "onboarding" },
|
|
49
|
+
applies: (viewer) => options.pushTiers.includes(viewer.tier),
|
|
50
|
+
done: async (viewer) =>
|
|
51
|
+
(await options.outbox.lastReceiptAt(viewer.principalId)) !== undefined,
|
|
52
|
+
...(required && options.push.waiver
|
|
53
|
+
? {
|
|
54
|
+
waiver: {
|
|
55
|
+
label: "Skip for now",
|
|
56
|
+
confirm:
|
|
57
|
+
"Skip turning on notifications? You will not hear when something needs you.",
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
: {}),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return steps;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function linked(
|
|
67
|
+
options: BuiltInStepsOptions,
|
|
68
|
+
provider: string,
|
|
69
|
+
viewer: Viewer,
|
|
70
|
+
): Promise<boolean> {
|
|
71
|
+
const prover = options.prover(provider);
|
|
72
|
+
if (prover && (await prover.status(viewer.principalId)).linked) return true;
|
|
73
|
+
const identities = await options.identity.identities(viewer.principalId);
|
|
74
|
+
return identities.some((link) => link.provider === provider);
|
|
75
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
|
+
import type { Migration } from "pi-roundtable";
|
|
3
|
+
import type { WaiverRead } from "../web-app.ts";
|
|
4
|
+
|
|
5
|
+
/** The required steps people waived, with who and when, in `web_onboarding`. */
|
|
6
|
+
export class OnboardingWaivers implements WaiverRead {
|
|
7
|
+
readonly #sql: SQL;
|
|
8
|
+
|
|
9
|
+
constructor(sql: SQL) {
|
|
10
|
+
this.#sql = sql;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static readonly migration: Migration = {
|
|
14
|
+
name: "web-onboarding",
|
|
15
|
+
up: async (sql) => {
|
|
16
|
+
await sql`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS web_onboarding (
|
|
18
|
+
principal_id text NOT NULL,
|
|
19
|
+
step text NOT NULL,
|
|
20
|
+
state text NOT NULL CHECK (state IN ('waived')),
|
|
21
|
+
at timestamptz NOT NULL DEFAULT now(),
|
|
22
|
+
PRIMARY KEY (principal_id, step)
|
|
23
|
+
)`;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
async waived(principalId: string, step: string): Promise<boolean> {
|
|
28
|
+
const [found]: { n: number }[] = await this.#sql`
|
|
29
|
+
SELECT count(*)::int AS n FROM web_onboarding
|
|
30
|
+
WHERE principal_id = ${principalId} AND step = ${step} AND state = 'waived'`;
|
|
31
|
+
return (found?.n ?? 0) > 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Records that the person waived the step; waiving twice keeps the first time. */
|
|
35
|
+
async waive(principalId: string, step: string): Promise<void> {
|
|
36
|
+
await this.#sql`
|
|
37
|
+
INSERT INTO web_onboarding (principal_id, step, state)
|
|
38
|
+
VALUES (${principalId}, ${step}, 'waived') ON CONFLICT DO NOTHING`;
|
|
39
|
+
}
|
|
40
|
+
}
|