diff --git a/wake.xcodeproj/project.pbxproj b/wake.xcodeproj/project.pbxproj index 7793c77..40f2a91 100644 --- a/wake.xcodeproj/project.pbxproj +++ b/wake.xcodeproj/project.pbxproj @@ -328,6 +328,7 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -362,6 +363,7 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/wake/Models/OrderInfo.swift b/wake/Models/OrderInfo.swift new file mode 100644 index 0000000..7cbca93 --- /dev/null +++ b/wake/Models/OrderInfo.swift @@ -0,0 +1,101 @@ +import Foundation + +/// 订单信息模型 +struct OrderInfo: Codable, Identifiable { + let id: String + let userId: String + let totalAmount: Amount + let status: String + let items: [OrderItem] + let paymentInfo: PaymentInfo? + let createdAt: String + let updatedAt: String + let expiredAt: String + + enum CodingKeys: String, CodingKey { + case id + case userId = "user_id" + case totalAmount = "total_amount" + case status + case items + case paymentInfo = "payment_info" + case createdAt = "created_at" + case updatedAt = "updated_at" + case expiredAt = "expired_at" + } +} + +/// 支付信息模型 +struct PaymentInfo: Codable { + let id: String + let paymentMethod: String + let paymentStatus: String + let paymentAmount: Amount + let transactionId: String? + let thirdPartyTransactionId: String? + let paidAt: String? + let createdAt: String + let updatedAt: String + + enum CodingKeys: String, CodingKey { + case id + case paymentMethod = "payment_method" + case paymentStatus = "payment_status" + case paymentAmount = "payment_amount" + case transactionId = "transaction_id" + case thirdPartyTransactionId = "third_party_transaction_id" + case paidAt = "paid_at" + case createdAt = "created_at" + case updatedAt = "updated_at" + } +} + +/// 金额模型 +struct Amount: Codable { + let amount: String + let currency: String +} + +/// 订单项模型 +struct OrderItem: Codable, Identifiable { + let id: String + let productId: Int + let productType: String + let productCode: String + let productName: String + let unitPrice: Amount + let discountAmount: Amount + let quantity: Int + let totalPrice: Amount + + enum CodingKeys: String, CodingKey { + case id + case productId = "product_id" + case productType = "product_type" + case productCode = "product_code" + case productName = "product_name" + case unitPrice = "unit_price" + case discountAmount = "discount_amount" + case quantity + case totalPrice = "total_price" + } +} + +/// 订单状态 +enum OrderStatus: Int, Codable { + case pending = 0 // 待支付 + case paid = 1 // 已支付 + case completed = 2 // 已完成 + case cancelled = 3 // 已取消 + case refunded = 4 // 已退款 + + var description: String { + switch self { + case .pending: return "待支付" + case .paid: return "已支付" + case .completed: return "已完成" + case .cancelled: return "已取消" + case .refunded: return "已退款" + } + } +} diff --git a/wake/Utils/IAPManager.swift b/wake/Utils/IAPManager.swift index 27fb2c9..e9b8ae2 100644 --- a/wake/Utils/IAPManager.swift +++ b/wake/Utils/IAPManager.swift @@ -34,13 +34,12 @@ final class IAPManager: ObservableObject { } // Trigger App Store purchase sheet - func purchasePioneer() async { - guard !isPurchasing else { return } + func purchasePioneer() async throws -> String { + guard !isPurchasing else { throw NSError(domain: "IAPError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Purchase already in progress"]) } guard let product = pioneerProduct else { - // Surface an actionable error so the UI can inform the user - self.errorMessage = "Subscription product unavailable. Please try again later." - return + throw NSError(domain: "IAPError", code: -2, userInfo: [NSLocalizedDescriptionKey: "Subscription product unavailable"]) } + isPurchasing = true defer { isPurchasing = false } @@ -50,21 +49,26 @@ final class IAPManager: ObservableObject { case .success(let verification): switch verification { case .unverified(_, let error): - self.errorMessage = "Purchase unverified: \(error.localizedDescription)" - case .verified(let transaction): - // Update entitlement for the purchased product + throw error + case .verified(let transaction): + print("🎉 订阅成功!", transaction) + print("🔄 交易验证通过 - ID: \(transaction.id), 原始ID: \(transaction.originalID), 产品ID: \(transaction.productID)") updateEntitlement(from: transaction) + let transactionID = String(transaction.id) + print("📝 使用交易ID: \(transactionID)") await transaction.finish() + return transactionID } case .userCancelled: - break + throw NSError(domain: "IAPError", code: -3, userInfo: [NSLocalizedDescriptionKey: "Purchase was cancelled"]) case .pending: - break + throw NSError(domain: "IAPError", code: -4, userInfo: [NSLocalizedDescriptionKey: "Purchase is pending approval"]) @unknown default: - break + throw NSError(domain: "IAPError", code: -5, userInfo: [NSLocalizedDescriptionKey: "Unknown purchase result"]) } } catch { self.errorMessage = "Purchase failed: \(error.localizedDescription)" + throw error } } diff --git a/wake/View/Subscribe/SubscribeView.swift b/wake/View/Subscribe/SubscribeView.swift index a59a80c..bc3604e 100644 --- a/wake/View/Subscribe/SubscribeView.swift +++ b/wake/View/Subscribe/SubscribeView.swift @@ -7,6 +7,7 @@ import SwiftUI import StoreKit +import Network // MARK: - 订阅计划枚举 enum SubscriptionPlan: String, CaseIterable { @@ -46,6 +47,7 @@ struct SubscribeView: View { @State private var showErrorAlert = false @State private var errorText = "" @State private var memberProfile: MemberProfile? + @State private var showSuccessAlert = false // 功能对比数据 private let features = [ @@ -122,6 +124,11 @@ struct SubscribeView: View { } message: { Text(errorText) } + .alert("Purchase Success", isPresented: $showSuccessAlert) { + Button("OK", role: .cancel) { } + } message: { + Text("购买成功!") + } } // MARK: - 当前订阅状态卡片 @@ -280,7 +287,286 @@ struct SubscribeView: View { // MARK: - 订阅处理 private func handleSubscribe() { - Task { await store.purchasePioneer() } + 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