wake-ios/wake/View/Subscribe/SubscribeView.swift
2025-09-01 19:42:32 +08:00

268 lines
8.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// SubscribeView.swift
// wake
//
// Created by fairclip on 2025/8/19.
//
import SwiftUI
import StoreKit
// MARK: -
enum SubscriptionPlan: String, CaseIterable {
case free = "Free"
case pioneer = "Pioneer"
var displayName: String {
return self.rawValue
}
var price: String {
switch self {
case .free:
return "Free"
case .pioneer:
return "1$/Mon"
}
}
var isPopular: Bool {
return self == .pioneer
}
}
// MARK: -
struct SubscriptionFeature {
let name: String
let freeValue: String
let proValue: String
}
struct SubscribeView: View {
@Environment(\.dismiss) private var dismiss
@StateObject private var store = IAPManager()
@State private var selectedPlan: SubscriptionPlan? = .pioneer
@State private var isLoading = false
@State private var showErrorAlert = false
@State private var errorText = ""
//
private let features = [
SubscriptionFeature(name: "Mystery Box Purchase:", freeValue: "3 /week", proValue: "Free"),
SubscriptionFeature(name: "Material Upload:", freeValue: "50 images and\n5 videos/day", proValue: "Unlimited"),
SubscriptionFeature(name: "Free Credits:", freeValue: "200 /day", proValue: "500 /day")
]
var body: some View {
VStack(spacing: 0) {
//
SimpleNaviHeader(title: "Subscription") {
dismiss()
}
.background(Color.themeTextWhiteSecondary)
.padding(.bottom, Theme.Spacing.lg)
ScrollView {
VStack(spacing: 0) {
//
currentSubscriptionCard
//
creditsSection
VStack {
//
subscriptionPlansSection
//
specialOfferBanner
}
.background(Theme.Colors.cardBackground)
.cornerRadius(Theme.CornerRadius.medium)
.padding(.horizontal, Theme.Spacing.lg)
.padding(.vertical, Theme.Spacing.lg)
//
featureComparisonTable
//
subscribeButton
//
legalLinks
Spacer(minLength: 100)
}
}
.background(Theme.Colors.background)
}
.background(Color.themeTextWhiteSecondary)
.navigationBarHidden(true)
.task {
// Load products and refresh current entitlements on appear
await store.loadProducts()
await store.refreshEntitlements()
}
.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: -
private var currentSubscriptionCard: some View {
let status: SubscriptionStatus = {
if store.isSubscribed {
return .pioneer(expiryDate: store.subscriptionExpiry ?? Date())
} else {
return .free
}
}()
return SubscriptionStatusBar(
status: status,
onSubscribeTap: {
//
handleSubscribe()
}
)
.padding(.horizontal, Theme.Spacing.xl)
}
// MARK: -
private var creditsSection: some View {
VStack(spacing: 16) {
CreditsInfoCard(
totalCredits: 3290,
onInfoTap: {
//
},
onDetailTap: {
//
}
)
}
.padding(.horizontal, Theme.Spacing.xl)
.padding(.top, Theme.Spacing.xl)
}
// MARK: -
private var subscriptionPlansSection: some View {
PlanSelector(
selectedPlan: $selectedPlan,
onPlanSelected: { plan in
print("Selected plan: \(plan.displayName)")
}
)
.padding(.horizontal, Theme.Spacing.xl)
.padding(.top, Theme.Spacing.xl)
}
// MARK: -
private var specialOfferBanner: some View {
HStack(spacing: 0) {
Text("First")
.font(Typography.font(for: .footnote, family: .quicksandRegular))
.foregroundColor(Theme.Colors.textPrimary)
Text(" 100")
.font(Typography.font(for: .footnote, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
Text(" users get a special deal: justs")
.font(Typography.font(for: .footnote, family: .quicksandRegular))
.foregroundColor(Theme.Colors.textPrimary)
Text(" $1")
.font(Typography.font(for: .footnote, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
Text(" for your first month!")
.font(Typography.font(for: .footnote, family: .quicksandRegular))
.foregroundColor(Theme.Colors.textPrimary)
}
.multilineTextAlignment(.center)
.padding(.horizontal, Theme.Spacing.lg)
.padding(.top, Theme.Spacing.sm)
.padding(.bottom, Theme.Spacing.lg)
}
// MARK: -
private var featureComparisonTable: some View {
PlanCompare()
.padding(.horizontal, Theme.Spacing.lg)
}
// MARK: -
private var subscribeButton: some View {
VStack(spacing: 12) {
SubscribeButton(
title: "Subscribe",
isLoading: isLoading,
subscribed: store.isSubscribed,
action: handleSubscribe
)
}
.padding(.horizontal, Theme.Spacing.xl)
.padding(.top, Theme.Spacing.lg)
}
// MARK: -
private var legalLinks: some View {
HStack(spacing: 8) {
Button(action: {
//
}) {
Text("Terms of Service")
.underline()
}
Text("|")
.foregroundColor(.secondary)
Button(action: {
//
}) {
Text("Privacy Policy")
.underline()
}
Text("|")
.foregroundColor(.secondary)
Button(action: {
Task { await store.restorePurchases() }
}) {
Text("Restore Purchase")
.underline()
}
}
.font(Typography.font(for: .caption, family: .quicksandRegular))
.foregroundColor(.secondary)
.padding(.top, Theme.Spacing.sm)
}
// MARK: -
private func handleSubscribe() {
Task { await store.purchasePioneer() }
}
}
#Preview {
SubscribeView()
}