From 140275022d979c281b2770a2da3cd3d97a9fa9ca Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 21:45:15 +0000 Subject: [PATCH] =?UTF-8?q?Kundenansicht=20neu=20E-Fix=20M6:=20Zahlungskn?= =?UTF-8?q?=C3=B6pfe=20sind=20nach=20der=20R=C3=BCckkehr=20von=20Stripe=20?= =?UTF-8?q?wieder=20bedienbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nach "Zahlungspflichtig bestellen" setzt die Seite den Knopf auf "Weiter zur Zahlung…" (gesperrt) und leitet zu Stripe. Bricht der Kunde per Zurück-Wischgeste ab, stellt Safari die Seite aus dem Bfcache her: der Knopf blieb gesperrt, ein neuer Versuch ging nur nach Neuladen. Nur ansicht-wechsel.js hatte einen pageshow-Handler. Neuer Helfer beiRueckkehr(el, fn) in ansicht-hilfen.js (pageshow mit persisted, entfernt sich, sobald das Element nicht mehr im Dokument steht); genutzt von der Bestellseite und von "Zahlungsart aktualisieren" (Verträge). Speicher und Gutschrift leiten nicht zu Stripe, dort ist nichts zu tun. Co-Authored-By: Claude Sonnet 5.5 --- public/neu/ansicht-bestellen.js | 5 ++++- public/neu/ansicht-hilfen.js | 15 +++++++++++++++ public/neu/vertraege-abo.js | 3 ++- test/knAnsichtAbo.test.js | 2 ++ test/knAnsichtBestellen.test.js | 15 +++++++++++++++ 5 files changed, 38 insertions(+), 2 deletions(-) diff --git a/public/neu/ansicht-bestellen.js b/public/neu/ansicht-bestellen.js index 8b7b917..9ee9233 100644 --- a/public/neu/ansicht-bestellen.js +++ b/public/neu/ansicht-bestellen.js @@ -9,7 +9,7 @@ import { api } from './api.js' import { h, leeren } from './dom.js' -import { K, ladeAbo, sichereAdresse, meldeTitel, fmtEuro, fmtDatum, ABO_NICHT_GELADEN } from './ansicht-hilfen.js' +import { K, ladeAbo, sichereAdresse, meldeTitel, beiRueckkehr, fmtEuro, fmtDatum, ABO_NICHT_GELADEN } from './ansicht-hilfen.js' import { baueBlock, baueEinzelheiten, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon, leistungLink } from './bestell-bausteine.js' const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs @@ -114,6 +114,9 @@ function bestellung(daten, productKey, { viewAs, kundenart }) { baueKundenartZeile(kundenart, T.kundenart, { id: 'abo-kundenart' }), einzelheiten) + // Zurück von der Zahlungsseite (Bfcache): der Knopf ist wieder bedienbar (E-Fix M6, wie ansicht-wechsel.js) + beiRueckkehr(abschnitt, () => { kaufen.disabled = viewAs; kaufen.textContent = T.knopf || KNOPF }) + // Das Häkchen schaltet die Zeilen „Fällig“ und „Beginn“ um haken?.addEventListener('change', () => { const neu = faelligBeginn({ lage: haken.checked ? 'wunsch' : 'frist', erste, frist }) diff --git a/public/neu/ansicht-hilfen.js b/public/neu/ansicht-hilfen.js index fa3bdf2..d57f12e 100644 --- a/public/neu/ansicht-hilfen.js +++ b/public/neu/ansicht-hilfen.js @@ -146,6 +146,21 @@ export function meldeTitel(el, text) { el.dispatchEvent(new CustomEvent('kn:titel', { bubbles: true, detail: { text } })) } +/** + * Zurück aus dem Bfcache (E-Fix M6): kommt der Kunde von der Zahlungsseite von Stripe per Wischgeste zurück, stellt Safari die Seite so wieder her, + * wie sie war (Knopf gesperrt, "Weiter zur Zahlung…"). fn läuft bei pageshow mit persisted, solange `el` im Dokument steht; ist es ersetzt worden, + * entfernt sich der Hörer. Ohne Fenster tut es nichts. + */ +export function beiRueckkehr(el, fn) { + const win = globalThis.window + if (!win || typeof win.addEventListener !== 'function') return + const hoerer = (ev) => { + if (!el.isConnected) { win.removeEventListener('pageshow', hoerer); return } + if (ev.persisted) fn() + } + win.addEventListener('pageshow', hoerer) +} + /** Zähler neu laden lassen: was = 'chat' | 'vertraege' | 'alle'. Ohne Dokument tut es nichts. */ export function meldeZaehlerNeu(was) { if (typeof document === 'undefined' || typeof document.dispatchEvent !== 'function' || typeof CustomEvent === 'undefined') return diff --git a/public/neu/vertraege-abo.js b/public/neu/vertraege-abo.js index 55b6dab..86e35a6 100644 --- a/public/neu/vertraege-abo.js +++ b/public/neu/vertraege-abo.js @@ -3,7 +3,7 @@ import { api } from './api.js' import { h, leeren } from './dom.js' -import { K, zuAnsicht, fmtEuro, fmtDatum, fmtTagMonat, sichereAdresse } from './ansicht-hilfen.js' +import { K, zuAnsicht, fmtEuro, fmtDatum, fmtTagMonat, sichereAdresse, beiRueckkehr } from './ansicht-hilfen.js' const laeufe = new WeakMap() // Reiter-Inhalt → Nummer des jüngsten Aufrufs const TEXT_UST = 'Gemäß § 19 UStG ohne Umsatzsteuer.' @@ -70,6 +70,7 @@ function zahlungsKnopf(viewAs) { knopf.disabled = false } }) + beiRueckkehr(knopf, () => { knopf.disabled = viewAs }) // zurück von Stripe (Bfcache): wieder bedienbar (E-Fix M6) return [knopf, viewAs ? h('span', { class: K.meldung }, TEXT_ANSICHTSMODUS) : null, fehler] // ohne Umhüllung: der Knopf ist das Element mit diesem Text } diff --git a/test/knAnsichtAbo.test.js b/test/knAnsichtAbo.test.js index b01833f..e275d1f 100644 --- a/test/knAnsichtAbo.test.js +++ b/test/knAnsichtAbo.test.js @@ -26,6 +26,8 @@ test('Zahlungsproblem: Knopf öffnet das Stripe-Portal; Widerruf nur in der Fris const daten = aboAntwort({ subscription: abo({ paymentProblem: true, withdrawalOpen: true, withdrawalUntil: '2026-10-17T21:59:00.000Z' }) }) const b = await zeige(fx, daten); assert.match(fx.text(b), /Bitte aktualisiere deine Zahlungsart, damit deine Website online bleibt\./); assert.match(fx.finde(b, 'a[href="/widerrufen.html"]').textContent, /^Vertrag widerrufen · bis 17\.10\./) await fx.klick(fx.finde(b, (e) => e.textContent === 'Zahlungsart aktualisieren')); assert.equal(fx.window.location.href, 'https://billing.stripe.com/x') + const portal = fx.finde(b, (e) => e.textContent === 'Zahlungsart aktualisieren'); assert.equal(portal.hasAttribute('disabled'), true, 'während der Weiterleitung gesperrt') + fx.window.dispatchEvent({ type: 'pageshow', persisted: true }); assert.equal(portal.hasAttribute('disabled'), false, 'M6: zurück von Stripe wieder bedienbar') const ansicht = await zeige(fx, daten, me({ viewAs: true })); assert.equal(fx.finde(ansicht, (e) => e.textContent === 'Zahlungsart aktualisieren').hasAttribute('disabled'), true) assert.equal(fx.finde(await zeige(fx, WARTUNG), 'a[href="/widerrufen.html"]'), null) })) diff --git a/test/knAnsichtBestellen.test.js b/test/knAnsichtBestellen.test.js index 0f7fc88..129ff0e 100644 --- a/test/knAnsichtBestellen.test.js +++ b/test/knAnsichtBestellen.test.js @@ -99,6 +99,21 @@ test('Privat und leer: ein Häkchen aus dem Wortlaut, ungehakt nach der Frist, g } })) +test('M6: zurück von der Zahlungsseite (Wischgeste, Bfcache) ist der Knopf wieder bedienbar; ein später entfernter Knopf bekommt nichts mehr', mit({ 'POST /api/abo/checkout': { url: 'https://checkout.stripe.test/x' } }, async (fx) => { + const w = await zeige(fx, aboNeu(), meMit('privat')) + const knopf = fx.finde(w, '#abo-buy') + await fx.klick(knopf); await fx.ruhe() + assert.deepEqual([knopf.hasAttribute('disabled'), knopf.textContent], [true, 'Weiter zur Zahlung…']) + fx.window.dispatchEvent({ type: 'pageshow', persisted: false }); assert.equal(knopf.hasAttribute('disabled'), true, 'ein normales Laden (nicht persisted) ändert nichts') + fx.window.dispatchEvent({ type: 'pageshow', persisted: true }) + assert.deepEqual([knopf.hasAttribute('disabled'), knopf.textContent], [false, 'Zahlungspflichtig bestellen']) + // der Knopf kann wieder bestellen + await fx.klick(knopf); await fx.ruhe(); assert.equal(fx.fetchLog.filter((a) => a.url === '/api/abo/checkout').length, 2) + // die Seite wurde ersetzt: der Hörer entfernt sich und fasst den alten Knopf nicht mehr an + w.replaceChildren(); knopf.disabled = true + fx.window.dispatchEvent({ type: 'pageshow', persisted: true }); assert.equal(knopf.hasAttribute('disabled'), true) +})) + test('Der Body enthält nie kundenart oder startChoice', mit({ 'POST /api/abo/checkout': { url: 'https://checkout.stripe.test/x' } }, async (fx) => { for (const m of [meMit('unternehmen'), meMit('privat'), meMit('')]) { const w = await zeige(fx, aboNeu(), m)