diff --git a/ylgamehall/Resources/ChannelConfig.plist b/ylgamehall/Resources/ChannelConfig.plist index 416ad9b..c7bd05c 100644 --- a/ylgamehall/Resources/ChannelConfig.plist +++ b/ylgamehall/Resources/ChannelConfig.plist @@ -17,7 +17,7 @@ agent veRa0qrBf0df2K1G4de2tgfmVxB2jxpv appversion - 43 + 44 other appleconfig diff --git a/ylgamehall/Source/WebView/WebContainerViewController.swift b/ylgamehall/Source/WebView/WebContainerViewController.swift index d98d8b1..91b4c30 100644 --- a/ylgamehall/Source/WebView/WebContainerViewController.swift +++ b/ylgamehall/Source/WebView/WebContainerViewController.swift @@ -194,6 +194,21 @@ public final class WebContainerViewController: UIViewController { // 异步 snapshot NWPath 区分"真断网"和"权限被拒",文案和按钮语义按 kind 精确分流。 let path = await Self.currentPathSnapshot() let kind = Self.classifyBootError(error, path: path) + + // 首次启动权限授予竞争:iOS 弹权限对话框时,pipeline 的第一个网络请求 + // 已经在跑;如果用户没在请求 timeout 前点"允许",请求会 fail 且 NWPath + // 报 wifiDenied / cellularDenied —— 但用户可能"正准备点允许"。此时立 + // 即弹"前往设置"会让用户困惑("我明明点了允许还弹")。 + // + // 策略:只在 kind.isPermissionDenied 时,短暂观察 NWPath;5s 内若变 + // satisfied → 静默重跑 pipeline(等价用户点允许后我们自动重试);超时 + // 或仍拒绝 → 走原逻辑弹"前往设置"。真拒绝的长尾场景多等 5s 可接受。 + if kind.isPermissionDenied, + await Self.waitForNetworkPermissionResolution(timeout: 5.0) { + await runBootPipeline() + return + } + presentBootError(error, kind: kind) } } @@ -698,6 +713,34 @@ public final class WebContainerViewController: UIViewController { monitor.start(queue: DispatchQueue.global(qos: .userInitiated)) } } + + /// 等首次启动权限对话框有决定 —— 最长 `timeout` 秒内观察 NWPath 是否变 + /// `.satisfied`。用于消除"用户点允许太慢就弹前往设置"的误报体验。 + /// 返回 true 表示已授权(可静默重试 pipeline);false 表示超时或明确拒绝 + /// (应走原逻辑弹前往设置)。 + private static func waitForNetworkPermissionResolution(timeout: TimeInterval) async -> Bool { + await withCheckedContinuation { (cont: CheckedContinuation) in + let monitor = NWPathMonitor() + nonisolated(unsafe) var resumed = false + + DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { + guard !resumed else { return } + resumed = true + monitor.cancel() + cont.resume(returning: false) + } + + monitor.pathUpdateHandler = { path in + guard !resumed else { return } + if path.status == .satisfied { + resumed = true + monitor.cancel() + cont.resume(returning: true) + } + } + monitor.start(queue: DispatchQueue.global(qos: .userInitiated)) + } + } } // MARK: - WKNavigationDelegate