chore: 修改文案

This commit is contained in:
Junhui Chen 2025-08-20 13:44:10 +08:00
parent bae3923475
commit 28a9db04ab
11 changed files with 518 additions and 84 deletions

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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
View 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
View 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
}
}
}

View File

@ -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 {

View File

@ -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(

View File

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

View File

@ -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
//
}
} }
} }