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 | })(); |