Files
location-spoofer/App/FirstSetupView.swift
T

297 lines
12 KiB
Swift

import SwiftUI
enum SetupStep: Int, CaseIterable {
case proxy
case cert
var title: String {
switch self {
case .proxy: return "配置 Wi-Fi 代理"
case .cert: return "初始化 CA 证书"
}
}
}
struct FirstSetupView: View {
@ObservedObject var setup: SetupCoordinator
let onComplete: () -> Void
@State private var step: SetupStep
@State private var downloadedDone = false
@State private var installedDone = false
@State private var trustedDone = false
@State private var result: VerificationResult?
@State private var isVerifying = false
@State private var manualHint = ""
@State private var showDiagnostics = false
@StateObject private var diagnosticActions = LocationActionCoordinator()
init(setup: SetupCoordinator, onComplete: @escaping () -> Void) {
self.setup = setup
self.onComplete = onComplete
_step = State(initialValue: setup.setupStep)
}
private var certificateStepsComplete: Bool { downloadedDone && installedDone && trustedDone }
private var diagnosticFavorite: FavoriteLocation {
FavoriteLocation(name: "诊断位置", latitude: 22.544577, longitude: 113.94114, accuracy: 25)
}
var body: some View {
NavigationView {
VStack(spacing: 0) {
progress
ScrollView {
VStack(alignment: .leading, spacing: 20) {
switch step {
case .proxy: proxyStep
case .cert: certificateStep
}
if let result { resultView(result) }
}
.padding(20)
}
Divider()
primaryAction
.padding(.horizontal, 20)
.padding(.vertical, 12)
}
.navigationTitle("开始使用")
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $showDiagnostics) {
NavigationView {
RuntimeLogsView(
setup: setup,
actions: diagnosticActions,
testFavorite: diagnosticFavorite
)
}
}
.alert("无法直接跳转", isPresented: Binding(
get: { !manualHint.isEmpty },
set: { if !$0 { manualHint = "" } }
)) {
Button("知道了", role: .cancel) {}
} message: { Text(manualHint) }
}
}
private var progress: some View {
HStack(spacing: 8) {
ForEach(SetupStep.allCases, id: \.rawValue) { value in
HStack(spacing: 6) {
Circle()
.fill(value.rawValue <= step.rawValue ? Color.blue : Color.gray.opacity(0.3))
.frame(width: 10, height: 10)
Text(value.title).font(.caption).foregroundStyle(.secondary)
}
if value != SetupStep.allCases.last {
Rectangle().fill(Color.gray.opacity(0.3)).frame(width: 28, height: 2)
}
}
}
.padding(.vertical, 16)
}
private var proxyStep: some View {
VStack(alignment: .leading, spacing: 16) {
GroupBox(label: Label("先配置 Wi-Fi 系统代理", systemImage: "wifi")) {
Text("在当前 Wi-Fi 的详情页,将「HTTP 代理」设为「手动」:服务器填 127.0.0.1,端口填 8888。完成后点击下方「我已配置,开始检测」。检测会自动判断是 Wi-Fi 代理还是证书信任有问题。")
.font(.caption).foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 4)
}
HStack(spacing: 12) {
Button { UIPasteboard.general.string = "127.0.0.1:8888" } label: {
Label("复制地址", systemImage: "doc.on.doc").frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
Button { openSettings(.wifi) } label: {
Label("打开 Wi-Fi 设置", systemImage: "gearshape").frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
}
}
}
private var certificateStep: some View {
VStack(alignment: .leading, spacing: 20) {
certificateCard(
title: "第 1 步:下载证书",
icon: "arrow.down.circle",
description: "下载本机随机生成的 CA 根证书。私钥仅保存在此设备的钥匙串中,不会随证书文件导出。Safari 出现配置描述文件下载提示时,选择「允许」。",
actionTitle: "去下载",
actionIcon: "arrow.down.circle.fill",
complete: downloadedDone,
action: { Task { await setup.proxy.openCertificateDownload() } },
markComplete: { downloadedDone = true }
)
certificateCard(
title: "第 2 步:安装证书",
icon: "square.and.arrow.down",
description: "下载完成后打开系统「设置」。如果顶部显示「已下载描述文件」,点进去安装;否则进入「通用 → VPN 与设备管理」,找到 WLOC CA 并完成安装。",
actionTitle: "去安装",
actionIcon: "gearshape",
complete: installedDone,
action: { openSettings(.general) },
markComplete: { installedDone = true }
)
certificateCard(
title: "第 3 步:信任证书",
icon: "shield.checkered",
description: "安装后进入「设置 → 通用 → 关于本机 → 证书信任设置」,找到 WLOC CA 并开启完全信任。iOS 保留钥匙串数据时,重装 App 会继续复用同一证书。",
actionTitle: "去信任",
actionIcon: "shield.checkered",
complete: trustedDone,
action: { openSettings(.general) },
markComplete: { trustedDone = true }
)
}
}
private func certificateCard(
title: String,
icon: String,
description: String,
actionTitle: String,
actionIcon: String,
complete: Bool,
action: @escaping () -> Void,
markComplete: @escaping () -> Void
) -> some View {
GroupBox(label: Label(title, systemImage: icon)) {
VStack(alignment: .leading, spacing: 12) {
Color.clear.frame(height: 0).padding(.top, 2)
Text(description)
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 10) {
Button(action: action) {
Label(actionTitle, systemImage: actionIcon).frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.tint(.blue)
Button(action: markComplete) {
Label(
complete ? "已完成 ✓" : "已完成",
systemImage: complete ? "checkmark.circle.fill" : "circle"
)
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.tint(complete ? .green : .secondary)
}
}
}
}
@ViewBuilder
private func resultView(_ result: VerificationResult) -> some View {
let success = result.isSuccess
VStack(alignment: .leading, spacing: 10) {
Label(success ? "环境检测通过" : failureSummary(result), systemImage: success ? "checkmark.circle.fill" : "xmark.circle.fill")
.foregroundStyle(success ? .green : .red)
.font(.subheadline.weight(.semibold))
if !success {
Text(setup.testLog).font(.caption.monospaced()).textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(8)
Button {
showDiagnostics = true
} label: {
Label("查看诊断日志", systemImage: "doc.text.magnifyingglass")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
}
}
.padding(12)
.background((success ? Color.green : Color.red).opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
}
@ViewBuilder
private var primaryAction: some View {
if step == .proxy {
Button {
verifyAfterProxyConfirmation()
} label: {
actionLabel("我已配置,开始检测")
}
.buttonStyle(.borderedProminent)
.disabled(isVerifying)
} else {
Button {
verifyAfterCertificateConfirmation()
} label: {
actionLabel("确认完成,重新检测")
}
.buttonStyle(.borderedProminent)
.disabled(!certificateStepsComplete || isVerifying)
}
}
private func actionLabel(_ title: String) -> some View {
HStack {
if isVerifying { ProgressView().tint(.white).controlSize(.small) }
Text(title).frame(maxWidth: .infinity)
}
}
private func verifyAfterProxyConfirmation() {
runVerification { result in
if result.isSuccess {
onComplete()
} else if result == .certNotTrusted {
step = .cert
} else {
step = .proxy
}
}
}
private func verifyAfterCertificateConfirmation() {
runVerification { result in
if result.isSuccess {
onComplete()
} else if result != .certNotTrusted {
step = .proxy
}
}
}
private func runVerification(completion: @escaping (VerificationResult) -> Void) {
guard !isVerifying else { return }
isVerifying = true
result = nil
Task {
let verification = await setup.runVerificationTest()
setup.applyVerificationResult(verification)
guard !Task.isCancelled else { return }
result = verification
isVerifying = false
completion(verification)
}
}
private func failureSummary(_ result: VerificationResult) -> String {
switch result {
case .certNotTrusted: return "证书尚未安装或信任"
case .wifiProxyNotConfigured: return "Wi-Fi 代理未正确设置"
case .proxyNotRunning: return "本地代理未能启动"
case .verificationInProgress: return "检测仍在进行"
case .verificationSuperseded: return "检测结果已过期"
case .coordinateWriteFailed: return "坐标写入失败"
case .patchFailed: return "定位改写检测失败"
case .success: return "环境检测通过"
}
}
@MainActor
private func openSettings(_ destination: SystemSettingsDestination) {
SystemSettingsNavigator.open(destination) { fallbackHint in
if let fallbackHint { manualHint = fallbackHint }
}
}
}