mana-swift-ui/Sources/ManaWebShell/WebShellConfig.swift
Till JS 8f4d4b0c03 feat(webshell): neues Library-Product ManaWebShell (v0.6.0)
WKWebView-Huelle fuer Hybrid-Apps (Web-Lese-Surfaces + native
Submit/Widget/ShareExt). Extrahiert aus den fast-byte-identischen
`WebShell/`-Ordnern in seepuls-native und zitare-native (~900 LOC,
davon ~700 LOC Duplikat). Audit 2026-05-17 V2.

Neu (public API):
- `WebShellView` — WKWebView-Wrapper mit Progress-Bar, Pull-to-
  Refresh (iOS), Fehler-Snackbar, External-Link-Delegation. Universal
  (iOS + macOS)
- `WebShellConfig` — Host-Whitelist mit Wildcard-Support (`"*.mana.how"`),
  User-Agent, Theme-Hints, User-Scripts
- `WebTarget` — URL + monoton wachsender reloadToken
- `WebNavState` — @Observable, @MainActor, reaktiver Nav-State
- `WebShellCoordinator` — WKNavigationDelegate + WKUIDelegate
- `WebShellScripts` — Helfer fuer `preferDarkScheme`,
  `syncDarkMode(localStorageKey:)`, `hideElements(selectors:tagName:)`

Logging unter Subsystem `ev.mana.webshell` (App-OSLog bleibt eigen).

Tests: 6 neue Tests gegen `WebShellConfig.isAllowed` (Wildcards,
Negativ-Cases). 50/50 grün insgesamt (6 ManaWebShell + 44 ManaAuthUI).

Doku: `mana/docs/playbooks/HYBRID_NATIVE_APP.md` (Schwester-Repo).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:11:47 +02:00

94 lines
3.4 KiB
Swift

import Foundation
import SwiftUI
import WebKit
/// Konfiguration für ``WebShellView``.
///
/// Beispiel:
///
/// ```swift
/// WebShellView(
/// target: WebTarget(url: URL(string: "https://seepuls.mana.how")!),
/// config: WebShellConfig(
/// allowedHosts: ["seepuls.mana.how", "*.mana.how", "mana.how"],
/// userAgent: "SeepulsNative/0.1 (iOS)"
/// )
/// )
/// ```
///
/// Apps mit eigenem Theme injizieren `background` / `progressTint` /
/// `warning` etc. default werden System-Farben benutzt.
public struct WebShellConfig: Sendable {
/// Liste erlaubter Hosts. Unterstützt:
/// - exakte Hosts: `"seepuls.mana.how"`
/// - Wildcard-Subdomains: `"*.mana.how"`
///
/// Pfade auf nicht-gelisteten Hosts werden via `OpenURLAction` an
/// den System-Browser delegiert. Ein leeres Array bedeutet
/// **alles extern** selten gewünscht, aber explizit erlaubt.
public let allowedHosts: [String]
/// `applicationNameForUserAgent`. WKWebView hängt das an seinen
/// Standard-UA an, ersetzt ihn nicht. Konvention im mana-Ökosystem:
/// `"<AppName>Native/<version> (<platform>)"`.
public let userAgent: String
/// Hintergrund hinter dem WKWebView (verhindert Flash vor first
/// paint). Default: `.clear`. Caller setzt typischerweise auf
/// App-Theme-Background.
public let backgroundColor: Color
/// Tint der Fortschritts-Linie oben (Linear-ProgressView). Default:
/// `.accentColor`.
public let progressTint: Color
/// Hintergrund der Fehler-Snackbar. Default: `.gray.opacity(0.15)`.
public let errorBackgroundColor: Color
/// Vordergrund der Fehler-Snackbar (Icon + Text). Default: `.primary`.
public let errorForegroundColor: Color
/// Icon-Farbe (Warn-Dreieck) in der Fehler-Snackbar. Default: `.orange`.
public let errorIconColor: Color
/// User-Scripts, die in `WKUserContentController` injiziert werden
/// (Reihenfolge bleibt erhalten). Häufig genutzt: Theme-Sync,
/// Web-Nav-Verstecken. Siehe ``WebShellScripts`` für Default-Helfer.
public let userScripts: [WKUserScript]
public init(
allowedHosts: [String],
userAgent: String,
backgroundColor: Color = .clear,
progressTint: Color = .accentColor,
errorBackgroundColor: Color = Color.gray.opacity(0.15),
errorForegroundColor: Color = .primary,
errorIconColor: Color = .orange,
userScripts: [WKUserScript] = []
) {
self.allowedHosts = allowedHosts
self.userAgent = userAgent
self.backgroundColor = backgroundColor
self.progressTint = progressTint
self.errorBackgroundColor = errorBackgroundColor
self.errorForegroundColor = errorForegroundColor
self.errorIconColor = errorIconColor
self.userScripts = userScripts
}
/// Prüft, ob ein Host in dieser Konfiguration erlaubt ist.
/// Unterstützt `*.<root>`-Wildcards (subdomain-suffix + Root selbst).
public func isAllowed(host: String) -> Bool {
for pattern in allowedHosts {
if pattern.hasPrefix("*.") {
let suffix = String(pattern.dropFirst(1)) // ".mana.how"
if host.hasSuffix(suffix) { return true }
let root = String(suffix.dropFirst(1)) // "mana.how"
if host == root { return true }
} else if host == pattern {
return true
}
}
return false
}
}