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() }
}
}