Merge branch 'V2.0.0' of https://git.fairclip.cn/FairClip/wake-ios into V2.0.0
This commit is contained in:
commit
08db3dc287
29
wake/Models/SubscriptionPlan.swift
Normal file
29
wake/Models/SubscriptionPlan.swift
Normal file
@ -0,0 +1,29 @@
|
||||
import Foundation
|
||||
|
||||
struct SubscriptionPlan: Hashable, Identifiable {
|
||||
let id: String
|
||||
let displayName: String
|
||||
let price: String
|
||||
let isPopular: Bool
|
||||
|
||||
static let free = SubscriptionPlan(
|
||||
id: "free",
|
||||
displayName: "Free",
|
||||
price: "Free",
|
||||
isPopular: false
|
||||
)
|
||||
|
||||
static let pioneer = SubscriptionPlan(
|
||||
id: "pioneer",
|
||||
displayName: "Pioneer",
|
||||
price: "$0.99/month",
|
||||
isPopular: true
|
||||
)
|
||||
|
||||
init(id: String, displayName: String, price: String, isPopular: Bool = false) {
|
||||
self.id = id
|
||||
self.displayName = displayName
|
||||
self.price = price
|
||||
self.isPopular = isPopular
|
||||
}
|
||||
}
|
||||
29
wake/SubscriptionPlan.swift
Normal file
29
wake/SubscriptionPlan.swift
Normal file
@ -0,0 +1,29 @@
|
||||
import Foundation
|
||||
|
||||
struct SubscriptionPlan: Hashable, Identifiable {
|
||||
let id: String
|
||||
let displayName: String
|
||||
let price: String
|
||||
let isPopular: Bool
|
||||
|
||||
static let free = SubscriptionPlan(
|
||||
id: "free",
|
||||
displayName: "Free",
|
||||
price: "Free",
|
||||
isPopular: false
|
||||
)
|
||||
|
||||
static let pioneer = SubscriptionPlan(
|
||||
id: "pioneer",
|
||||
displayName: "Pioneer",
|
||||
price: "$0.99/month",
|
||||
isPopular: true
|
||||
)
|
||||
|
||||
init(id: String, displayName: String, price: String, isPopular: Bool = false) {
|
||||
self.id = id
|
||||
self.displayName = displayName
|
||||
self.price = price
|
||||
self.isPopular = isPopular
|
||||
}
|
||||
}
|
||||
@ -6,33 +6,48 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import StoreKit
|
||||
|
||||
// MARK: - 计划选择器组件
|
||||
struct PlanSelector: View {
|
||||
@Binding var selectedPlan: SubscriptionPlan?
|
||||
let onPlanSelected: (SubscriptionPlan) -> Void
|
||||
|
||||
private let plans: [SubscriptionPlan] = [.free, .pioneer]
|
||||
|
||||
init(
|
||||
selectedPlan: Binding<SubscriptionPlan?>,
|
||||
onPlanSelected: @escaping (SubscriptionPlan) -> Void = { _ in }
|
||||
) {
|
||||
self._selectedPlan = selectedPlan
|
||||
self.onPlanSelected = onPlanSelected
|
||||
}
|
||||
@Binding var selectedPlan: WakeSubscriptionPlan?
|
||||
@StateObject private var store = SubscriptionStore()
|
||||
let onPlanSelected: (WakeSubscriptionPlan) -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: Theme.Spacing.md) {
|
||||
ForEach(plans, id: \.self) { plan in
|
||||
PlanCard(
|
||||
plan: plan,
|
||||
isSelected: selectedPlan == plan,
|
||||
onTap: {
|
||||
selectedPlan = plan
|
||||
onPlanSelected(plan)
|
||||
if store.isLoading {
|
||||
ProgressView()
|
||||
.frame(height: 150)
|
||||
} else if let error = store.error {
|
||||
Text("Error loading plans: \(error.localizedDescription)")
|
||||
.foregroundColor(.red)
|
||||
.frame(height: 150)
|
||||
} else {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: Theme.Spacing.md) {
|
||||
// Add free plan
|
||||
PlanCard(
|
||||
plan: .free,
|
||||
isSelected: selectedPlan?.id == "free",
|
||||
onTap: {
|
||||
selectedPlan = .free
|
||||
onPlanSelected(.free)
|
||||
}
|
||||
)
|
||||
|
||||
// Add subscription plans from StoreKit
|
||||
ForEach(store.subscriptionPlans()) { plan in
|
||||
PlanCard(
|
||||
plan: plan,
|
||||
isSelected: selectedPlan?.id == plan.id,
|
||||
onTap: {
|
||||
selectedPlan = plan
|
||||
onPlanSelected(plan)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, Theme.Spacing.lg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -40,7 +55,7 @@ struct PlanSelector: View {
|
||||
|
||||
// MARK: - 单个计划卡片
|
||||
struct PlanCard: View {
|
||||
let plan: SubscriptionPlan
|
||||
let plan: WakeSubscriptionPlan
|
||||
let isSelected: Bool
|
||||
let onTap: () -> Void
|
||||
|
||||
@ -49,7 +64,7 @@ struct PlanCard: View {
|
||||
ZStack {
|
||||
// 主卡片内容
|
||||
VStack(spacing: Theme.Spacing.sm) {
|
||||
// Popular 标签
|
||||
// Popular 标签
|
||||
if plan.isPopular {
|
||||
VStack {
|
||||
HStack {
|
||||
@ -68,10 +83,10 @@ struct PlanCard: View {
|
||||
// 计划名称
|
||||
Text(plan.displayName)
|
||||
.font(Typography.font(for: .title, family: .quicksandBold, size: 18))
|
||||
.foregroundColor(plan == .pioneer ? Theme.Colors.textPrimary: Theme.Colors.textTertiary )
|
||||
.foregroundColor(plan.id != "free" ? Theme.Colors.textPrimary : Theme.Colors.textTertiary)
|
||||
|
||||
// 价格
|
||||
if plan == .pioneer {
|
||||
if plan.id != "free" {
|
||||
Text(plan.price)
|
||||
.font(Typography.font(for: .body, family: .quicksandBold, size: 20))
|
||||
.foregroundColor(Theme.Colors.textPrimary)
|
||||
@ -80,46 +95,43 @@ struct PlanCard: View {
|
||||
Spacer()
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 计划名称
|
||||
Text(plan.displayName)
|
||||
.font(Typography.font(for: .title, family: .quicksandBold, size: 18))
|
||||
.foregroundColor(plan == .pioneer ? Theme.Colors.textPrimary: Theme.Colors.textTertiary )
|
||||
|
||||
// 价格
|
||||
if plan == .pioneer {
|
||||
Text(plan.price)
|
||||
.font(Typography.font(for: .body, family: .quicksandBold, size: 20))
|
||||
.foregroundColor(Theme.Colors.textPrimary)
|
||||
} else {
|
||||
VStack {
|
||||
// 计划名称
|
||||
Text(plan.displayName)
|
||||
.font(Typography.font(for: .title, family: .quicksandBold, size: 18))
|
||||
.foregroundColor(plan.id != "free" ? Theme.Colors.textPrimary : Theme.Colors.textTertiary)
|
||||
|
||||
// 价格
|
||||
if plan.id != "free" {
|
||||
Text(plan.price)
|
||||
.font(Typography.font(for: .body, family: .quicksandBold, size: 20))
|
||||
.foregroundColor(Theme.Colors.textPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 120)
|
||||
.background(
|
||||
plan == .pioneer ?
|
||||
Theme.Colors.primary :
|
||||
Theme.Colors.surfaceTertiary
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.stroke(
|
||||
isSelected ? Theme.Colors.borderDark : Theme.Colors.border,
|
||||
lineWidth: 2
|
||||
)
|
||||
)
|
||||
.cornerRadius(Theme.CornerRadius.medium)
|
||||
.frame(width: 180, height: 150)
|
||||
.background(
|
||||
plan.id == "free" ? Theme.Colors.surfaceTertiary : Theme.Colors.primary
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.stroke(
|
||||
isSelected ? Theme.Colors.borderDark : Theme.Colors.border,
|
||||
lineWidth: 2
|
||||
)
|
||||
)
|
||||
.cornerRadius(Theme.CornerRadius.medium)
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - 预览
|
||||
#Preview("Plan Selector") {
|
||||
@State var selectedPlan: SubscriptionPlan? = .pioneer
|
||||
@State var selectedPlan: WakeSubscriptionPlan? = .pioneer
|
||||
|
||||
VStack(spacing: 20) {
|
||||
PlanSelector(
|
||||
@ -128,7 +140,6 @@ struct PlanCard: View {
|
||||
print("Selected plan: \(plan.displayName)")
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
|
||||
1
wake/View/Subscribe/Models/SubscriptionPlan.swift
Normal file
1
wake/View/Subscribe/Models/SubscriptionPlan.swift
Normal file
@ -0,0 +1 @@
|
||||
// This file has been removed as SubscriptionPlan is now defined in the main module
|
||||
61
wake/View/Subscribe/Store/SubscriptionStore.swift
Normal file
61
wake/View/Subscribe/Store/SubscriptionStore.swift
Normal file
@ -0,0 +1,61 @@
|
||||
import Foundation
|
||||
import StoreKit
|
||||
|
||||
// MARK: - Subscription Plan Model
|
||||
// Removed, now imported from Models directory
|
||||
|
||||
@MainActor
|
||||
class SubscriptionStore: ObservableObject {
|
||||
@Published private(set) var products: [Product] = []
|
||||
@Published private(set) var isLoading = false
|
||||
@Published private(set) var error: Error?
|
||||
|
||||
// Product IDs from StoreKit configuration
|
||||
private let productIDs = [
|
||||
"MEMBERSHIP_PIONEER_MONTHLY",
|
||||
"MEMBERSHIP_PRO_MONTH",
|
||||
"MEMBERSHIP_PRO_QUARTERLY",
|
||||
"MEMBERSHIP_PRO_YEARLY"
|
||||
]
|
||||
|
||||
init() {
|
||||
Task {
|
||||
await loadProducts()
|
||||
}
|
||||
}
|
||||
|
||||
func loadProducts() async {
|
||||
isLoading = true
|
||||
do {
|
||||
// Fetch products from App Store
|
||||
products = try await Product.products(for: productIDs)
|
||||
print("Fetched products: \(products)")
|
||||
} catch {
|
||||
self.error = error
|
||||
print("Failed to fetch products: \(error)")
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
// Convert StoreKit Product to our SubscriptionPlan model
|
||||
func subscriptionPlans() -> [wake.SubscriptionPlan] {
|
||||
return products.map { product in
|
||||
wake.SubscriptionPlan(
|
||||
id: product.id,
|
||||
displayName: product.displayName,
|
||||
price: product.displayPrice,
|
||||
isPopular: product.id == "MEMBERSHIP_PIONEER_MONTHLY"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Extension for Product
|
||||
extension Product {
|
||||
var displayPrice: String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .currency
|
||||
formatter.locale = self.priceFormatStyle.locale
|
||||
return formatter.string(from: self.price as NSDecimalNumber) ?? self.price.description
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user