修复合作获客扫码绑定崩溃与 Toast 层级,并接入分成比例修改。

将扫码页提升到列表层弹出并补齐 AppDelegate.window,避免扫码时 UIKit 取主窗口崩溃;Toast 改用独立 UIWindow 显示在 sheet 之上;合作获客员支持短信验证修改分成比例,同时优化冷启动根视图创建时机并补充相关测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-03 20:37:24 +08:00
parent 00c3cd0a93
commit 49e997ddba
20 changed files with 905 additions and 70 deletions

View File

@ -7,6 +7,7 @@
import Combine
import SwiftUI
import UIKit
@MainActor
/// Toast
@ -62,26 +63,75 @@ final class ToastCenter: ObservableObject {
#endif
}
/// Toast Toast
private struct GlobalToastOverlay: ViewModifier {
@EnvironmentObject private var toastCenter: ToastCenter
/// Toast 宿 sheet / fullScreenCover present
@MainActor
private final class ToastWindowPresenter {
static let shared = ToastWindowPresenter()
func body(content: Content) -> some View {
content
.overlay {
Group {
if let message = toastCenter.message {
toastBanner(message)
.transition(.opacity)
}
}
.allowsHitTesting(false)
}
.animation(.easeInOut(duration: 0.18), value: toastCenter.message)
private var toastWindow: UIWindow?
/// UIWindow Toast
func show(message: String) {
guard let windowScene = resolveWindowScene() else { return }
let window: UIWindow
if let existing = toastWindow, existing.windowScene === windowScene {
window = existing
} else {
let newWindow = PassthroughWindow(windowScene: windowScene)
newWindow.windowLevel = .alert + 1
newWindow.backgroundColor = .clear
toastWindow = newWindow
window = newWindow
}
let hosting = UIHostingController(
rootView: ToastBannerHostView(message: message)
)
hosting.view.backgroundColor = .clear
window.rootViewController = hosting
window.isHidden = false
}
/// Toast
private func toastBanner(_ message: String) -> some View {
/// Toast
func hide() {
toastWindow?.isHidden = true
toastWindow?.rootViewController = nil
}
/// window scene Toast
private func resolveWindowScene() -> UIWindowScene? {
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
return scenes.first { $0.activationState == .foregroundActive } ?? scenes.first
}
}
/// Toast
private final class PassthroughWindow: UIWindow {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let view = super.hitTest(point, with: event) else { return nil }
return view === rootViewController?.view ? nil : view
}
}
/// Toast
private struct ToastBannerHostView: View {
let message: String
var body: some View {
ZStack {
ToastBannerContent(message: message)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.allowsHitTesting(false)
}
}
/// Toast
private struct ToastBannerContent: View {
let message: String
var body: some View {
HStack(spacing: 10) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 16, weight: .semibold))
@ -97,6 +147,31 @@ private struct GlobalToastOverlay: ViewModifier {
.padding(.vertical, 14)
.background(Color.black.opacity(0.78), in: RoundedRectangle(cornerRadius: 12))
.padding(.horizontal, 40)
.accessibilityIdentifier("global.toast")
}
}
/// Toast ToastCenter
private struct GlobalToastOverlay: ViewModifier {
@EnvironmentObject private var toastCenter: ToastCenter
func body(content: Content) -> some View {
content
.onAppear {
syncToastWindow(with: toastCenter.message)
}
.onChange(of: toastCenter.message) { message in
syncToastWindow(with: message)
}
}
/// Toast
private func syncToastWindow(with message: String?) {
if let message {
ToastWindowPresenter.shared.show(message: message)
} else {
ToastWindowPresenter.shared.hide()
}
}
}