chore: 修改文案
This commit is contained in:
parent
bae3923475
commit
28a9db04ab
Binary file not shown.
81
wake.xcodeproj/xcshareddata/xcschemes/wake.xcscheme
Normal file
81
wake.xcodeproj/xcshareddata/xcschemes/wake.xcscheme
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1640"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "ABB4E2072E4B75D900660198"
|
||||||
|
BuildableName = "wake.app"
|
||||||
|
BlueprintName = "wake"
|
||||||
|
ReferencedContainer = "container:wake.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "ABB4E2072E4B75D900660198"
|
||||||
|
BuildableName = "wake.app"
|
||||||
|
BlueprintName = "wake"
|
||||||
|
ReferencedContainer = "container:wake.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<StoreKitConfigurationFileReference
|
||||||
|
identifier = "../../wake/MemoWake.storekit">
|
||||||
|
</StoreKitConfigurationFileReference>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "ABB4E2072E4B75D900660198"
|
||||||
|
BuildableName = "wake.app"
|
||||||
|
BlueprintName = "wake"
|
||||||
|
ReferencedContainer = "container:wake.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@ -3,22 +3,4 @@
|
|||||||
uuid = "55F37A93-4556-4005-B9BD-8F1A1D6A8474"
|
uuid = "55F37A93-4556-4005-B9BD-8F1A1D6A8474"
|
||||||
type = "1"
|
type = "1"
|
||||||
version = "2.0">
|
version = "2.0">
|
||||||
<Breakpoints>
|
|
||||||
<BreakpointProxy
|
|
||||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
|
||||||
<BreakpointContent
|
|
||||||
uuid = "F1BBF7E2-4D6E-4646-83BC-F57E600056E4"
|
|
||||||
shouldBeEnabled = "Yes"
|
|
||||||
ignoreCount = "0"
|
|
||||||
continueAfterRunningActions = "No"
|
|
||||||
filePath = "wake/View/Subscribe/SubscribeView.swift"
|
|
||||||
startingColumnNumber = "9223372036854775807"
|
|
||||||
endingColumnNumber = "9223372036854775807"
|
|
||||||
startingLineNumber = "65"
|
|
||||||
endingLineNumber = "65"
|
|
||||||
landmarkName = "body"
|
|
||||||
landmarkType = "24">
|
|
||||||
</BreakpointContent>
|
|
||||||
</BreakpointProxy>
|
|
||||||
</Breakpoints>
|
|
||||||
</Bucket>
|
</Bucket>
|
||||||
|
|||||||
@ -10,5 +10,13 @@
|
|||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
<dict>
|
||||||
|
<key>ABB4E2072E4B75D900660198</key>
|
||||||
|
<dict>
|
||||||
|
<key>primary</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -79,6 +79,17 @@ struct ContentView: View {
|
|||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 订阅测试按钮
|
||||||
|
NavigationLink(destination: SubscribeView()) {
|
||||||
|
Text("Subscribe")
|
||||||
|
.font(.subheadline)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.background(Color.orange)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
.padding(.trailing)
|
.padding(.trailing)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
215
wake/MemoWake.storekit
Normal file
215
wake/MemoWake.storekit
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
125
wake/Utils/IAPManager.swift
Normal file
125
wake/Utils/IAPManager.swift
Normal file
@ -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<T>(_ result: VerificationResult<T>) 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,35 +23,12 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// 自定义导航栏
|
// 简洁导航头
|
||||||
HStack {
|
SimpleNaviHeader(title: "Setting") {
|
||||||
// 返回按钮
|
withAnimation(animation) {
|
||||||
Button(action: {
|
isPresented = false
|
||||||
withAnimation(animation) {
|
|
||||||
isPresented = false
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Image(systemName: "chevron.left")
|
|
||||||
.font(.system(size: 16, weight: .medium))
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 标题
|
|
||||||
Text("Setting")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 用于平衡布局的透明视图
|
|
||||||
Color.clear
|
|
||||||
.frame(width: 44, height: 44)
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal,16)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
|
|
||||||
// 设置项列表
|
// 设置项列表
|
||||||
List {
|
List {
|
||||||
|
|||||||
@ -25,7 +25,7 @@ struct PlanCompare: View {
|
|||||||
title: "Mystery Box Purchase:",
|
title: "Mystery Box Purchase:",
|
||||||
subtitle: nil,
|
subtitle: nil,
|
||||||
freeValue: "3 /week",
|
freeValue: "3 /week",
|
||||||
pioneerValue: "Free",
|
pioneerValue: "Unlimited",
|
||||||
icon: nil
|
icon: nil
|
||||||
),
|
),
|
||||||
PlanFeature(
|
PlanFeature(
|
||||||
|
|||||||
@ -5,15 +5,18 @@ struct SubscribeButton: View {
|
|||||||
let title: String
|
let title: String
|
||||||
let isLoading: Bool
|
let isLoading: Bool
|
||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
|
let subscribed: Bool
|
||||||
|
|
||||||
init(
|
init(
|
||||||
title: String = "Subscribe",
|
title: String = "Subscribe",
|
||||||
isLoading: Bool,
|
isLoading: Bool,
|
||||||
action: @escaping () -> Void
|
subscribed: Bool,
|
||||||
|
action: @escaping () -> Void,
|
||||||
) {
|
) {
|
||||||
self.title = title
|
self.title = title
|
||||||
self.isLoading = isLoading
|
self.isLoading = isLoading
|
||||||
self.action = action
|
self.action = action
|
||||||
|
self.subscribed = subscribed
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -29,17 +32,25 @@ struct SubscribeButton: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
if subscribed {
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(title)
|
Text("Subscribed")
|
||||||
.font(Typography.font(for: .body, family: .quicksandBold))
|
.font(Typography.font(for: .body, family: .quicksandBold))
|
||||||
Spacer()
|
Spacer()
|
||||||
// Fixed subtitle text as requested
|
}
|
||||||
Text("And get 5,000 Permanent Credits")
|
else {
|
||||||
.font(Typography.font(for: .caption, family: .quicksandRegular))
|
Spacer()
|
||||||
.foregroundColor(Theme.Colors.textPrimary)
|
Spacer()
|
||||||
Spacer()
|
Text(title)
|
||||||
Spacer()
|
.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)
|
.frame(height: 56)
|
||||||
@ -54,16 +65,17 @@ struct SubscribeButton: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.disabled(isLoading)
|
.disabled(isLoading || subscribed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("SubscribeButton") {
|
#Preview("SubscribeButton") {
|
||||||
VStack(spacing: Theme.Spacing.xl) {
|
VStack(spacing: Theme.Spacing.xl) {
|
||||||
SubscribeButton(isLoading: false) {}
|
SubscribeButton(isLoading: false, subscribed: false) {}
|
||||||
SubscribeButton(isLoading: true) {}
|
SubscribeButton(isLoading: true, subscribed: false) {}
|
||||||
|
SubscribeButton(isLoading: false, subscribed: true) {}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.background(Theme.Colors.background)
|
.background(Theme.Colors.background)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import StoreKit
|
||||||
|
|
||||||
// MARK: - 订阅计划枚举
|
// MARK: - 订阅计划枚举
|
||||||
enum SubscriptionPlan: String, CaseIterable {
|
enum SubscriptionPlan: String, CaseIterable {
|
||||||
@ -38,9 +39,13 @@ struct SubscriptionFeature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct SubscribeView: View {
|
struct SubscribeView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@StateObject private var store = IAPManager()
|
||||||
@State private var selectedPlan: SubscriptionPlan? = .pioneer
|
@State private var selectedPlan: SubscriptionPlan? = .pioneer
|
||||||
@State private var isLoading = false
|
@State private var isLoading = false
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@State private var showErrorAlert = false
|
||||||
|
@State private var errorText = ""
|
||||||
|
|
||||||
|
|
||||||
// 功能对比数据
|
// 功能对比数据
|
||||||
private let features = [
|
private let features = [
|
||||||
@ -50,12 +55,14 @@ struct SubscribeView: View {
|
|||||||
]
|
]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
VStack(spacing: 0) {
|
||||||
|
// 自定义简洁导航头,统一风格
|
||||||
|
SimpleNaviHeader(title: "Subscription") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// 导航栏
|
|
||||||
navigationHeader
|
|
||||||
|
|
||||||
// 当前订阅状态卡片
|
// 当前订阅状态卡片
|
||||||
currentSubscriptionCard
|
currentSubscriptionCard
|
||||||
|
|
||||||
@ -88,21 +95,42 @@ struct SubscribeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(Theme.Colors.background)
|
.background(Theme.Colors.background)
|
||||||
.navigationBarHidden(true)
|
|
||||||
}
|
}
|
||||||
}
|
.navigationBarHidden(true)
|
||||||
|
.task {
|
||||||
// MARK: - 导航栏
|
// Load products and refresh current entitlements on appear
|
||||||
private var navigationHeader: some View {
|
await store.loadProducts()
|
||||||
NaviHeader(title: "Subscription") {
|
await store.refreshEntitlements()
|
||||||
presentationMode.wrappedValue.dismiss()
|
}
|
||||||
|
.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: - 当前订阅状态卡片
|
// MARK: - 当前订阅状态卡片
|
||||||
private var currentSubscriptionCard: some View {
|
private var currentSubscriptionCard: some View {
|
||||||
SubscriptionStatusBar(
|
let status: SubscriptionStatus = {
|
||||||
status: .pioneer(expiryDate: Date()) ,
|
if store.isSubscribed {
|
||||||
|
return .pioneer(expiryDate: store.subscriptionExpiry ?? Date())
|
||||||
|
} else {
|
||||||
|
return .free
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return SubscriptionStatusBar(
|
||||||
|
status: status,
|
||||||
onSubscribeTap: {
|
onSubscribeTap: {
|
||||||
// 订阅操作
|
// 订阅操作
|
||||||
handleSubscribe()
|
handleSubscribe()
|
||||||
@ -182,6 +210,7 @@ struct SubscribeView: View {
|
|||||||
SubscribeButton(
|
SubscribeButton(
|
||||||
title: "Subscribe",
|
title: "Subscribe",
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
|
subscribed: store.isSubscribed,
|
||||||
action: handleSubscribe
|
action: handleSubscribe
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -213,7 +242,7 @@ struct SubscribeView: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
// 恢复购买
|
Task { await store.restorePurchases() }
|
||||||
}) {
|
}) {
|
||||||
Text("Restore Purchase")
|
Text("Restore Purchase")
|
||||||
.underline()
|
.underline()
|
||||||
@ -226,13 +255,7 @@ struct SubscribeView: View {
|
|||||||
|
|
||||||
// MARK: - 订阅处理
|
// MARK: - 订阅处理
|
||||||
private func handleSubscribe() {
|
private func handleSubscribe() {
|
||||||
isLoading = true
|
Task { await store.purchasePioneer() }
|
||||||
|
|
||||||
// 模拟订阅处理
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
|
||||||
isLoading = false
|
|
||||||
// 处理订阅逻辑
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user