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,28 +6,40 @@
// //
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 {
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) { HStack(spacing: Theme.Spacing.md) {
ForEach(plans, id: \.self) { plan in // 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( PlanCard(
plan: plan, plan: plan,
isSelected: selectedPlan == plan, isSelected: selectedPlan?.id == plan.id,
onTap: { onTap: {
selectedPlan = plan selectedPlan = plan
onPlanSelected(plan) onPlanSelected(plan)
@ -35,12 +47,15 @@ struct PlanSelector: View {
) )
} }
} }
.padding(.horizontal, Theme.Spacing.lg)
}
}
} }
} }
// 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
@ -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,27 +95,25 @@ 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(maxWidth: .infinity) }
.frame(height: 120) .frame(width: 180, height: 150)
.background( .background(
plan == .pioneer ? plan.id == "free" ? Theme.Colors.surfaceTertiary : Theme.Colors.primary
Theme.Colors.primary :
Theme.Colors.surfaceTertiary
) )
.overlay( .overlay(
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
@ -116,10 +129,9 @@ struct PlanCard: View {
} }
} }
// 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))

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