// // SubscribeView.swift // wake // // Created by fairclip on 2025/8/19. // import SwiftUI import StoreKit import Network // MARK: - 订阅计划枚举 enum SubscriptionPlan: String, CaseIterable { case free = "Free" case pioneer = "Pioneer" var displayName: String { return self.rawValue } var price: String { switch self { case .free: return "Free" case .pioneer: return "1$/Mon" } } var isPopular: Bool { return self == .pioneer } } // MARK: - 功能特性结构体 struct SubscriptionFeature { let name: String let freeValue: String let proValue: String } struct SubscribeView: View { @Environment(\.dismiss) private var dismiss @StateObject private var store = IAPManager() @State private var selectedPlan: SubscriptionPlan? = .pioneer @State private var isLoading = false @State private var showErrorAlert = false @State private var errorText = "" @State private var memberProfile: MemberProfile? @State private var showSuccessAlert = false // 功能对比数据 private let features = [ SubscriptionFeature(name: "Mystery Box Purchase:", freeValue: "3 /week", proValue: "Free"), SubscriptionFeature(name: "Material Upload:", freeValue: "50 images and\n5 videos/day", proValue: "Unlimited"), SubscriptionFeature(name: "Free Credits:", freeValue: "200 /day", proValue: "500 /day") ] var body: some View { VStack(spacing: 0) { // 自定义简洁导航头,统一风格 SimpleNaviHeader(title: "Subscription") { dismiss() } .background(Color.themeTextWhiteSecondary) ScrollView { VStack(spacing: 0) { // 当前订阅状态卡片 currentSubscriptionCard // 积分信息 creditsSection VStack { // 订阅计划选择 subscriptionPlansSection // 特别优惠提示 specialOfferBanner } .background(Theme.Colors.cardBackground) .cornerRadius(Theme.CornerRadius.medium) .padding(.horizontal, Theme.Spacing.lg) .padding(.vertical, Theme.Spacing.lg) // 功能对比表 featureComparisonTable // 订阅按钮 subscribeButton // 法律链接 legalLinks Spacer(minLength: 10) } } .background(Theme.Colors.background) } .background(Color.themeTextWhiteSecondary) .navigationBarHidden(true) .onAppear { fetchMemberInfo() } .task { // Load products and refresh current entitlements on appear await store.loadProducts() await store.refreshEntitlements() } .onChange(of: store.isPurchasing) { newValue in // Bind purchasing state to button loading isLoading = newValue } .onChange(of: store.errorMessage) { newValue in if let message = newValue, !message.isEmpty { errorText = message showErrorAlert = true } } .alert("Purchase Error", isPresented: $showErrorAlert) { Button("OK", role: .cancel) { store.errorMessage = nil } } message: { Text(errorText) } .alert("Purchase Success", isPresented: $showSuccessAlert) { Button("OK", role: .cancel) { } } message: { Text("购买成功!") } } // MARK: - 当前订阅状态卡片 private var currentSubscriptionCard: some View { let status: SubscriptionStatus = { if memberProfile?.membershipLevel == "Pioneer" { let dateFormatter = ISO8601DateFormatter() dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] let expiryDate = memberProfile.flatMap { dateFormatter.date(from: $0.membershipEndAt) } ?? Date() return .pioneer(expiryDate: expiryDate) } else { return .free } }() return SubscriptionStatusBar( status: status, onSubscribeTap: { // 订阅操作 handleSubscribe() } ) .padding(.horizontal, Theme.Spacing.xl) } // MARK: - 积分信息 private var creditsSection: some View { VStack(spacing: 16) { CreditsInfoCard( totalCredits: memberProfile?.remainPoints ?? 0, onInfoTap: { // 显示积分信息说明 }, onDetailTap: { // 跳转到积分详情页面 } ) } .padding(.horizontal, Theme.Spacing.xl) .padding(.top, Theme.Spacing.xl) } // MARK: - 订阅计划选择 private var subscriptionPlansSection: some View { PlanSelector( selectedPlan: $selectedPlan, onPlanSelected: { plan in print("Selected plan: \(plan.displayName)") } ) .padding(.horizontal, Theme.Spacing.xl) .padding(.top, Theme.Spacing.xl) } // MARK: - 特别优惠横幅 private var specialOfferBanner: some View { HStack(spacing: 0) { Text("First") .font(Typography.font(for: .footnote, family: .quicksandRegular)) .foregroundColor(Theme.Colors.textPrimary) Text(" 100") .font(Typography.font(for: .footnote, family: .quicksandBold)) .foregroundColor(Theme.Colors.textPrimary) Text(" users get a special deal: justs") .font(Typography.font(for: .footnote, family: .quicksandRegular)) .foregroundColor(Theme.Colors.textPrimary) Text(" $1") .font(Typography.font(for: .footnote, family: .quicksandBold)) .foregroundColor(Theme.Colors.textPrimary) Text(" for your first month!") .font(Typography.font(for: .footnote, family: .quicksandRegular)) .foregroundColor(Theme.Colors.textPrimary) } .multilineTextAlignment(.center) .padding(.horizontal, Theme.Spacing.lg) .padding(.top, Theme.Spacing.sm) .padding(.bottom, Theme.Spacing.lg) } // MARK: - 功能对比表 private var featureComparisonTable: some View { PlanCompare() .padding(.horizontal, Theme.Spacing.lg) } // MARK: - 订阅按钮 private var subscribeButton: some View { VStack(spacing: 12) { SubscribeButton( title: "Subscribe", isLoading: isLoading, subscribed: store.isSubscribed, action: handleSubscribe ) } .padding(.horizontal, Theme.Spacing.xl) .padding(.top, Theme.Spacing.lg) } // MARK: - 法律链接 private var legalLinks: some View { HStack(spacing: 8) { Button(action: { // 打开服务条款 if let url = URL(string: "https://memorywake.com/privacy-policy") { UIApplication.shared.open(url) } }) { Text("Terms of Service") .underline() } Text("|") .foregroundColor(.secondary) Button(action: { // 打开隐私政策 if let url = URL(string: "https://memorywake.com/privacy-policy") { UIApplication.shared.open(url) } }) { Text("Privacy Policy") .underline() } Text("|") .foregroundColor(.secondary) // Button(action: { // Task { await store.restorePurchases() } // }) { // Text("Restore Purchase") // .underline() // } Button(action: { // 打开隐私政策 if let url = URL(string: "https://memorywake.com/privacy-policy") { UIApplication.shared.open(url) } }) { Text("AI Usage Guidelines") .underline() } } .font(Typography.font(for: .caption, family: .quicksandRegular)) .foregroundColor(.secondary) .padding(.top, Theme.Spacing.sm) } // MARK: - 订阅处理 private func handleSubscribe() { isLoading = true Task { do { print("🔄 开始订阅流程...") // 1. 调用后端创建订单 print("🔄 正在创建订单...") let orderInfo = try await createOrder() // 2. 根据创建订单返回的id来调用创建支付接口 print("🔄 正在创建支付...") let paymentInfo = try await createPayment(orderId: orderInfo.id) // 3. 使用订单信息进行应用内购买 print("🔄 开始苹果内购流程...") do { // 发起苹果内购 let transactionId = try await store.purchasePioneer() print("✅ 苹果内购成功,交易ID: \(transactionId)") // 4. 通知服务器支付成功 print("🔄 正在通知服务器支付处理中...") _ = try await notifyPaymentProcessing( transactionId: paymentInfo.transactionId ?? paymentInfo.id, // thirdPartyTransactionId: transactionId ) print("🔄 正在通知服务器支付成功...") _ = try await notifyPaymentSuccess( transactionId: paymentInfo.transactionId ?? paymentInfo.id, // thirdPartyTransactionId: transactionId ) print("✅ 订阅流程完成") // 5. 成功后关闭页面 await MainActor.run { self.isLoading = false self.dismiss() } } catch let purchaseError as NSError { print("❌ 苹果内购失败: \(purchaseError.localizedDescription)") // 通知服务器支付失败 print("🔄 正在通知服务器支付失败...") _ = try? await notifyPaymentFailure( transactionId: paymentInfo.transactionId ?? paymentInfo.id, reason: purchaseError.localizedDescription ) // 重新抛出错误以便外部处理 throw purchaseError } } catch let error as NSError { print("❌ 订阅失败: \(error.localizedDescription)") // 根据错误类型显示不同的错误信息 var errorMessage = error.localizedDescription if error.domain == "NetworkError" { errorMessage = "网络连接失败,请检查您的网络设置" } else if error.domain == "APIError" { errorMessage = "请求失败,请稍后重试 (错误码: \(error.code))" } else if error.domain == NSURLErrorDomain { switch error.code { case NSURLErrorNotConnectedToInternet, NSURLErrorNetworkConnectionLost: errorMessage = "网络连接已断开,请检查您的网络设置" case NSURLErrorTimedOut: errorMessage = "请求超时,请稍后重试" case NSURLErrorCannotConnectToHost, NSURLErrorCannotFindHost: errorMessage = "无法连接到服务器,请稍后重试" default: errorMessage = "网络错误: \(error.localizedDescription)" } } // 在主线程更新UI await MainActor.run { self.isLoading = false self.errorText = errorMessage self.showErrorAlert = true print("❌ 错误提示: \(errorMessage)") } } } } // 创建订单 private func createOrder() async throws -> OrderInfo { return try await withCheckedThrowingContinuation { continuation in let parameters: [String: Any] = [ "items": [ [ "product_item_id": 5, "quantity": 1 ] ] ] print("🔄 开始创建订单请求,参数:\(parameters)") // 检查网络连接 let monitor = NWPathMonitor() let queue = DispatchQueue(label: "NetworkMonitor") monitor.pathUpdateHandler = { path in if path.status == .satisfied { // 网络可用,继续执行网络请求 NetworkService.shared.postWithToken( path: "/order/create", parameters: parameters ) { (result: Result, NetworkError>) in switch result { case .success(let response): print("✅ 请求成功,状态码:\(response.code)") print("📦 返回数据:\(String(describing: response.data))") if response.code == 0 { continuation.resume(returning: response.data) } else { let errorMessage = "创建订单失败,状态码:\(response.code)" print("❌ \(errorMessage)") continuation.resume(throwing: NSError( domain: "APIError", code: response.code, userInfo: [NSLocalizedDescriptionKey: errorMessage] )) } case .failure(let error): print("❌ 请求异常:\(error.localizedDescription)") print("🔍 错误详情:\(error)") if let urlError = error as? URLError { print("🌐 URL错误: \(urlError.code.rawValue) - \(urlError.localizedDescription)") print("🔗 失败URL: \(urlError.failingURL?.absoluteString ?? "未知")") } continuation.resume(throwing: error) } } } else { // 网络不可用,抛出错误 let errorMessage = "网络连接不可用,请检查网络设置" print("❌ \(errorMessage)") continuation.resume(throwing: NSError(domain: "NetworkError", code: 0, userInfo: [NSLocalizedDescriptionKey: errorMessage])) } } monitor.start(queue: queue) } } // 创建支付 private func createPayment(orderId: String) async throws -> PaymentInfo { return try await withCheckedThrowingContinuation { continuation in let parameters: [String: Any] = [ "order_id": orderId, "payment_method": "ApplePay" ] print("🔄 开始创建支付请求,参数:\(parameters)") NetworkService.shared.postWithToken( path: "/order/pay", parameters: parameters ) { (result: Result, NetworkError>) in switch result { case .success(let response): print("✅ 请求成功,状态码:\(response.code)") print("📦 返回数据:\(String(describing: response.data))") if response.code == 0 { continuation.resume(returning: response.data) } else { let errorMessage = "创建支付失败,状态码:\(response.code)" print("❌ \(errorMessage)") continuation.resume(throwing: NSError( domain: "APIError", code: response.code, userInfo: [NSLocalizedDescriptionKey: errorMessage] )) } case .failure(let error): print("❌ 请求异常:\(error.localizedDescription)") print("🔍 错误详情:\(error)") if let urlError = error as? URLError { print("🌐 URL错误: \(urlError.code.rawValue) - \(urlError.localizedDescription)") print("🔗 失败URL: \(urlError.failingURL?.absoluteString ?? "未知")") } continuation.resume(throwing: error) } } } } // MARK: - 支付结果处理 /// 通知服务器支付处理中 /// - Parameter transactionId: 交易ID /// - Parameter thirdPartyTransactionId: 第三方交易ID,可选 private func notifyPaymentProcessing(transactionId: String, thirdPartyTransactionId: String? = nil) async throws -> Bool { return try await withCheckedThrowingContinuation { continuation in var parameters: [String: Any] = ["transaction_id": transactionId] // 只有在提供了第三方交易ID时才添加到参数中 if let thirdPartyId = thirdPartyTransactionId { parameters["third_party_transaction_id"] = thirdPartyId } print("🔄 通知服务器支付处理中,参数:\(parameters)") NetworkService.shared.postWithToken( path: "/order/pay-processing", parameters: parameters ) { (result: Result, NetworkError>) in switch result { case .success(let response): print("✅ 支付处理通知发送成功,状态码:\(response.code)") continuation.resume(returning: response.code == 0) case .failure(let error): print("❌ 支付处理通知发送失败:\(error.localizedDescription)") continuation.resume(throwing: error) } } } } /// 通知服务器支付成功 /// - Parameter transactionId: 交易ID /// - Parameter thirdPartyTransactionId: 第三方交易ID,可选 private func notifyPaymentSuccess(transactionId: String, thirdPartyTransactionId: String? = nil) async throws -> Bool { return try await withCheckedThrowingContinuation { continuation in var parameters: [String: Any] = ["transaction_id": transactionId] // 只有在提供了第三方交易ID时才添加到参数中 if let thirdPartyId = thirdPartyTransactionId { parameters["third_party_transaction_id"] = thirdPartyId } print("🔄 通知服务器支付成功,参数:\(parameters)") NetworkService.shared.postWithToken( path: "/order/pay-success", parameters: parameters ) { (result: Result, NetworkError>) in switch result { case .success(let response): print("✅ 支付成功通知发送成功,状态码:\(response.code)") continuation.resume(returning: response.code == 0) case .failure(let error): print("❌ 支付成功通知发送失败:\(error.localizedDescription)") continuation.resume(throwing: error) } } } } /// 通知服务器支付失败 /// - Parameter transactionId: 交易ID /// - Parameter reason: 失败原因 private func notifyPaymentFailure(transactionId: String, reason: String) async throws -> Bool { return try await withCheckedThrowingContinuation { continuation in let parameters: [String: Any] = [ "transaction_id": transactionId, "reason": reason ] print("🔄 通知服务器支付失败,参数:\(parameters)") NetworkService.shared.postWithToken( path: "/order/pay-failure", parameters: parameters ) { (result: Result, NetworkError>) in switch result { case .success(let response): print("✅ 支付失败通知发送成功,状态码:\(response.code)") continuation.resume(returning: response.code == 0) case .failure(let error): print("❌ 支付失败通知发送失败:\(error.localizedDescription)") continuation.resume(throwing: error) } } } } // MARK: - Helper Methods private func fetchMemberInfo() { NetworkService.shared.get( path: "/membership/personal-center-info", parameters: nil ) { (result: Result, NetworkError>) in DispatchQueue.main.async { switch result { case .success(let response): self.memberProfile = response.data print("✅ 成功获取会员信息:", response.data) print("✅ 用户ID:", response.data.userInfo.userId) print("✅ 用户昵称:", response.data.userInfo.nickname) print("✅ 用户邮箱:", response.data.userInfo.email) case .failure(let error): print("❌ 获取会员信息失败:", error) // Optionally show an error message to the user self.errorText = "Failed to load member info: \(error.localizedDescription)" self.showErrorAlert = true } } } } } #Preview { SubscribeView() }