Kundenansicht neu P2: Installationshinweis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3c44aaea77
commit
166188c069
2 files changed
+95
No files matched your search
@@ -0,0 +1,59 @@
|
||||
/* Kundenansicht neu (P2): Hinweis "WEBklar als App" (Logik aus app.js renderInstallOffers). Chrome/Android per Installationsdialog,
|
||||
iPhone über Teilen und Zum Home-Bildschirm. Kein DOM-Zugriff beim Import. */
|
||||
|
||||
import { h } from './dom.js'
|
||||
|
||||
const SCHLUESSEL = 'kb-install-dismissed'
|
||||
|
||||
/** Zeigen nur auf dem Handy, nicht installiert, mit Installationsdialog oder iOS und nicht ausgeblendet. */
|
||||
export function installAngebot({ appLayout, standalone, canPrompt, ios, dismissed }) {
|
||||
const zeigen = Boolean(appLayout) && !standalone && Boolean(canPrompt || ios) && !dismissed
|
||||
return { zeigen, art: zeigen ? (canPrompt ? 'prompt' : 'ios') : null }
|
||||
}
|
||||
|
||||
export function baueInstallHinweis({ art, onInstall, onDismiss }) {
|
||||
const text = art === 'prompt'
|
||||
? 'Mit einem Tipp auf deinem Homescreen – ohne App-Store.'
|
||||
: 'Tippe auf „Teilen“ und dann auf „Zum Home-Bildschirm“.'
|
||||
return h('div', { class: 'kn-install', role: 'region', 'aria-label': 'WEBklar als App' },
|
||||
h('div', { class: 'kn-install-text' }, h('strong', null, 'WEBklar als App'), h('span', null, text)),
|
||||
art === 'prompt' ? h('button', { type: 'button', class: 'kn-knopf', on: { click: () => onInstall?.() } }, 'Installieren') : null,
|
||||
h('button', { type: 'button', class: 'kn-textknopf', 'aria-label': 'Hinweis ausblenden', on: { click: () => onDismiss?.() } }, '×'))
|
||||
}
|
||||
|
||||
const quellen = new WeakMap()
|
||||
|
||||
/** Je Fenster genau eine Quelle: merkt den Installationsdialog (beforeinstallprompt), räumt bei appinstalled. */
|
||||
export function starteInstallQuelle(win = window) {
|
||||
if (quellen.has(win)) return quellen.get(win)
|
||||
const nav = win.navigator ?? {}
|
||||
const ios = /iphone|ipad|ipod/i.test(nav.userAgent ?? '') || (nav.platform === 'MacIntel' && nav.maxTouchPoints > 1)
|
||||
let angebot = null
|
||||
const hoerer = []
|
||||
const melde = () => { for (const fn of [...hoerer]) fn() }
|
||||
const quelle = {
|
||||
ios,
|
||||
standalone: () => Boolean(typeof win.matchMedia === 'function' && win.matchMedia('(display-mode: standalone)')?.matches) || nav.standalone === true,
|
||||
kannPrompt: () => angebot !== null,
|
||||
async installiere() {
|
||||
const p = angebot
|
||||
if (!p) return
|
||||
angebot = null
|
||||
try { await p.prompt() } catch { /* abgebrochen */ }
|
||||
melde()
|
||||
},
|
||||
beiAenderung(fn) { hoerer.push(fn) },
|
||||
}
|
||||
win.addEventListener('beforeinstallprompt', (ev) => { ev.preventDefault?.(); angebot = ev; melde() })
|
||||
win.addEventListener('appinstalled', () => { angebot = null; melde() })
|
||||
quellen.set(win, quelle)
|
||||
return quelle
|
||||
}
|
||||
|
||||
export function istAusgeblendet() {
|
||||
try { return localStorage.getItem(SCHLUESSEL) === '1' } catch { return false }
|
||||
}
|
||||
|
||||
export function merkeAusgeblendet() {
|
||||
try { localStorage.setItem(SCHLUESSEL, '1') } catch { /* ohne Speicher wird der Hinweis wieder gezeigt */ }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { installAngebot, baueInstallHinweis, starteInstallQuelle, istAusgeblendet, merkeAusgeblendet } from '../public/neu/install-hinweis.js'
|
||||
import { mitDom } from './support/knDaten.js'
|
||||
|
||||
const a = (o) => installAngebot({ appLayout: true, standalone: false, canPrompt: false, ios: false, dismissed: false, ...o })
|
||||
test('installAngebot: nur Handy, nicht installiert, mit Prompt oder iOS, nicht ausgeblendet', () => {
|
||||
assert.deepEqual(a({ canPrompt: true }), { zeigen: true, art: 'prompt' })
|
||||
assert.deepEqual(a({ ios: true }), { zeigen: true, art: 'ios' })
|
||||
assert.deepEqual(a({ ios: true, canPrompt: true }), { zeigen: true, art: 'prompt' })
|
||||
for (const o of [{}, { canPrompt: true, standalone: true }, { canPrompt: true, appLayout: false }, { canPrompt: true, dismissed: true }]) assert.equal(a(o).zeigen, false)
|
||||
})
|
||||
test('Hinweis: Texte, Knöpfe, Sperre bei kaputtem localStorage', () => mitDom((fx) => {
|
||||
let n = 0; const zaehle = () => { n += 1 }
|
||||
const ios = baueInstallHinweis({ art: 'ios', onInstall() {}, onDismiss() {} })
|
||||
assert.match(ios.textContent, /WEBklar als App/); assert.match(ios.textContent, /Tippe auf „Teilen“ und dann auf „Zum Home-Bildschirm“\./)
|
||||
const prompt = baueInstallHinweis({ art: 'prompt', onInstall: zaehle, onDismiss: zaehle })
|
||||
assert.match(prompt.textContent, /Mit einem Tipp auf deinem Homescreen – ohne App-Store\./)
|
||||
fx.klick(fx.finde(prompt, (e) => e.textContent === 'Installieren')); fx.klick(fx.finde(prompt, '[aria-label="Hinweis ausblenden"]')); assert.equal(n, 2)
|
||||
globalThis.localStorage = { getItem() { throw new Error('x') }, setItem() { throw new Error('x') } }
|
||||
assert.equal(istAusgeblendet(), false); assert.doesNotThrow(merkeAusgeblendet); delete globalThis.localStorage
|
||||
}))
|
||||
test('Installquelle: beforeinstallprompt merken, installieren, appinstalled räumt', async () => {
|
||||
const handler = {}; const win = { addEventListener: (t, f) => { handler[t] = f }, matchMedia: () => ({ matches: false }), navigator: { userAgent: 'iPhone' } }
|
||||
const q = starteInstallQuelle(win); assert.equal(q.ios, true); assert.equal(starteInstallQuelle(win), q)
|
||||
let prompted = 0; handler.beforeinstallprompt({ preventDefault() {}, prompt: async () => { prompted += 1 } })
|
||||
assert.equal(q.kannPrompt(), true); await q.installiere(); assert.equal(prompted, 1); assert.equal(q.kannPrompt(), false)
|
||||
})
|
||||
test('Installquelle: appinstalled räumt, beiAenderung meldet, fehlendes matchMedia ist false', async () => {
|
||||
const handler = {}; const win = { addEventListener: (t, f) => { handler[t] = f }, navigator: { userAgent: 'Android', standalone: false } }
|
||||
const q = starteInstallQuelle(win); const meldungen = []; q.beiAenderung(() => meldungen.push(q.kannPrompt()))
|
||||
assert.equal(q.ios, false); assert.equal(q.standalone(), false)
|
||||
handler.beforeinstallprompt({ preventDefault() {}, prompt: async () => {} }); assert.equal(q.kannPrompt(), true)
|
||||
handler.appinstalled(); assert.equal(q.kannPrompt(), false); assert.deepEqual(meldungen, [true, false])
|
||||
assert.equal(starteInstallQuelle({ addEventListener() {}, navigator: { userAgent: '', platform: 'MacIntel', maxTouchPoints: 5 } }).ios, true)
|
||||
})
|
||||
Reference in new issue
Block a user