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 SwiftUI
|
||||||
|
import StoreKit
|
||||||
|
|
||||||
// MARK: - 计划选择器组件
|
// MARK: - 计划选择器组件
|
||||||
struct PlanSelector: View {
|
struct PlanSelector: View {
|
||||||
@Binding var selectedPlan: SubscriptionPlan?
|
@Binding var selectedPlan: WakeSubscriptionPlan?
|
||||||
let onPlanSelected: (SubscriptionPlan) -> Void
|
@StateObject private var store = SubscriptionStore()
|
||||||
|
let onPlanSelected: (WakeSubscriptionPlan) -> Void
|
||||||
private let plans: [SubscriptionPlan] = [.free, .pioneer]
|
|
||||||
|
|
||||||
init(
|
|
||||||
selectedPlan: Binding<SubscriptionPlan?>,
|
|
||||||
onPlanSelected: @escaping (SubscriptionPlan) -> Void = { _ in }
|
|
||||||
) {
|
|
||||||
self._selectedPlan = selectedPlan
|
|
||||||
self.onPlanSelected = onPlanSelected
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: Theme.Spacing.md) {
|
if store.isLoading {
|
||||||
ForEach(plans, id: \.self) { plan in
|
ProgressView()
|
||||||
PlanCard(
|
.frame(height: 150)
|
||||||
plan: plan,
|
} else if let error = store.error {
|
||||||
isSelected: selectedPlan == plan,
|
Text("Error loading plans: \(error.localizedDescription)")
|
||||||
onTap: {
|
.foregroundColor(.red)
|
||||||
selectedPlan = plan
|
.frame(height: 150)
|
||||||
onPlanSelected(plan)
|
} 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: - 单个计划卡片
|
// MARK: - 单个计划卡片
|
||||||
struct PlanCard: View {
|
struct PlanCard: View {
|
||||||
let plan: SubscriptionPlan
|
let plan: WakeSubscriptionPlan
|
||||||
let isSelected: Bool
|
let isSelected: Bool
|
||||||
let onTap: () -> Void
|
let onTap: () -> Void
|
||||||
|
|
||||||
@ -49,7 +64,7 @@ struct PlanCard: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
// 主卡片内容
|
// 主卡片内容
|
||||||
VStack(spacing: Theme.Spacing.sm) {
|
VStack(spacing: Theme.Spacing.sm) {
|
||||||
// Popular 标签
|
// Popular 标签
|
||||||
if plan.isPopular {
|
if plan.isPopular {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
@ -68,10 +83,10 @@ struct PlanCard: View {
|
|||||||
// 计划名称
|
// 计划名称
|
||||||
Text(plan.displayName)
|
Text(plan.displayName)
|
||||||
.font(Typography.font(for: .title, family: .quicksandBold, size: 18))
|
.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)
|
Text(plan.price)
|
||||||
.font(Typography.font(for: .body, family: .quicksandBold, size: 20))
|
.font(Typography.font(for: .body, family: .quicksandBold, size: 20))
|
||||||
.foregroundColor(Theme.Colors.textPrimary)
|
.foregroundColor(Theme.Colors.textPrimary)
|
||||||
@ -80,46 +95,43 @@ struct PlanCard: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
VStack {
|
||||||
// 计划名称
|
// 计划名称
|
||||||
Text(plan.displayName)
|
Text(plan.displayName)
|
||||||
.font(Typography.font(for: .title, family: .quicksandBold, size: 18))
|
.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)
|
Text(plan.price)
|
||||||
.font(Typography.font(for: .body, family: .quicksandBold, size: 20))
|
.font(Typography.font(for: .body, family: .quicksandBold, size: 20))
|
||||||
.foregroundColor(Theme.Colors.textPrimary)
|
.foregroundColor(Theme.Colors.textPrimary)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.frame(width: 180, height: 150)
|
||||||
.frame(maxWidth: .infinity)
|
.background(
|
||||||
.frame(height: 120)
|
plan.id == "free" ? Theme.Colors.surfaceTertiary : Theme.Colors.primary
|
||||||
.background(
|
)
|
||||||
plan == .pioneer ?
|
.overlay(
|
||||||
Theme.Colors.primary :
|
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||||
Theme.Colors.surfaceTertiary
|
.stroke(
|
||||||
)
|
isSelected ? Theme.Colors.borderDark : Theme.Colors.border,
|
||||||
.overlay(
|
lineWidth: 2
|
||||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
)
|
||||||
.stroke(
|
)
|
||||||
isSelected ? Theme.Colors.borderDark : Theme.Colors.border,
|
.cornerRadius(Theme.CornerRadius.medium)
|
||||||
lineWidth: 2
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.cornerRadius(Theme.CornerRadius.medium)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - 预览
|
// MARK: - 预览
|
||||||
#Preview("Plan Selector") {
|
#Preview("Plan Selector") {
|
||||||
@State var selectedPlan: SubscriptionPlan? = .pioneer
|
@State var selectedPlan: WakeSubscriptionPlan? = .pioneer
|
||||||
|
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
PlanSelector(
|
PlanSelector(
|
||||||
@ -128,7 +140,6 @@ struct PlanCard: View {
|
|||||||
print("Selected plan: \(plan.displayName)")
|
print("Selected plan: \(plan.displayName)")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.background(Color(.systemGroupedBackground))
|
.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