wake-ios/wake/View/Subscribe/SubscribeView.swift
2025-09-02 20:29:25 +08:00

298 lines
9.5 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 = ""
@State private var memberProfile: MemberProfile?
//
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)
.onAppear {
fetchMemberInfo()
}
.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 memberProfile?.membershipLevel == "pioneer" {
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let expiryDate = memberProfile.flatMap { dateFormatter.date(from: $0.membershipEndAt) } ?? Date()
return .pioneer(expiryDate: expiryDate)
} else {
return .free
}
}()
return SubscriptionStatusBar(
status: .pioneer(expiryDate: Date()),
onSubscribeTap: {
//
handleSubscribe()
}
)
.padding(.horizontal, Theme.Spacing.xl)
}
// MARK: -
private var creditsSection: some View {
VStack(spacing: 16) {
CreditsInfoCard(
totalCredits: memberProfile?.remainPoints ?? 0,
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() }
}
// MARK: - Helper Methods
private func fetchMemberInfo() {
NetworkService.shared.get(
path: "/membership/personal-center-info",
parameters: nil
) { (result: Result<APIResponse<MemberProfile>, NetworkError>) in
DispatchQueue.main.async {
switch result {
case .success(let response):
self.memberProfile = response.data
print("✅ 成功获取会员信息:", response.data)
print("✅ 用户ID:", response.data.userInfo.userId)
print("✅ 用户昵称:", response.data.userInfo.nickname)
print("✅ 用户邮箱:", response.data.userInfo.email)
case .failure(let error):
print("❌ 获取会员信息失败:", error)
// Optionally show an error message to the user
self.errorText = "Failed to load member info: \(error.localizedDescription)"
self.showErrorAlert = true
}
}
}
}
}
#Preview {
SubscribeView()
}