600 lines
24 KiB
Swift
600 lines
24 KiB
Swift
//
|
||
// 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: 100)
|
||
}
|
||
}
|
||
.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<APIResponse<OrderInfo>, 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<APIResponse<PaymentInfo>, 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<APIResponse<[String: String]?>, 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<APIResponse<[String: String]?>, 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<APIResponse<[String: String]?>, 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<APIResponse<MemberProfile>, 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()
|
||
}
|