Merge branch 'V2.0.0' of https://git.fairclip.cn/FairClip/wake-ios into V2.0.0

This commit is contained in:
jinyaqiu 2025-09-02 13:53:44 +08:00
commit 08db3dc287
5 changed files with 187 additions and 56 deletions

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

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

View File

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

View File

@ -0,0 +1 @@
// This file has been removed as SubscriptionPlan is now defined in the main module

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