/** * Das Unterstützungsangebot – eine Bitte, kein Handel. * * ## Eine Adresse, und leer heißt lautlos * * {@link UNTERSTUETZUNG_URL} ist die **einzige** Stelle, an der die Adresse * steht. Ist sie leer, lässt die Anwendung das Angebot vollständig weg: keine * Überschrift, kein Knopf, keine leere Zeile. Ein Knopf, der ins Nichts führt, * wäre schlimmer als kein Knopf – er sagt zu, dass es dort weitergeht. Wer den * Dienst wechselt oder das Angebot zurücknehmen will, ändert diese eine * Zeile. * * Gewählt ist Ko-fi. Die Form bleibt geprüft: `https:`, keine Zugangsdaten * in der Adresse, sonst gilt sie als nicht gesetzt. Ein Wechsel des Dienstes * ist auch später eine Zeile – die Adresse steht nur hier, und * `main/sicherheit.ts` liest sie von dieser Stelle. * * ## Warum es nichts freischaltet * * Die Anwendung soll in den Microsoft Store. Dessen Richtlinien lassen bei * kostenlosen Anwendungen einen Verweis auf eine Spendenseite zu, verlangen * aber, dass dahinter **keine digitalen Güter und keine Funktionen verkauft** * werden. Deshalb schaltet dieser Knopf nichts frei: keinen Pro-Modus, keine * Zusatzfunktion, keine Werbefreiheit – es gibt hier keine Werbung, die man * abstellen könnte. Wer nichts gibt, hat dieselbe Anwendung wie alle anderen. * Das ist nicht bloß eine Store-Auflage, sondern der Grund, warum das Angebot * überhaupt vertretbar ist: Eine Prüfungsvorbereitung, die einen Teil ihres * Stoffs hinter eine Zahlung legt, benachteiligt genau die Prüflinge, die * ohnehin am wenigsten übrig haben. * * ## Warum es sich nicht von selbst meldet * * `docs/entscheidung-motivation.md` hält fest, warum es hier keinen Streak * und keinen Tagesziel-Ring gibt: Diese Anwendung erzeugt keinen Druck, um * Verhalten zu erzeugen. Für eine Spendenbitte gilt dasselbe, nur schärfer – * sie hat ein Eigeninteresse. Deshalb gibt es **kein** Einblenden nach der * zehnten Frage, keinen Zähler „schon 300 Fragen gelernt“, keine Erinnerung, * kein zweites Fragen. Das Angebot steht an einer Stelle, an der ohnehin * nachsieht, wer wissen will, woher diese Anwendung kommt und was sie kostet: * in „Über diese Software“, unterhalb der Lizenzangaben – und ausdrücklich * **nicht** vor dem Rückmeldeweg, der dort zuoberst steht. Wer die Ansicht * öffnet, weil er auf eine Barriere gestoßen ist, soll nicht zuerst um Geld * gebeten werden. * * ## Warum die Anwendung dabei keine Verbindung öffnet * * „Diese Anwendung läuft vollständig offline“ ist eine Zusage, die in „Über * diese Software“ ausdrücklich steht. Der Knopf bricht sie nicht: Er lädt * nichts, prüft nichts nach und meldet nichts – er übergibt die Adresse an * den Hauptprozess, und der reicht sie mit `shell.openExternal` an den * Standardbrowser weiter. Das Fenster verlässt dabei niemand unangekündigt: * {@link UNTERSTUETZUNG_KNOPF} sagt es in der Beschriftung (WCAG 3.2). * * Was über die Brücke geht, wird trotzdem geprüft – `main/sicherheit.ts` * vergleicht die gewünschte Adresse mit genau dieser Konstanten und öffnet * sonst nichts. Eine Adresse mitschicken zu dürfen heißt sonst, auch eine * andere mitschicken zu dürfen. */ /** * Adresse der Unterstützungsseite. **Leer = kein Angebot.** * * Muss mit `https://` beginnen; alles andere behandelt * {@link unterstuetzungsziel} wie „nicht gesetzt“. Wird zugleich von * `main/sicherheit.ts` als einzige zulässige Zieladresse gelesen – eine * Änderung hier verschiebt also auch, was die Brücke überhaupt öffnen darf. */ export const UNTERSTUETZUNG_URL = 'https://ko-fi.com/olafwillerding'; /** Überschrift des Blocks in „Über diese Software“. */ export const UNTERSTUETZUNG_TITEL = 'Diese Arbeit unterstützen'; /** * Beschriftung des Knopfes. * * Sie sagt beides: was geschieht und **dass das Fenster verlassen wird**. Ein * Verweis, der unangekündigt den Browser startet, ist genau die Überraschung, * die WCAG 3.2 vermeiden will. Die sichtbare Beschriftung ist zugleich der * zugängliche Name – es gibt kein `aria-label`, das daneben laufen könnte * (WCAG 2.5.3). */ export const UNTERSTUETZUNG_KNOPF = 'Unterstützungsseite im Browser öffnen'; /** * Die geprüfte Adresse, oder `null`, wenn keine gesetzt ist. * * Eine reine Funktion mit ausdrücklichem Eingabewert, damit beide Seiten * dasselbe rechnen und beide Fälle prüfbar sind – auch der, den es heute noch * nicht gibt. * * Abgelehnt wird alles, was kein `https:` ist. `http:` fiele in der * Auslieferung an unbekannte Empfänger auf; `mailto:`, `javascript:` und * Ähnliches haben an dieser Stelle nichts verloren. Zugangsdaten in der * Adresse (`https://name:wort@…`) gelten ebenfalls als Fehleintrag: Sie * gehören in keinen Verweis, den jemand im Browser sieht. * * Ein Vertipper führt damit zu demselben Ergebnis wie ein leeres Feld – das * Angebot entfällt. Eine Fehlermeldung an dieser Stelle wäre für den * Lernenden ein Mangel der Anwendung, obwohl es einer des Eintrags ist. */ export function unterstuetzungsziel(url: string): string | null { const adresse = url.trim(); if (adresse.length === 0) { return null; } let geprueft: URL; try { geprueft = new URL(adresse); } catch { return null; } if (geprueft.protocol !== 'https:') { return null; } /* Hier stand zusätzlich eine Prüfung auf einen leeren Rechnernamen. Sie ist entfernt, weil sie nicht auslösen kann: Für die besonderen Schemata – `https:` gehört dazu – verlangt der URL-Standard einen Rechnernamen. Nachgemessen mit Node: `https://` wirft `ERR_INVALID_URL` (dann greift der `catch` oben), und `https:///pfad` liefert `pfad` als Rechnernamen. Eine Bedingung, die nie zutrifft, behauptet einen Schutz, den es nicht gibt – dieselbe Überlegung wie bei den stummen Wachen in `sicherheit.test.ts`. */ if (geprueft.username.length > 0 || geprueft.password.length > 0) { return null; } /* Zurück kommt die eingetragene Schreibweise und nicht `geprueft.href`: Angezeigt und geöffnet wird so dieselbe Zeichenkette, und der Vergleich im Hauptprozess trifft genau das, was auf dem Bildschirm stand. */ return adresse; }