diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate index b3d4d60..754cc0d 100644 Binary files a/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate and b/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/wake.xcodeproj/xcshareddata/xcschemes/wake.xcscheme b/wake.xcodeproj/xcshareddata/xcschemes/wake.xcscheme new file mode 100644 index 0000000..13c5de3 --- /dev/null +++ b/wake.xcodeproj/xcshareddata/xcschemes/wake.xcscheme @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wake.xcodeproj/xcuserdata/fairclip.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/wake.xcodeproj/xcuserdata/fairclip.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 0ff5029..0ed7045 100644 --- a/wake.xcodeproj/xcuserdata/fairclip.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/wake.xcodeproj/xcuserdata/fairclip.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -3,22 +3,4 @@ uuid = "55F37A93-4556-4005-B9BD-8F1A1D6A8474" type = "1" version = "2.0"> - - - - - - diff --git a/wake.xcodeproj/xcuserdata/fairclip.xcuserdatad/xcschemes/xcschememanagement.plist b/wake.xcodeproj/xcuserdata/fairclip.xcuserdatad/xcschemes/xcschememanagement.plist index a4c98a3..76572c9 100644 --- a/wake.xcodeproj/xcuserdata/fairclip.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/wake.xcodeproj/xcuserdata/fairclip.xcuserdatad/xcschemes/xcschememanagement.plist @@ -10,5 +10,13 @@ 0 + SuppressBuildableAutocreation + + ABB4E2072E4B75D900660198 + + primary + + + diff --git a/wake/ContentView.swift b/wake/ContentView.swift index 976c126..2d9ecaf 100644 --- a/wake/ContentView.swift +++ b/wake/ContentView.swift @@ -79,6 +79,17 @@ struct ContentView: View { .foregroundColor(.white) .cornerRadius(8) } + + // 订阅测试按钮 + NavigationLink(destination: SubscribeView()) { + Text("Subscribe") + .font(.subheadline) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.orange) + .foregroundColor(.white) + .cornerRadius(8) + } .padding(.trailing) } diff --git a/wake/MemoWake.storekit b/wake/MemoWake.storekit new file mode 100644 index 0000000..1434950 --- /dev/null +++ b/wake/MemoWake.storekit @@ -0,0 +1,215 @@ +{ + "appPolicies" : { + "eula" : "", + "policies" : [ + { + "locale" : "en_US", + "policyText" : "", + "policyURL" : "" + } + ] + }, + "identifier" : "C75471B9", + "nonRenewingSubscriptions" : [ + + ], + "products" : [ + + ], + "settings" : { + "_applicationInternalID" : "6748205761", + "_developerTeamID" : "392N3QB7XR", + "_failTransactionsEnabled" : false, + "_lastSynchronizedDate" : 777364219.49411595, + "_locale" : "en_US", + "_storefront" : "USA", + "_storeKitErrors" : [ + { + "current" : null, + "enabled" : false, + "name" : "Load Products" + }, + { + "current" : null, + "enabled" : false, + "name" : "Purchase" + }, + { + "current" : null, + "enabled" : false, + "name" : "Verification" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Store Sync" + }, + { + "current" : null, + "enabled" : false, + "name" : "Subscription Status" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Transaction" + }, + { + "current" : null, + "enabled" : false, + "name" : "Manage Subscriptions Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Refund Request Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Offer Code Redeem Sheet" + } + ] + }, + "subscriptionGroups" : [ + { + "id" : "21759571", + "localizations" : [ + + ], + "name" : "Membership", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "0.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "6751260055", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "The Pioneer Plan unlocks many restrictions.", + "displayName" : "Pioneer Plan", + "locale" : "en_US" + }, + { + "description" : "先锋计划用户,不限制盲盒购买数量,不限制回忆上传数量,每天免费获取500积分", + "displayName" : "先锋计划", + "locale" : "zh_Hans" + } + ], + "productID" : "MEMBERSHIP_PIONEER_MONTHLY", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "Pioneer计划", + "subscriptionGroupID" : "21759571", + "type" : "RecurringSubscription", + "winbackOffers" : [ + + ] + } + ] + }, + { + "id" : "21740727", + "localizations" : [ + + ], + "name" : "Pro会员", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "12.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "6749133482", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "Pro会员每月有更高的存储空间与积分数量", + "displayName" : "季度Pro会员", + "locale" : "zh_Hans" + } + ], + "productID" : "MEMBERSHIP_PRO_QUARTERLY", + "recurringSubscriptionPeriod" : "P3M", + "referenceName" : "季度Pro会员", + "subscriptionGroupID" : "21740727", + "type" : "RecurringSubscription", + "winbackOffers" : [ + + ] + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "59.99", + "familyShareable" : false, + "groupNumber" : 2, + "internalID" : "6749229999", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "Pro会员每月有更高的存储空间与积分数量", + "displayName" : "年度Pro会员", + "locale" : "zh_Hans" + } + ], + "productID" : "MEMBERSHIP_PRO_YEARLY", + "recurringSubscriptionPeriod" : "P1Y", + "referenceName" : "年度Pro会员", + "subscriptionGroupID" : "21740727", + "type" : "RecurringSubscription", + "winbackOffers" : [ + + ] + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "3.99", + "familyShareable" : false, + "groupNumber" : 3, + "internalID" : "6749230171", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "Pro会员每月有更高的存储空间与积分数量", + "displayName" : "月度Pro会员", + "locale" : "zh_Hans" + } + ], + "productID" : "MEMBERSHIP_PRO_MONTH", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "月度Pro会员", + "subscriptionGroupID" : "21740727", + "type" : "RecurringSubscription", + "winbackOffers" : [ + + ] + } + ] + } + ], + "version" : { + "major" : 4, + "minor" : 0 + } +} diff --git a/wake/Utils/IAPManager.swift b/wake/Utils/IAPManager.swift new file mode 100644 index 0000000..27fb2c9 --- /dev/null +++ b/wake/Utils/IAPManager.swift @@ -0,0 +1,125 @@ +import Foundation +import StoreKit + +@MainActor +final class IAPManager: ObservableObject { + @Published var isPurchasing: Bool = false + @Published var pioneerProduct: Product? + @Published var errorMessage: String? + @Published var isSubscribed: Bool = false + @Published var subscriptionExpiry: Date? = nil + + // Product IDs from App Store Connect + private let productIDs: [String] = [ + "MEMBERSHIP_PIONEER_MONTHLY" + ] + + init() { + Task { await observeTransactions() } + } + + // Load products defined in App Store Connect + func loadProducts() async { + do { + let products = try await Product.products(for: productIDs) + // You can refine selection logic if you have multiple tiers + self.pioneerProduct = products.first + if products.isEmpty { + // No products found is a common setup issue (App Store Connect, StoreKit config, or bundle ID) + self.errorMessage = "No subscription products found. Please try again later." + } + } catch { + self.errorMessage = "Failed to load products: \(error.localizedDescription)" + } + } + + // Trigger App Store purchase sheet + func purchasePioneer() async { + guard !isPurchasing else { return } + 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 + } + isPurchasing = true + defer { isPurchasing = false } + + do { + let result = try await product.purchase() + switch result { + 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 + updateEntitlement(from: transaction) + await transaction.finish() + } + case .userCancelled: + break + case .pending: + break + @unknown default: + break + } + } catch { + self.errorMessage = "Purchase failed: \(error.localizedDescription)" + } + } + + // Restore purchases (sync entitlements) + func restorePurchases() async { + do { + try await AppStore.sync() + } catch { + self.errorMessage = "Restore failed: \(error.localizedDescription)" + } + } + + // Observe transaction updates for entitlement changes + private func observeTransactions() async { + for await result in Transaction.updates { + do { + let transaction: Transaction = try checkVerified(result) + // Update entitlement state for transaction.productID + updateEntitlement(from: transaction) + await transaction.finish() + } catch { + // Ignore unverified transactions + } + } + } + + // Check current entitlements (useful on launch) + func refreshEntitlements() async { + for await result in Transaction.currentEntitlements { + if case .verified(let transaction) = result, + productIDs.contains(transaction.productID) { + updateEntitlement(from: transaction) + } + } + } + + // Helper: verify + private func checkVerified(_ result: VerificationResult) throws -> T { + switch result { + case .unverified(_, let error): + throw error + case .verified(let safe): + return safe + } + } + + private func updateEntitlement(from transaction: Transaction) { + guard productIDs.contains(transaction.productID) else { return } + // For auto-renewable subs, use expirationDate and revocationDate + if transaction.revocationDate == nil { + self.isSubscribed = true + self.subscriptionExpiry = transaction.expirationDate + } else { + self.isSubscribed = false + self.subscriptionExpiry = nil + } + } +} diff --git a/wake/View/Owner/SettingsView.swift b/wake/View/Owner/SettingsView.swift index b068b2a..d2abf64 100644 --- a/wake/View/Owner/SettingsView.swift +++ b/wake/View/Owner/SettingsView.swift @@ -23,35 +23,12 @@ struct SettingsView: View { var body: some View { VStack(spacing: 0) { - // 自定义导航栏 - HStack { - // 返回按钮 - Button(action: { - withAnimation(animation) { - isPresented = false - } - }) { - HStack(spacing: 4) { - Image(systemName: "chevron.left") - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.blue) - } + // 简洁导航头 + SimpleNaviHeader(title: "Setting") { + withAnimation(animation) { + isPresented = false } - - Spacer() - - // 标题 - Text("Setting") - .font(.headline) - - Spacer() - - // 用于平衡布局的透明视图 - Color.clear - .frame(width: 44, height: 44) } - .padding(.horizontal,16) - .padding(.vertical, 8) // 设置项列表 List { diff --git a/wake/View/Subscribe/Components/PlanCompare.swift b/wake/View/Subscribe/Components/PlanCompare.swift index 77fc88c..92b3380 100644 --- a/wake/View/Subscribe/Components/PlanCompare.swift +++ b/wake/View/Subscribe/Components/PlanCompare.swift @@ -25,7 +25,7 @@ struct PlanCompare: View { title: "Mystery Box Purchase:", subtitle: nil, freeValue: "3 /week", - pioneerValue: "Free", + pioneerValue: "Unlimited", icon: nil ), PlanFeature( diff --git a/wake/View/Subscribe/Components/SubscribeButton.swift b/wake/View/Subscribe/Components/SubscribeButton.swift index 307cdad..15ad9c7 100644 --- a/wake/View/Subscribe/Components/SubscribeButton.swift +++ b/wake/View/Subscribe/Components/SubscribeButton.swift @@ -5,15 +5,18 @@ struct SubscribeButton: View { let title: String let isLoading: Bool let action: () -> Void + let subscribed: Bool init( title: String = "Subscribe", isLoading: Bool, - action: @escaping () -> Void + subscribed: Bool, + action: @escaping () -> Void, ) { self.title = title self.isLoading = isLoading self.action = action + self.subscribed = subscribed } var body: some View { @@ -29,17 +32,25 @@ struct SubscribeButton: View { } VStack { - Spacer() - Spacer() - Text(title) - .font(Typography.font(for: .body, family: .quicksandBold)) - Spacer() - // Fixed subtitle text as requested - Text("And get 5,000 Permanent Credits") - .font(Typography.font(for: .caption, family: .quicksandRegular)) - .foregroundColor(Theme.Colors.textPrimary) - Spacer() - Spacer() + if subscribed { + Spacer() + Text("Subscribed") + .font(Typography.font(for: .body, family: .quicksandBold)) + Spacer() + } + else { + Spacer() + Spacer() + Text(title) + .font(Typography.font(for: .body, family: .quicksandBold)) + Spacer() + // Fixed subtitle text as requested + Text("And get 5,000 Permanent Credits") + .font(Typography.font(for: .caption, family: .quicksandRegular)) + .foregroundColor(Theme.Colors.textPrimary) + Spacer() + Spacer() + } } } .frame(height: 56) @@ -54,16 +65,17 @@ struct SubscribeButton: View { ) } .buttonStyle(.plain) - .disabled(isLoading) + .disabled(isLoading || subscribed) } } } #Preview("SubscribeButton") { VStack(spacing: Theme.Spacing.xl) { - SubscribeButton(isLoading: false) {} - SubscribeButton(isLoading: true) {} + SubscribeButton(isLoading: false, subscribed: false) {} + SubscribeButton(isLoading: true, subscribed: false) {} + SubscribeButton(isLoading: false, subscribed: true) {} } .padding() .background(Theme.Colors.background) -} \ No newline at end of file +} diff --git a/wake/View/Subscribe/SubscribeView.swift b/wake/View/Subscribe/SubscribeView.swift index d90c682..fb3966b 100644 --- a/wake/View/Subscribe/SubscribeView.swift +++ b/wake/View/Subscribe/SubscribeView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import StoreKit // MARK: - 订阅计划枚举 enum SubscriptionPlan: String, CaseIterable { @@ -38,9 +39,13 @@ struct SubscriptionFeature { } struct SubscribeView: View { + @Environment(\.dismiss) private var dismiss + @StateObject private var store = IAPManager() @State private var selectedPlan: SubscriptionPlan? = .pioneer @State private var isLoading = false - @Environment(\.presentationMode) var presentationMode + @State private var showErrorAlert = false + @State private var errorText = "" + // 功能对比数据 private let features = [ @@ -50,12 +55,14 @@ struct SubscribeView: View { ] var body: some View { - NavigationView { + VStack(spacing: 0) { + // 自定义简洁导航头,统一风格 + SimpleNaviHeader(title: "Subscription") { + dismiss() + } + ScrollView { VStack(spacing: 0) { - // 导航栏 - navigationHeader - // 当前订阅状态卡片 currentSubscriptionCard @@ -88,21 +95,42 @@ struct SubscribeView: View { } } .background(Theme.Colors.background) - .navigationBarHidden(true) } - } - - // MARK: - 导航栏 - private var navigationHeader: some View { - NaviHeader(title: "Subscription") { - presentationMode.wrappedValue.dismiss() + .navigationBarHidden(true) + .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) } } // MARK: - 当前订阅状态卡片 private var currentSubscriptionCard: some View { - SubscriptionStatusBar( - status: .pioneer(expiryDate: Date()) , + let status: SubscriptionStatus = { + if store.isSubscribed { + return .pioneer(expiryDate: store.subscriptionExpiry ?? Date()) + } else { + return .free + } + }() + + return SubscriptionStatusBar( + status: status, onSubscribeTap: { // 订阅操作 handleSubscribe() @@ -182,6 +210,7 @@ struct SubscribeView: View { SubscribeButton( title: "Subscribe", isLoading: isLoading, + subscribed: store.isSubscribed, action: handleSubscribe ) } @@ -213,7 +242,7 @@ struct SubscribeView: View { .foregroundColor(.secondary) Button(action: { - // 恢复购买 + Task { await store.restorePurchases() } }) { Text("Restore Purchase") .underline() @@ -226,13 +255,7 @@ struct SubscribeView: View { // MARK: - 订阅处理 private func handleSubscribe() { - isLoading = true - - // 模拟订阅处理 - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - isLoading = false - // 处理订阅逻辑 - } + Task { await store.purchasePioneer() } } }