Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/js/legal.js

7.2 KiB, 1 run

created by r2519314175:1393, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1/* legal.js — the Terms and the Privacy Policy, reachable from inside the app.
2 *
3 * Daimond published both documents at daimond.app and then shipped an app on
4 * another origin that never mentioned them. `daimond.oxedyne.com/terms.html`
5 * is a 404, so the only way a user could read what they were agreeing to was
6 * to know that a second website existed and go there. A consent line with
7 * nowhere to point is not consent.
8 *
9 * A PWA TAB DRAWS ITSELF. The fix is not a link to daimond.app in a new tab:
10 * it is the documents, rendered by Daimond, in Daimond's own panel. They are
11 * generated into `www/guide/legal/` from the landing pages by
12 * dev/legal-pages.mjs — one source of prose, two renditions, and a verifier
13 * that fails if they drift — and shown through the machinery the user guide
14 * already uses: `DaimondWeb.guide(sub)`, which loads a page of our own site
15 * into the Web panel's frame. Nothing new was invented to show them.
16 *
17 * WHERE THIS IS REACHED FROM, today, in the shipped app:
18 *
19 * - The About dialog, whose foot is where the small print already lives.
20 * That row is added from here rather than written into the dialog, so the
21 * module that draws About does not have to know the documents exist.
22 * - The lapse notice (js/lapse.js), whose whole subject is a term of the
23 * contract, and which says which clause it is quoting.
24 *
25 * And `link()` exists for the one caller that is not built yet: the beta
26 * passcode's consent line. It hands back an anchor that opens the document in
27 * the panel, so that line can be written without another way out of the app.
28 */
29(function () {
30 'use strict';
31
32 /// What the app says, falling back to English while a key has no translation.
33 /// The twin of `tOr` in daimond.js: this file is a classic script and cannot
34 /// reach into that closure.
35 ///
36 /// `DaimondI18n.t` answers with the KEY when the table has no entry, which
37 /// would put `legal.terms` on screen. These strings are new and no locale
38 /// table has them yet, so each carries the English it means.
39 function t(k, fallback) {
40 var i18n = window.DaimondI18n;
41 if (i18n && i18n.has && i18n.has(k)) return i18n.t(k);
42 return fallback;
43 }
44
45 /// The two documents, and the page each is generated to. The path is relative
46 /// to `guide/`, because that is what `DaimondWeb.guide` prefixes.
47 var DOCS = {
48 terms: { sub: 'legal/terms.html', key: 'legal.terms', en: 'Terms of Service' },
49 privacy: { sub: 'legal/privacy.html', key: 'legal.privacy', en: 'Privacy Policy' },
50 };
51
52 /// What a document is called, in the reader's language.
53 function title(which) {
54 var d = DOCS[which];
55 return d ? t(d.key, d.en) : '';
56 }
57
58 /// Show one of the documents in the Web panel.
59 ///
60 /// `anchor` is a section id from the page itself — `terms.html#five-years`,
61 /// say — so a notice can put the reader on the clause it is talking about
62 /// rather than at the top of a twelve-page document.
63 ///
64 /// Returns true when the panel took it. The fallback opens the same file in
65 /// a tab of THIS origin, which is what the header's guide button does when
66 /// the web module is absent; it is still Daimond's own page.
67 function open(which, anchor) {
68 var d = DOCS[which];
69 if (!d) return false;
70 var sub = d.sub + (anchor ? '#' + String(anchor).replace(/^#/, '') : '');
71 if (window.DaimondWeb && DaimondWeb.guide) {
72 DaimondWeb.guide(sub);
73 return true;
74 }
75 try { window.open('guide/' + sub, '_blank', 'noopener'); } catch (e) { /* blocked */ }
76 return false;
77 }
78
79 /// An anchor that opens a document in the panel.
80 ///
81 /// A real `href`, so it can be copied, opened in a tab by a reader who wants
82 /// one, and read by a screen reader as the link it is — but an ordinary click
83 /// is taken by the panel instead, which is the whole point.
84 function link(which, words, anchor) {
85 var d = DOCS[which];
86 var a = document.createElement('a');
87 a.className = 'legal-link';
88 a.href = d ? ('guide/' + d.sub + (anchor ? '#' + String(anchor).replace(/^#/, '') : '')) : '#';
89 a.textContent = words || title(which);
90 a.addEventListener('click', function (e) {
91 // A modified click is the reader asking for a tab. Let the browser
92 // have it; everything else belongs in the panel.
93 if (e.metaKey || e.ctrlKey || e.shiftKey || e.button) return;
94 e.preventDefault();
95 open(which, anchor);
96 });
97 return a;
98 }
99
100 // ── The row in About ────────────────────────────────────────────
101 //
102 // About is where the app already says what it is, which build it is and who
103 // made it. The two documents belong in that company, and putting them there
104 // costs no new surface: no menu entry, no panel, nothing on screen until
105 // somebody asks what this is.
106 //
107 // It is added by watching for the dialog rather than by editing the module
108 // that draws it, so the two stay independent — About knows nothing about the
109 // legal pages, and this file knows only the class name of the foot it sits
110 // above. If that class ever changes the row is simply not added, and
111 // dev/verify_legalreach.mjs fails, which is the point of asserting it there.
112
113 /// Put the row into an About dialog that has just opened.
114 function decorate(card) {
115 var body = card.querySelector('.about-body');
116 if (!body || body.querySelector('.about-legal')) return;
117
118 var row = document.createElement('div');
119 row.className = 'about-legal';
120
121 var links = document.createElement('div');
122 links.className = 'about-legal-links';
123 links.appendChild(link('terms'));
124 var dot = document.createElement('span');
125 dot.className = 'about-legal-sep';
126 dot.textContent = '·';
127 dot.setAttribute('aria-hidden', 'true');
128 links.appendChild(dot);
129 links.appendChild(link('privacy'));
130 row.appendChild(links);
131
132 var note = document.createElement('p');
133 note.className = 'about-legal-note';
134 note.textContent = t('legal.draft_note',
135 'In force from 13 August 2026. Points marked [TO CONFIRM] are still being settled.');
136 row.appendChild(note);
137
138 // Above the maker's signature, which is the last thing in the card.
139 var maker = body.querySelector('.about-maker');
140 if (maker) body.insertBefore(row, maker);
141 else body.appendChild(row);
142 }
143
144 /// Watch for the About dialog. Cheap: one observer on `body`'s own children,
145 /// which change only when a dialog or a toast is put up.
146 function watch() {
147 var here = document.querySelector('.about-card');
148 if (here) decorate(here);
149 if (!window.MutationObserver) return;
150 new MutationObserver(function (recs) {
151 for (var i = 0; i < recs.length; i++) {
152 var added = recs[i].addedNodes;
153 for (var j = 0; j < added.length; j++) {
154 var n = added[j];
155 if (!n || n.nodeType !== 1) continue;
156 var card = n.classList && n.classList.contains('about-card')
157 ? n : (n.querySelector ? n.querySelector('.about-card') : null);
158 if (card) decorate(card);
159 }
160 }
161 }).observe(document.body, { childList: true });
162 }
163
164 if (document.readyState === 'loading') {
165 document.addEventListener('DOMContentLoaded', watch);
166 } else {
167 watch();
168 }
169
170 window.DaimondLegal = {
171 open: open,
172 link: link,
173 title: title,
174 /// Which documents there are, for a caller that offers both.
175 docs: function () { return Object.keys(DOCS); },
176 };
177})();