@ofidj/generator-fidj 1.0.0 → 1.0.3
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 +136 -57
- package/bin/create-fidj.cjs +62 -0
- package/generators/app/index.js +100 -189
- package/generators/app/templates/typescript/README.md +82 -0
- package/generators/app/templates/typescript/app.config.json +14 -0
- package/generators/app/templates/typescript/gitignore +5 -0
- package/generators/app/templates/typescript/package.json +25 -0
- package/generators/app/templates/typescript/public/fidj-logo.png +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexMono-400-latin-ext.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexMono-400-latin.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexMono-500-latin-ext.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexMono-500-latin.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexSans-400_600-latin-ext.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexSans-400_600-latin.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/InstrumentSerif-400-latin-ext.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/InstrumentSerif-400-latin.woff2 +0 -0
- package/generators/app/templates/typescript/public/index.html +16 -0
- package/generators/app/templates/typescript/scripts/build.mjs +49 -0
- package/generators/app/templates/typescript/scripts/privacy-check.mjs +21 -0
- package/generators/app/templates/typescript/scripts/render-content.mjs +14 -0
- package/generators/app/templates/typescript/server/data-store.ts +123 -0
- package/generators/app/templates/typescript/server/index.ts +338 -0
- package/generators/app/templates/typescript/server/privacy-adapter.ts +21 -0
- package/generators/app/templates/typescript/src/content.ts +491 -0
- package/generators/app/templates/typescript/src/fonts.css +92 -0
- package/generators/app/templates/typescript/src/main.ts +251 -0
- package/generators/app/templates/typescript/src/service-agreement.ts +40 -0
- package/generators/app/templates/typescript/src/style.css +916 -0
- package/generators/app/templates/typescript/src/tokens.css +56 -0
- package/generators/app/templates/typescript/test/server.test.mjs +287 -0
- package/generators/app/templates/typescript/tsconfig.json +15 -0
- package/lib/app-module.cjs +64 -0
- package/lib/scaffold.cjs +214 -0
- package/package.json +15 -13
- package/generators/app/templates/app2018/.travis.yml +0 -35
- package/generators/app/templates/app2018/LICENSE +0 -21
- package/generators/app/templates/app2018/Procfile +0 -1
- package/generators/app/templates/app2018/README.md +0 -49
- package/generators/app/templates/app2018/config.xml +0 -91
- package/generators/app/templates/app2018/gulpfile.js +0 -63
- package/generators/app/templates/app2018/ionic.config.json +0 -9
- package/generators/app/templates/app2018/package.json +0 -90
- package/generators/app/templates/app2018/platforms/README.md +0 -1
- package/generators/app/templates/app2018/resources/README.md +0 -6
- package/generators/app/templates/app2018/resources/icon.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-01-ipad.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-01-iphone.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-02-ipad.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-02-iphone.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-03-ipad.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-03-iphone.png +0 -0
- package/generators/app/templates/app2018/resources/marketing.json +0 -19
- package/generators/app/templates/app2018/resources/splash.png +0 -0
- package/generators/app/templates/app2018/src/app/app.component.ts +0 -88
- package/generators/app/templates/app2018/src/app/app.html +0 -19
- package/generators/app/templates/app2018/src/app/app.module.ts +0 -40
- package/generators/app/templates/app2018/src/app/app.scss +0 -16
- package/generators/app/templates/app2018/src/app/main.ts +0 -5
- package/generators/app/templates/app2018/src/app/shared/card.model.ts +0 -10
- package/generators/app/templates/app2018/src/app/shared/profile.service.ts +0 -46
- package/generators/app/templates/app2018/src/app/shared/shared.module.ts +0 -35
- package/generators/app/templates/app2018/src/app/shared/storage.service.ts +0 -11
- package/generators/app/templates/app2018/src/app/shared/version.const.ts +0 -5
- package/generators/app/templates/app2018/src/assets/icon/favicon.ico +0 -0
- package/generators/app/templates/app2018/src/assets/imgs/gg.png +0 -0
- package/generators/app/templates/app2018/src/assets/imgs/github.png +0 -0
- package/generators/app/templates/app2018/src/assets/imgs/login.svg +0 -5668
- package/generators/app/templates/app2018/src/assets/imgs/logo.png +0 -0
- package/generators/app/templates/app2018/src/index.html +0 -49
- package/generators/app/templates/app2018/src/manifest.json +0 -13
- package/generators/app/templates/app2018/src/pages/home/home.html +0 -56
- package/generators/app/templates/app2018/src/pages/home/home.scss +0 -3
- package/generators/app/templates/app2018/src/pages/home/home.ts +0 -126
- package/generators/app/templates/app2018/src/pages/login/login.html +0 -56
- package/generators/app/templates/app2018/src/pages/login/login.scss +0 -44
- package/generators/app/templates/app2018/src/pages/login/login.ts +0 -59
- package/generators/app/templates/app2018/src/pages/profile/profile.html +0 -33
- package/generators/app/templates/app2018/src/pages/profile/profile.scss +0 -3
- package/generators/app/templates/app2018/src/pages/profile/profile.ts +0 -36
- package/generators/app/templates/app2018/src/service-worker.js +0 -31
- package/generators/app/templates/app2018/src/theme/variables.scss +0 -88
- package/generators/app/templates/app2018/tests/karma.unit-tests.conf.js +0 -84
- package/generators/app/templates/app2018/tests/unit/app.test.js +0 -10
- package/generators/app/templates/app2018/tsconfig.json +0 -28
- package/generators/app/templates/app2018/tslint.json +0 -8
- package/generators/app/templates/app2018/web.js +0 -21
- package/generators/app/templates/app2021/.travis.yml +0 -32
- package/generators/app/templates/app2021/LICENSE +0 -21
- package/generators/app/templates/app2021/Procfile +0 -1
- package/generators/app/templates/app2021/README.md +0 -45
- package/generators/app/templates/app2021/angular.json +0 -188
- package/generators/app/templates/app2021/browserslist +0 -12
- package/generators/app/templates/app2021/ionic.config.json +0 -9
- package/generators/app/templates/app2021/karma.conf.js +0 -31
- package/generators/app/templates/app2021/package-lock.json +0 -5
- package/generators/app/templates/app2021/package.json +0 -77
- package/generators/app/templates/app2021/platforms/README.md +0 -1
- package/generators/app/templates/app2021/resources/README.md +0 -6
- package/generators/app/templates/app2021/resources/icon.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-01-ipad.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-01-iphone.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-02-ipad.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-02-iphone.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-03-ipad.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-03-iphone.png +0 -0
- package/generators/app/templates/app2021/resources/marketing.json +0 -19
- package/generators/app/templates/app2021/resources/splash.png +0 -0
- package/generators/app/templates/app2021/src/app/app-routing.module.ts +0 -25
- package/generators/app/templates/app2021/src/app/app.component.html +0 -3
- package/generators/app/templates/app2021/src/app/app.component.scss +0 -0
- package/generators/app/templates/app2021/src/app/app.component.spec.ts +0 -50
- package/generators/app/templates/app2021/src/app/app.component.ts +0 -28
- package/generators/app/templates/app2021/src/app/app.module.ts +0 -24
- package/generators/app/templates/app2021/src/app/content/content-routing.module.ts +0 -16
- package/generators/app/templates/app2021/src/app/content/content.module.ts +0 -20
- package/generators/app/templates/app2021/src/app/content/content.page.html +0 -31
- package/generators/app/templates/app2021/src/app/content/content.page.scss +0 -0
- package/generators/app/templates/app2021/src/app/content/content.page.ts +0 -26
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.html +0 -4
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.scss +0 -4
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.spec.ts +0 -24
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.ts +0 -16
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.module.ts +0 -14
- package/generators/app/templates/app2021/src/app/my/my-routing.module.ts +0 -36
- package/generators/app/templates/app2021/src/app/my/my.module.ts +0 -19
- package/generators/app/templates/app2021/src/app/my/my.page.html +0 -15
- package/generators/app/templates/app2021/src/app/my/my.page.scss +0 -1
- package/generators/app/templates/app2021/src/app/my/my.page.spec.ts +0 -26
- package/generators/app/templates/app2021/src/app/my/my.page.ts +0 -25
- package/generators/app/templates/app2021/src/app/profile/profile-routing.module.ts +0 -16
- package/generators/app/templates/app2021/src/app/profile/profile.module.ts +0 -20
- package/generators/app/templates/app2021/src/app/profile/profile.page.html +0 -49
- package/generators/app/templates/app2021/src/app/profile/profile.page.scss +0 -0
- package/generators/app/templates/app2021/src/app/profile/profile.page.spec.ts +0 -25
- package/generators/app/templates/app2021/src/app/profile/profile.page.ts +0 -44
- package/generators/app/templates/app2021/src/app/shared/const.ts +0 -3
- package/generators/app/templates/app2021/src/app/signin/signin-routing.module.ts +0 -16
- package/generators/app/templates/app2021/src/app/signin/signin.module.ts +0 -18
- package/generators/app/templates/app2021/src/app/signin/signin.page.html +0 -53
- package/generators/app/templates/app2021/src/app/signin/signin.page.scss +0 -36
- package/generators/app/templates/app2021/src/app/signin/signin.page.ts +0 -69
- package/generators/app/templates/app2021/src/assets/icon/favicon.png +0 -0
- package/generators/app/templates/app2021/src/assets/shapes.svg +0 -1
- package/generators/app/templates/app2021/src/environments/environment.prod.ts +0 -3
- package/generators/app/templates/app2021/src/environments/environment.ts +0 -16
- package/generators/app/templates/app2021/src/global.scss +0 -26
- package/generators/app/templates/app2021/src/index.html +0 -26
- package/generators/app/templates/app2021/src/main.ts +0 -12
- package/generators/app/templates/app2021/src/polyfills.ts +0 -67
- package/generators/app/templates/app2021/src/test.ts +0 -20
- package/generators/app/templates/app2021/src/theme/variables.scss +0 -232
- package/generators/app/templates/app2021/src/zone-flags.ts +0 -5
- package/generators/app/templates/app2021/tests/protractor.conf.js +0 -36
- package/generators/app/templates/app2021/tests/src/app.e2e-spec.ts +0 -14
- package/generators/app/templates/app2021/tests/src/app.po.ts +0 -11
- package/generators/app/templates/app2021/tests/tsconfig.json +0 -13
- package/generators/app/templates/app2021/tsconfig.app.json +0 -18
- package/generators/app/templates/app2021/tsconfig.json +0 -23
- package/generators/app/templates/app2021/tsconfig.spec.json +0 -18
- package/generators/app/templates/app2021/tslint.json +0 -149
- package/generators/app/templates/app2021/web.js +0 -21
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import {agreementMarkup, bindAgreement, acceptedAgreement} from "./service-agreement";
|
|
2
|
+
import { FidjNodeService } from "@ofidj/node";
|
|
3
|
+
import "./style.css";
|
|
4
|
+
|
|
5
|
+
type Session = { username: string; roles: string[] };
|
|
6
|
+
type Note = { id: string; title: string; body: string; createdAt: string };
|
|
7
|
+
type Settings = {
|
|
8
|
+
title: string;
|
|
9
|
+
appId: string;
|
|
10
|
+
apiEndpoint: string;
|
|
11
|
+
dashboardUrl: string;
|
|
12
|
+
localDemo: boolean;
|
|
13
|
+
};
|
|
14
|
+
const root = document.querySelector<HTMLDivElement>("#app")!;
|
|
15
|
+
const sdk = new FidjNodeService();
|
|
16
|
+
let settings: Settings;
|
|
17
|
+
let session: Session | null = null;
|
|
18
|
+
let notes: Note[] = [];
|
|
19
|
+
let privacy: {
|
|
20
|
+
consent: Record<string, boolean>;
|
|
21
|
+
history: Array<{ type: string; granted: boolean; changedAt: string }>;
|
|
22
|
+
} | null = null;
|
|
23
|
+
let view = "workspace";
|
|
24
|
+
let notice = "";
|
|
25
|
+
let error = "";
|
|
26
|
+
let busy = false;
|
|
27
|
+
let leaving = false;
|
|
28
|
+
const escape = (value: unknown) =>
|
|
29
|
+
String(value ?? "").replace(
|
|
30
|
+
/[&<>"']/g,
|
|
31
|
+
(char) =>
|
|
32
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
33
|
+
char
|
|
34
|
+
]!,
|
|
35
|
+
);
|
|
36
|
+
const el = <T extends HTMLElement>(id: string) =>
|
|
37
|
+
document.getElementById(id) as T;
|
|
38
|
+
async function api(path: string, method = "GET", data?: unknown) {
|
|
39
|
+
const token = await sdk.fidjGetIdToken();
|
|
40
|
+
const response = await fetch("/api/" + path, {
|
|
41
|
+
method,
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${token}`,
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
},
|
|
46
|
+
body: data === undefined ? undefined : JSON.stringify(data),
|
|
47
|
+
});
|
|
48
|
+
const result = await response.json();
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
if (response.status === 401) {
|
|
51
|
+
session = null;
|
|
52
|
+
await sdk.logout(true);
|
|
53
|
+
}
|
|
54
|
+
throw new Error(result.message || "Please retry.");
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
async function load() {
|
|
59
|
+
session = await api("session");
|
|
60
|
+
[notes, privacy] = await Promise.all([
|
|
61
|
+
api("notes").then((result) => result.notes),
|
|
62
|
+
api("privacy"),
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
async function action(task: () => Promise<void>) {
|
|
66
|
+
if (busy) return;
|
|
67
|
+
busy = true;
|
|
68
|
+
error = "";
|
|
69
|
+
notice = "";
|
|
70
|
+
try {
|
|
71
|
+
await task();
|
|
72
|
+
} catch (reason) {
|
|
73
|
+
error = reason instanceof Error ? reason.message : String(reason);
|
|
74
|
+
} finally {
|
|
75
|
+
busy = false;
|
|
76
|
+
render();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function render() {
|
|
80
|
+
document.title = settings.title + " · Fidj";
|
|
81
|
+
const canWrite = session?.roles.some((role) =>
|
|
82
|
+
["Owner", "Editor"].includes(role),
|
|
83
|
+
);
|
|
84
|
+
root.innerHTML = `<header class="topbar"><a class="brand" href="/"><span class="app-symbol">✳</span>${escape(settings.title)}</a><a class="fidj-brand" href="${escape(settings.dashboardUrl)}" target="_blank" rel="noopener"><img src="/fidj-logo.png" alt="Fidj">Privacy with Fidj ↗</a></header>
|
|
85
|
+
<main>${notice ? `<p class="notice" role="status">${escape(notice)}</p>` : ""}${error ? `<p class="error" role="alert">${escape(error)}</p>` : ""}
|
|
86
|
+
${
|
|
87
|
+
!session
|
|
88
|
+
? `<section class="welcome"><div><p class="eyebrow">A LITTLE SPACE FOR YOUR IDEAS</p><h1>Good ideas<br>start here.</h1><p>Keep your notes together, with access you understand and privacy you control.</p><div class="promise"><img src="/fidj-logo.png" alt=""><span>Your account connects through Fidj.<br>Your choices belong to this app.</span></div></div><form id="signin" class="card"><h2>Welcome to ${escape(settings.title)}</h2><p>Sign in with your Fidj account.</p><label for="email">Email</label><input id="email" type="email" autocomplete="username" required><label for="password">Password</label><input id="password" type="password" autocomplete="current-password" required>${agreementMarkup()}<button class="primary" type="submit" disabled>Continue</button>${settings.localDemo ? `<div class="demo"><strong>Try the local example</strong><p>Alex owns the app. Maya and Sam start with the Free role.</p><button type="button" data-demo="alex">Alex · owner</button><button type="button" data-demo="maya">Maya · member</button><button type="button" data-demo="sam">Sam · member</button></div>` : ""}</form></section>`
|
|
89
|
+
: `
|
|
90
|
+
<div class="page-heading"><div><p class="eyebrow">YOUR WORKSPACE</p><h1>A place to think.</h1><p>${escape(session.username)} <span class="roles">${session.roles.map(escape).join(" · ") || "No assigned roles"}</span></p></div><button id="signout">Sign out</button></div>
|
|
91
|
+
<nav><button id="workspace-tab" class="${view === "workspace" ? "selected" : ""}">My notes</button><button id="privacy-tab" class="${view === "privacy" ? "selected" : ""}">My privacy</button><button id="refresh">Refresh access</button></nav>
|
|
92
|
+
${
|
|
93
|
+
view === "workspace"
|
|
94
|
+
? `<section class="workspace"><div><div class="section-heading"><h2>Your notes</h2><span>${notes.length} saved</span></div>${notes.length ? notes.map((note) => `<article class="card note"><small>${escape(new Date(note.createdAt).toLocaleString())}</small><h3>${escape(note.title)}</h3><p>${escape(note.body)}</p></article>`).join("") : `<article class="card empty"><span>✳</span><h3>Room for your next idea.</h3><p>Your saved notes will appear here. Only you can read your notes.</p></article>`}</div><form id="note" class="card"><p class="eyebrow">CAPTURE SOMETHING</p><h2>A fresh note</h2><p>${canWrite ? "You have permission to save notes." : "Ask your app owner for Editor access to save notes."}</p><label for="note-title">Title</label><input id="note-title" maxlength="120" required ${!canWrite ? "disabled" : ""}><label for="note-body">Your note</label><textarea id="note-body" rows="6" maxlength="5000" ${!canWrite ? "disabled" : ""}></textarea><button class="primary" type="submit" ${!canWrite ? "disabled" : ""}>Save note</button><small>Your notes are saved by this app and survive a server restart.</small></form></section>`
|
|
95
|
+
: `<section class="privacy-grid"><article class="card"><p class="eyebrow">ONLY ${escape(settings.title).toUpperCase()}</p><h2>Your choices in this app</h2><p>These preferences are independent of Fidj and your other apps.</p><div class="agreement"><strong>Service agreement</strong><span>${privacy?.consent.terms ? "Accepted" : "Not recorded"}</span>${!privacy?.consent.terms ? '<p>This starter uses a demo agreement. Accept it to record your choice.</p><button id="accept-terms">Accept demo agreement</button>' : "<p>Leaving this app withdraws its required agreement.</p>"}</div>${["analytics", "communications", "optionalData"].map((key, index) => `<label class="toggle"><span><strong>${["Analytics", "Communications", "Optional data"][index]}</strong><small>${["Help improve this app.", "Receive optional news and updates.", "Allow data beyond the essential service."][index]}</small></span><input type="checkbox" data-purpose="${key}" ${privacy?.consent[key] ? "checked" : ""}></label>`).join("")}<button id="export">Export my data</button></article><article class="card"><h2>Consent history</h2>${
|
|
96
|
+
privacy?.history.length
|
|
97
|
+
? privacy.history
|
|
98
|
+
.slice()
|
|
99
|
+
.reverse()
|
|
100
|
+
.map(
|
|
101
|
+
(entry) =>
|
|
102
|
+
`<p><strong>${escape(entry.type)}</strong> · ${entry.granted ? "Accepted" : "Withdrawn"}<br><small>${escape(new Date(entry.changedAt).toLocaleString())}</small></p>`,
|
|
103
|
+
)
|
|
104
|
+
.join("")
|
|
105
|
+
: "<p>No changes yet.</p>"
|
|
106
|
+
}<hr><h2>Leave this app</h2><p>This removes your app membership and this starter’s notes. Your Fidj account and other memberships remain.</p>${session.roles.includes("Owner") ? "<p>As the app owner, resolve ownership before leaving.</p>" : leaving ? '<div role="alertdialog" aria-labelledby="leave-title"><h3 id="leave-title">Confirm departure</h3><p>Your membership, consent and notes in this app will be removed. Your Fidj account and other apps remain available.</p><button id="confirm-leave" class="danger">Confirm leaving and erase</button><button id="cancel-leave">Keep my membership</button></div>' : '<button id="leave" class="danger">Leave and erase my app data</button>'}<p class="fineprint">Exports here include your Fidj membership and this starter’s notes. The registered app-data handler lets Fidj export and erase these notes too. If cleanup is pending, retry from My privacy on Fidj. Minimal completion receipts are retained; backups and unregistered systems are outside this operation.</p></article></section>`
|
|
107
|
+
}`
|
|
108
|
+
}
|
|
109
|
+
<footer>Built with Fidj · One identity. Separate choices for every app.</footer></main>`;
|
|
110
|
+
void bindAgreement(el<HTMLFormElement>("signin"), settings.title, settings.apiEndpoint, settings.appId);
|
|
111
|
+
el<HTMLFormElement>("signin")?.addEventListener("submit", (event) => {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
const acceptance = acceptedAgreement(event.currentTarget as HTMLFormElement);
|
|
114
|
+
if (!acceptance) return;
|
|
115
|
+
const email = el<HTMLInputElement>("email").value;
|
|
116
|
+
const password = el<HTMLInputElement>("password").value;
|
|
117
|
+
void action(async () => {
|
|
118
|
+
await sdk.login(email, password, { autoSignup: false, ...acceptance });
|
|
119
|
+
await load();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
root.querySelectorAll<HTMLButtonElement>("[data-demo]").forEach((button) =>
|
|
123
|
+
button.addEventListener("click", () => {
|
|
124
|
+
el<HTMLInputElement>("email").value = button.dataset.demo + "@fidj.local";
|
|
125
|
+
el<HTMLInputElement>("password").value =
|
|
126
|
+
button.dataset.demo === "alex"
|
|
127
|
+
? "local-demo-only"
|
|
128
|
+
: "local-member-only";
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
el("signout")?.addEventListener(
|
|
132
|
+
"click",
|
|
133
|
+
() =>
|
|
134
|
+
void action(async () => {
|
|
135
|
+
await sdk.logout(true);
|
|
136
|
+
session = null;
|
|
137
|
+
notes = [];
|
|
138
|
+
privacy = null;
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
el("workspace-tab")?.addEventListener("click", () => {
|
|
142
|
+
view = "workspace";
|
|
143
|
+
render();
|
|
144
|
+
});
|
|
145
|
+
el("privacy-tab")?.addEventListener("click", () => {
|
|
146
|
+
view = "privacy";
|
|
147
|
+
render();
|
|
148
|
+
});
|
|
149
|
+
el("refresh")?.addEventListener(
|
|
150
|
+
"click",
|
|
151
|
+
() =>
|
|
152
|
+
void action(async () => {
|
|
153
|
+
await load();
|
|
154
|
+
notice = "Access and preferences refreshed.";
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
el<HTMLFormElement>("note")?.addEventListener("submit", (event) => {
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
const data = {
|
|
160
|
+
title: el<HTMLInputElement>("note-title").value,
|
|
161
|
+
body: el<HTMLTextAreaElement>("note-body").value,
|
|
162
|
+
};
|
|
163
|
+
void action(async () => {
|
|
164
|
+
await api("notes", "POST", data);
|
|
165
|
+
await load();
|
|
166
|
+
notice = "Note saved.";
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
root.querySelectorAll<HTMLInputElement>("[data-purpose]").forEach((input) =>
|
|
170
|
+
input.addEventListener(
|
|
171
|
+
"change",
|
|
172
|
+
() =>
|
|
173
|
+
void action(async () => {
|
|
174
|
+
await api("privacy", "PUT", {
|
|
175
|
+
[input.dataset.purpose!]: input.checked,
|
|
176
|
+
});
|
|
177
|
+
await load();
|
|
178
|
+
notice = "Saved for this app. Your other apps are unchanged.";
|
|
179
|
+
}),
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
el("accept-terms")?.addEventListener(
|
|
183
|
+
"click",
|
|
184
|
+
() =>
|
|
185
|
+
void action(async () => {
|
|
186
|
+
await api("privacy", "PUT", { terms: true });
|
|
187
|
+
await load();
|
|
188
|
+
notice = "Demo agreement accepted.";
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
el("export")?.addEventListener(
|
|
192
|
+
"click",
|
|
193
|
+
() =>
|
|
194
|
+
void action(async () => {
|
|
195
|
+
const data = await api("privacy/export");
|
|
196
|
+
const url = URL.createObjectURL(
|
|
197
|
+
new Blob([JSON.stringify(data, null, 2)], {
|
|
198
|
+
type: "application/json",
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
const link = document.createElement("a");
|
|
202
|
+
link.href = url;
|
|
203
|
+
link.download = `${settings.appId}-my-data.json`;
|
|
204
|
+
link.click();
|
|
205
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
206
|
+
notice = "Exported your membership, choices and notes.";
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
el("leave")?.addEventListener("click", () => {
|
|
210
|
+
leaving = true;
|
|
211
|
+
render();
|
|
212
|
+
});
|
|
213
|
+
el("cancel-leave")?.addEventListener("click", () => {
|
|
214
|
+
leaving = false;
|
|
215
|
+
render();
|
|
216
|
+
});
|
|
217
|
+
el("confirm-leave")?.addEventListener("click", () => {
|
|
218
|
+
void action(async () => {
|
|
219
|
+
const result = await api("privacy/leave", "DELETE", {
|
|
220
|
+
confirm: settings.appId,
|
|
221
|
+
});
|
|
222
|
+
await sdk.logout(true);
|
|
223
|
+
session = null;
|
|
224
|
+
notes = [];
|
|
225
|
+
privacy = null;
|
|
226
|
+
leaving = false;
|
|
227
|
+
notice =
|
|
228
|
+
result.status === "pending"
|
|
229
|
+
? "Access revoked. Cleanup is pending. Open My privacy on Fidj and choose Retry cleanup."
|
|
230
|
+
: "You left this app. Its membership and registered app data were erased. Your other apps are unchanged.";
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
async function start() {
|
|
235
|
+
try {
|
|
236
|
+
settings = await (await fetch("/api/config")).json();
|
|
237
|
+
await sdk.init(settings.appId, {
|
|
238
|
+
apiEndpoint: settings.apiEndpoint,
|
|
239
|
+
prod: !settings.localDemo,
|
|
240
|
+
});
|
|
241
|
+
if (sdk.isLoggedIn()) await load();
|
|
242
|
+
} catch (reason) {
|
|
243
|
+
error =
|
|
244
|
+
reason instanceof Error ? reason.message : "Cannot connect to Fidj.";
|
|
245
|
+
}
|
|
246
|
+
if (settings) render();
|
|
247
|
+
else
|
|
248
|
+
root.textContent =
|
|
249
|
+
"This app could not load. Please check the server and retry.";
|
|
250
|
+
}
|
|
251
|
+
void start();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function agreementMarkup() {
|
|
2
|
+
return `<div class="signin-agreement"><label class="agreement-choice"><input id="service-agreement" type="checkbox" required disabled><span id="agreement-label">I accept the service agreement for this app.</span></label><button type="button" id="read-agreement" disabled>Read service agreement</button><p id="agreement-status" class="fineprint" role="status">Loading service agreement…</p></div><dialog id="agreement-dialog" aria-labelledby="agreement-heading"><h2 id="agreement-heading">Service agreement</h2><p id="agreement-version"></p><p id="agreement-text"></p><button type="button" id="close-agreement">Close agreement</button></dialog>`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function acceptedAgreement(form: HTMLFormElement) {
|
|
6
|
+
const checkbox = form.querySelector<HTMLInputElement>("#service-agreement");
|
|
7
|
+
if (!checkbox?.checked || checkbox.disabled || !checkbox.dataset.version) return null;
|
|
8
|
+
return { termsAccepted: true, termsVersion: checkbox.dataset.version };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function bindAgreement(form: HTMLFormElement | null, title: string, endpoint: string, appId: string) {
|
|
12
|
+
if (!form) return;
|
|
13
|
+
const checkbox = form.querySelector<HTMLInputElement>("#service-agreement")!;
|
|
14
|
+
const read = form.querySelector<HTMLButtonElement>("#read-agreement")!;
|
|
15
|
+
const dialog = form.querySelector<HTMLDialogElement>("#agreement-dialog")!;
|
|
16
|
+
const status = form.querySelector<HTMLElement>("#agreement-status")!;
|
|
17
|
+
const submitButtons = form.querySelectorAll<HTMLButtonElement>('button[type="submit"]');
|
|
18
|
+
const update = () => submitButtons.forEach(button => { button.disabled = !acceptedAgreement(form); });
|
|
19
|
+
form.querySelector("#agreement-label")!.textContent = `I accept the service agreement for ${title}.`;
|
|
20
|
+
update();
|
|
21
|
+
checkbox.addEventListener("change", update);
|
|
22
|
+
read.addEventListener("click", () => dialog.showModal());
|
|
23
|
+
form.querySelector("#close-agreement")!.addEventListener("click", () => dialog.close());
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(`${endpoint}/apps/${encodeURIComponent(appId)}`, {signal: AbortSignal.timeout(10000)});
|
|
26
|
+
if (!response.ok) throw new Error("Agreement unavailable");
|
|
27
|
+
const agreement = (await response.json()).app?.agreement;
|
|
28
|
+
if (!agreement || typeof agreement.version !== "string" || !agreement.version || typeof agreement.text !== "string" || !agreement.text) throw new Error("Agreement unavailable");
|
|
29
|
+
if (!form.isConnected) return;
|
|
30
|
+
checkbox.dataset.version = agreement.version;
|
|
31
|
+
checkbox.disabled = false;
|
|
32
|
+
read.disabled = false;
|
|
33
|
+
dialog.querySelector("#agreement-version")!.textContent = `Version ${agreement.version}`;
|
|
34
|
+
dialog.querySelector("#agreement-text")!.textContent = agreement.text;
|
|
35
|
+
status.textContent = "Required to sign in. Optional data choices stay separate.";
|
|
36
|
+
update();
|
|
37
|
+
} catch {
|
|
38
|
+
if (form.isConnected) status.textContent = "The service agreement could not be loaded. Reload this page to try again.";
|
|
39
|
+
}
|
|
40
|
+
}
|