feat: 订阅页面

This commit is contained in:
Junhui Chen 2025-08-20 10:39:06 +08:00
parent 417e101333
commit c293b248d0
10 changed files with 579 additions and 347 deletions

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "55F37A93-4556-4005-B9BD-8F1A1D6A8474"
type = "1"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "F1BBF7E2-4D6E-4646-83BC-F57E600056E4"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "wake/View/Subscribe/SubscribeView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "65"
endingLineNumber = "65"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

View File

@ -23,7 +23,7 @@ struct Theme {
static let accent = Color(hex: "FF6B6B") // static let accent = Color(hex: "FF6B6B") //
// MARK: - // MARK: -
static let background = Color(hex: "F8F9FA") // static let background = Color(hex: "FAFAFA") //
static let surface = Color.white // static let surface = Color.white //
static let surfaceSecondary = Color(hex: "F5F5F5") // static let surfaceSecondary = Color(hex: "F5F5F5") //
@ -40,14 +40,18 @@ struct Theme {
static let info = Color(hex: "3B82F6") // static let info = Color(hex: "3B82F6") //
// MARK: - // MARK: -
static let border = Color(hex: "E5E7EB") // static let border = Color(hex: "D9D9D9") //
static let borderLight = Color(hex: "F3F4F6") // static let borderLight = Color(hex: "F3F4F6") //
static let borderDark = Color(hex: "D1D5DB") // static let borderBlack = Color.black //
static let borderDark = borderBlack //
// MARK: - // MARK: -
static let freeBackground = primaryLight // Free static let freeBackground = primaryLight // Free
static let pioneerBackground = primary // Pioneer static let pioneerBackground = primary // Pioneer
static let subscribeButton = primary // static let subscribeButton = primary //
// MARK: -
static let cardBackground = Color.white //
} }
// MARK: - // MARK: -
@ -59,9 +63,13 @@ struct Theme {
) )
static let backgroundGradient = LinearGradient( static let backgroundGradient = LinearGradient(
colors: [Colors.background, Colors.surface], gradient: Gradient(colors: [
startPoint: .top, Color(hex: "FBC063"),
endPoint: .bottom Color(hex: "FEE9BE"),
Color(hex: "FAB851")
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
) )
static let accentGradient = LinearGradient( static let accentGradient = LinearGradient(
@ -69,6 +77,12 @@ struct Theme {
startPoint: .leading, startPoint: .leading,
endPoint: .trailing endPoint: .trailing
) )
// static let creditsInfoTooltip = LinearGradient(
// colors: [Colors(hex: "FFD38F"), Colors(hex: "FFF8DE"), Colors(hex: "FECE83")],
// startPoint: .topLeading,
// endPoint: .bottomTrailing
// )
} }
// MARK: - // MARK: -

View File

@ -60,21 +60,21 @@ struct Typography {
/// - style: /// - style:
/// - family: nil 使 /// - family: nil 使
/// - Returns: Font /// - Returns: Font
static func font(for style: TypographyStyle, family: FontFamily? = nil) -> Font { static func font(for style: TypographyStyle, family: FontFamily? = nil, size: CGFloat? = nil) -> Font {
let fontFamily = family ?? defaultFontFamily let fontFamily = family ?? defaultFontFamily
guard let config = styleConfig[style] else { guard let config = styleConfig[style] else {
return .body return .body
} }
// //
if let customFont = UIFont(name: fontFamily.name, size: config.size) { if let customFont = UIFont(name: fontFamily.name, size: size ?? config.size) {
let metrics = UIFontMetrics(forTextStyle: config.textStyle) let metrics = UIFontMetrics(forTextStyle: config.textStyle)
let scaledFont = metrics.scaledFont(for: customFont) let scaledFont = metrics.scaledFont(for: customFont)
return Font(scaledFont) return Font(scaledFont)
} }
// 退 // 退
let systemFont = UIFont.systemFont(ofSize: config.size, weight: config.weight) let systemFont = UIFont.systemFont(ofSize: size ?? config.size, weight: config.weight)
let metrics = UIFontMetrics(forTextStyle: config.textStyle) let metrics = UIFontMetrics(forTextStyle: config.textStyle)
let scaledFont = metrics.scaledFont(for: systemFont) let scaledFont = metrics.scaledFont(for: systemFont)
return Font(scaledFont) return Font(scaledFont)

View File

@ -0,0 +1,288 @@
//
// CreditsDetailView.swift
// wake
//
// Created by fairclip on 2025/8/19.
//
import SwiftUI
// MARK: -
enum CreditTransactionType: String, CaseIterable {
case photoUnderstanding = "Photo Understanding"
case videoUnderstanding = "Video Understanding"
case mysteryBoxPurchase = "Mystery Box Purchase"
case dailyBonus = "Daily Bonus"
case subscriptionBonus = "Subscription Bonus"
var creditChange: Int {
switch self {
case .photoUnderstanding:
return -1
case .videoUnderstanding:
return -32
case .mysteryBoxPurchase:
return -100
case .dailyBonus:
return 200
case .subscriptionBonus:
return 500
}
}
var icon: String {
switch self {
case .photoUnderstanding:
return "photo"
case .videoUnderstanding:
return "video"
case .mysteryBoxPurchase:
return "gift"
case .dailyBonus:
return "calendar"
case .subscriptionBonus:
return "star.fill"
}
}
}
// MARK: -
struct CreditTransaction {
let id = UUID()
let type: CreditTransactionType
let date: Date
let creditChange: Int
init(type: CreditTransactionType, date: Date, creditChange: Int? = nil) {
self.type = type
self.date = date
self.creditChange = creditChange ?? type.creditChange
}
}
// MARK: -
struct CreditsDetailView: View {
@Environment(\.presentationMode) var presentationMode
@State private var showRules = false
//
private let totalCredits = 3290
private let expiringToday = 200
private let transactions: [CreditTransaction] = [
CreditTransaction(type: .photoUnderstanding, date: Calendar.current.date(byAdding: .hour, value: -2, to: Date()) ?? Date()),
CreditTransaction(type: .videoUnderstanding, date: Calendar.current.date(byAdding: .hour, value: -4, to: Date()) ?? Date()),
CreditTransaction(type: .mysteryBoxPurchase, date: Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date()),
CreditTransaction(type: .dailyBonus, date: Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date()),
CreditTransaction(type: .subscriptionBonus, date: Calendar.current.date(byAdding: .day, value: -2, to: Date()) ?? Date())
]
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 0) {
//
navigationHeader
//
mainCreditsCard
//
creditsHistorySection
Spacer(minLength: 100)
}
}
.background(Color(.systemGroupedBackground))
.navigationBarHidden(true)
}
}
// MARK: -
private var navigationHeader: some View {
NaviHeader(title: "Credits") {
presentationMode.wrappedValue.dismiss()
}
}
// MARK: -
private var mainCreditsCard: some View {
VStack(spacing: 0) {
//
HStack {
//
Circle()
.fill(Color.black)
.frame(width: 80, height: 80)
.overlay(
Image(systemName: "triangle.fill")
.foregroundColor(.white)
.font(.system(size: 24, weight: .bold))
)
Spacer()
//
VStack(alignment: .trailing, spacing: 8) {
HStack(spacing: 8) {
Circle()
.fill(Color.black)
.frame(width: 24, height: 24)
.overlay(
Image(systemName: "triangle.fill")
.foregroundColor(.white)
.font(.system(size: 8))
)
Text("\(totalCredits)")
.font(Typography.font(for: .headline, family: .quicksandBold, size: 36))
.foregroundColor(.black)
}
Text("Expiring Today : \(expiringToday)")
.font(Typography.font(for: .body, family: .quicksand))
.foregroundColor(.black.opacity(0.8))
}
}
.padding(Theme.Spacing.xl)
// 线
DashedLine()
.stroke(Color.black.opacity(0.3), style: StrokeStyle(lineWidth: 1, dash: [5, 5]))
.frame(height: 1)
.padding(.horizontal, Theme.Spacing.xl)
//
creditsRulesSection
}
.background(
LinearGradient(
colors: [
Color(hex: "FFB645"),
Color(hex: "FFA726")
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.cornerRadius(Theme.CornerRadius.large)
.padding(.horizontal, Theme.Spacing.xl)
.padding(.top, Theme.Spacing.xl)
}
// MARK: -
private var creditsRulesSection: some View {
VStack(spacing: 0) {
//
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
showRules.toggle()
}
}) {
HStack {
Text("Credits Rules")
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(.black)
Spacer()
Image(systemName: showRules ? "chevron.up" : "chevron.down")
.foregroundColor(.black)
.font(.system(size: 14, weight: .medium))
}
.padding(.horizontal, Theme.Spacing.xl)
.padding(.vertical, Theme.Spacing.lg)
}
//
if showRules {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Credits can be used for material indexing (1 credit per photo or per second of video) and for buying blind boxes (100 credits each).")
.font(Typography.font(for: .subtitle, family: .quicksand))
.foregroundColor(.black.opacity(0.8))
.multilineTextAlignment(.leading)
}
.padding(.horizontal, Theme.Spacing.xl)
.padding(.bottom, Theme.Spacing.lg)
}
}
}
// MARK: -
private var creditsHistorySection: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.lg) {
Text("Points History")
.font(Typography.font(for: .title, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
.padding(.horizontal, Theme.Spacing.xl)
LazyVStack(spacing: 0) {
ForEach(Array(transactions.enumerated()), id: \.element.id) { index, transaction in
CreditTransactionRow(
transaction: transaction,
isLast: index == transactions.count - 1
)
}
}
.background(Color(.systemBackground))
.cornerRadius(Theme.CornerRadius.medium)
.padding(.horizontal, Theme.Spacing.xl)
}
.padding(.top, Theme.Spacing.xl)
}
}
// MARK: -
struct CreditTransactionRow: View {
let transaction: CreditTransaction
let isLast: Bool
var body: some View {
VStack(spacing: 0) {
HStack(spacing: Theme.Spacing.lg) {
VStack(alignment: .leading, spacing: 4) {
Text(transaction.type.rawValue)
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
Text(formatDate(transaction.date))
.font(Typography.font(for: .caption, family: .quicksand))
.foregroundColor(Theme.Colors.textSecondary)
}
Spacer()
Text("\(transaction.creditChange > 0 ? "+" : "")\(transaction.creditChange)")
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(transaction.creditChange > 0 ? Theme.Colors.success : Theme.Colors.textPrimary)
}
.padding(.horizontal, Theme.Spacing.lg)
.padding(.vertical, Theme.Spacing.lg)
if !isLast {
Divider()
.background(Theme.Colors.borderLight)
}
}
}
private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "MM-dd-yyyy"
return formatter.string(from: date)
}
}
// MARK: - 线
struct DashedLine: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
return path
}
}
// MARK: -
#Preview {
CreditsDetailView()
}

View File

@ -1,4 +1,4 @@
// //
// CreditsInfoCard.swift // CreditsInfoCard.swift
// wake // wake
// //
@ -7,294 +7,93 @@
import SwiftUI import SwiftUI
// MARK: -
enum CreditType: String, CaseIterable {
case daily = "Daily"
case purchased = "Purchased"
case bonus = "Bonus"
case permanent = "Permanent"
var displayName: String {
switch self {
case .daily:
return "Daily Credits"
case .purchased:
return "Purchased Credits"
case .bonus:
return "Bonus Credits"
case .permanent:
return "Permanent Credits"
}
}
var icon: String {
switch self {
case .daily:
return "calendar"
case .purchased:
return "creditcard"
case .bonus:
return "gift"
case .permanent:
return "infinity"
}
}
var color: Color {
switch self {
case .daily:
return Theme.Colors.info
case .purchased:
return Theme.Colors.success
case .bonus:
return Theme.Colors.warning
case .permanent:
return Theme.Colors.primary
}
}
}
// MARK: -
struct CreditInfo {
let type: CreditType
let amount: Int
let description: String
}
// MARK: - // MARK: -
struct CreditsInfoCard: View { struct CreditsInfoCard: View {
let totalCredits: Int let totalCredits: Int
let creditBreakdown: [CreditInfo]
let onInfoTap: (() -> Void)? let onInfoTap: (() -> Void)?
let onDetailTap: (() -> Void)? let onDetailTap: (() -> Void)?
@State private var showBreakdown = false @State private var showInfoPopover = false
init( init(
totalCredits: Int, totalCredits: Int,
creditBreakdown: [CreditInfo] = [],
onInfoTap: (() -> Void)? = nil, onInfoTap: (() -> Void)? = nil,
onDetailTap: (() -> Void)? = nil onDetailTap: (() -> Void)? = nil
) { ) {
self.totalCredits = totalCredits self.totalCredits = totalCredits
self.creditBreakdown = creditBreakdown
self.onInfoTap = onInfoTap self.onInfoTap = onInfoTap
self.onDetailTap = onDetailTap self.onDetailTap = onDetailTap
} }
var body: some View { var body: some View {
VStack(spacing: 0) { Button(action: {
// onDetailTap?()
}) {
mainCreditsSection mainCreditsSection
//
if showBreakdown && !creditBreakdown.isEmpty {
creditsBreakdownSection
}
} }
.background(Color(.systemBackground)) .buttonStyle(PlainButtonStyle())
.cornerRadius(Theme.CornerRadius.medium) .background(Theme.Colors.primaryLight)
.cornerRadius(Theme.CornerRadius.extraLarge)
.shadow(color: Theme.Shadows.small, radius: Theme.Shadows.cardShadow.radius, x: Theme.Shadows.cardShadow.x, y: Theme.Shadows.cardShadow.y) .shadow(color: Theme.Shadows.small, radius: Theme.Shadows.cardShadow.radius, x: Theme.Shadows.cardShadow.x, y: Theme.Shadows.cardShadow.y)
} }
// MARK: - // MARK: -
private var mainCreditsSection: some View { private var mainCreditsSection: some View {
HStack(spacing: Theme.Spacing.lg) { HStack(spacing: Theme.Spacing.md) {
// //
HStack(spacing: Theme.Spacing.sm) { HStack(spacing: Theme.Spacing.sm) {
Circle() Text("Credits:")
.fill(Theme.Gradients.primaryGradient) .font(Typography.font(for: .subtitle, family: .quicksandBold))
.frame(width: 40, height: 40) .foregroundColor(Theme.Colors.textPrimary)
.overlay(
Image(systemName: "star.fill")
.foregroundColor(.white)
.font(.system(size: 18, weight: .semibold))
)
VStack(alignment: .leading, spacing: 2) { Text("\(totalCredits)")
Text("Credits") .font(Typography.font(for: .subtitle, family: .quicksandBold))
.font(Typography.font(for: .caption, family: .quicksand)) .foregroundColor(Theme.Colors.textPrimary)
.foregroundColor(Theme.Colors.textSecondary)
Text("\(totalCredits)")
.font(Typography.font(for: .title, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
}
} }
Spacer()
// //
HStack(spacing: Theme.Spacing.sm) { HStack(spacing: Theme.Spacing.sm) {
// //
Button(action: { Button(action: {
showInfoPopover = true
onInfoTap?() onInfoTap?()
}) { }) {
Image(systemName: "info.circle") Image(systemName: "questionmark.circle")
.foregroundColor(Theme.Colors.textSecondary) .foregroundColor(Theme.Colors.textSecondary)
.font(.system(size: 16)) .font(.system(size: 16))
} }
.popover(isPresented: $showInfoPopover, attachmentAnchor: .point(.bottom), arrowEdge: .top) {
// / Text("Credits can be used for material indexing (1 credit per photo or per second of video) and for buying blind boxes (100 crediteach)")
if !creditBreakdown.isEmpty { .font(Typography.font(for: .caption, family: .quicksandRegular))
Button(action: { .multilineTextAlignment(.center)
withAnimation(.easeInOut(duration: 0.3)) { .presentationBackground(Theme.Gradients.backgroundGradient)
showBreakdown.toggle() .frame(minWidth: 240, maxWidth: UIScreen.main.bounds.width * 0.6)
} .presentationCompactAdaptation(.popover)
}) { .padding(.horizontal, Theme.Spacing.md)
Image(systemName: showBreakdown ? "chevron.up" : "chevron.down") .padding(.vertical, Theme.Spacing.sm)
.foregroundColor(Theme.Colors.textSecondary)
.font(.system(size: 14, weight: .medium))
}
} }
//
Button(action: {
onDetailTap?()
}) {
Image(systemName: "chevron.right")
.foregroundColor(Theme.Colors.textSecondary)
.font(.system(size: 14, weight: .medium))
}
}
}
.padding(Theme.Spacing.lg)
}
// MARK: -
private var creditsBreakdownSection: some View {
VStack(spacing: 0) {
Divider()
.background(Theme.Colors.border)
VStack(spacing: Theme.Spacing.sm) {
ForEach(Array(creditBreakdown.enumerated()), id: \.offset) { index, credit in
CreditBreakdownRow(credit: credit, isLast: index == creditBreakdown.count - 1)
}
}
.padding(Theme.Spacing.lg)
}
}
}
// MARK: -
struct CreditBreakdownRow: View {
let credit: CreditInfo
let isLast: Bool
var body: some View {
HStack(spacing: Theme.Spacing.md) {
//
Circle()
.fill(credit.type.color.opacity(0.1))
.frame(width: 32, height: 32)
.overlay(
Image(systemName: credit.type.icon)
.foregroundColor(credit.type.color)
.font(.system(size: 14, weight: .medium))
)
//
VStack(alignment: .leading, spacing: 2) {
Text(credit.type.displayName)
.font(Typography.font(for: .subtitle, family: .quicksand))
.foregroundColor(Theme.Colors.textPrimary)
Text(credit.description)
.font(Typography.font(for: .caption, family: .quicksand))
.foregroundColor(Theme.Colors.textSecondary)
.lineLimit(2)
}
Spacer()
//
Text("+\(credit.amount)")
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(credit.type.color)
}
.padding(.vertical, Theme.Spacing.xs)
if !isLast {
Divider()
.background(Theme.Colors.borderLight)
.padding(.leading, 44)
}
}
}
// MARK: - 使
struct CreditsUsageCard: View {
let todayUsed: Int
let weeklyUsed: Int
let monthlyUsed: Int
var body: some View {
VStack(spacing: Theme.Spacing.md) {
HStack {
Text("Credits Usage")
.font(Typography.font(for: .subtitle, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
Spacer() Spacer()
Text("This Period") //
.font(Typography.font(for: .caption, family: .quicksand)) Image(systemName: "chevron.right")
.foregroundColor(Theme.Colors.textSecondary) .foregroundColor(Theme.Colors.textPrimary)
} .font(.system(size: 14, weight: .medium))
HStack(spacing: Theme.Spacing.lg) {
UsageStatItem(title: "Today", value: todayUsed, color: Theme.Colors.info)
Divider()
.frame(height: 40)
UsageStatItem(title: "Week", value: weeklyUsed, color: Theme.Colors.warning)
Divider()
.frame(height: 40)
UsageStatItem(title: "Month", value: monthlyUsed, color: Theme.Colors.success)
} }
} }
.padding(Theme.Spacing.lg) .padding(Theme.Spacing.lg)
.background(Color(.systemBackground))
.cornerRadius(Theme.CornerRadius.medium)
.shadow(color: Theme.Shadows.small, radius: Theme.Shadows.cardShadow.radius, x: Theme.Shadows.cardShadow.x, y: Theme.Shadows.cardShadow.y)
} }
} }
// MARK: - 使
struct UsageStatItem: View {
let title: String
let value: Int
let color: Color
var body: some View {
VStack(spacing: Theme.Spacing.xs) {
Text("\(value)")
.font(Typography.font(for: .title, family: .quicksandBold))
.foregroundColor(color)
Text(title)
.font(Typography.font(for: .caption, family: .quicksand))
.foregroundColor(Theme.Colors.textSecondary)
}
.frame(maxWidth: .infinity)
}
}
// MARK: - // MARK: -
#Preview("Credits Info Card") { #Preview("Credits Info Card") {
VStack(spacing: 20) { VStack(spacing: 20) {
CreditsInfoCard( CreditsInfoCard(
totalCredits: 3290, totalCredits: 3290,
creditBreakdown: [
CreditInfo(type: .daily, amount: 200, description: "Daily free credits"),
CreditInfo(type: .purchased, amount: 1000, description: "Purchased package"),
CreditInfo(type: .bonus, amount: 500, description: "Welcome bonus"),
CreditInfo(type: .permanent, amount: 1590, description: "Subscription credits")
],
onInfoTap: { onInfoTap: {
print("Info tapped") print("Info tapped")
}, },
@ -302,12 +101,6 @@ struct UsageStatItem: View {
print("Detail tapped") print("Detail tapped")
} }
) )
CreditsUsageCard(
todayUsed: 45,
weeklyUsed: 280,
monthlyUsed: 1150
)
} }
.padding() .padding()
.background(Color(.systemGroupedBackground)) .background(Color(.systemGroupedBackground))

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,135 @@
//
// PlanSelector.swift
// wake
//
// Created by fairclip on 2025/8/19.
//
import SwiftUI
// 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
}
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)
}
)
}
}
}
}
// MARK: -
struct PlanCard: View {
let plan: SubscriptionPlan
let isSelected: Bool
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
ZStack {
//
VStack(spacing: Theme.Spacing.sm) {
// Popular
if plan.isPopular {
VStack {
HStack {
Spacer()
Text("Popular")
.font(Typography.font(for: .caption, family: .quicksandRegular))
.foregroundColor(Color.white)
.padding(.horizontal, Theme.Spacing.sm)
.padding(.vertical, Theme.Spacing.xs)
.background(Color.black)
.cornerRadius(Theme.CornerRadius.round, corners: [.bottomLeft])
}
Spacer()
VStack {
//
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)
}
}
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)
}
}
}
.frame(maxWidth: .infinity)
.frame(height: 120)
.background(
plan == .pioneer ?
Theme.Colors.primary :
Theme.Colors.surface
)
.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
VStack(spacing: 20) {
PlanSelector(
selectedPlan: $selectedPlan,
onPlanSelected: { plan in
print("Selected plan: \(plan.displayName)")
}
)
}
.padding()
.background(Color(.systemGroupedBackground))
}

View File

@ -61,21 +61,21 @@ struct SubscriptionStatusBar: View {
var body: some View { var body: some View {
HStack(spacing: 16) { HStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 20) {
// //
Text(status.title) Text(status.title)
.font(.system(size: 28, weight: .bold, design: .rounded)) .font(Typography.font(for: .headline, family: .quicksandBold, size: 32))
.foregroundColor(status.textColor) .foregroundColor(status.textColor)
// //
if case .pioneer(let expiryDate) = status { if case .pioneer(let expiryDate) = status {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("Expires on :") Text("Expires on :")
.font(.system(size: 14, weight: .medium)) .font(Typography.font(for: .body, family: .quicksandRegular))
.foregroundColor(status.textColor.opacity(0.8)) .foregroundColor(status.textColor.opacity(0.7))
Text(formatDate(expiryDate)) Text(formatDate(expiryDate))
.font(.system(size: 16, weight: .semibold)) .font(Typography.font(for: .body, family: .quicksandRegular))
.foregroundColor(status.textColor) .foregroundColor(status.textColor)
} }
} else { } else {
@ -83,12 +83,12 @@ struct SubscriptionStatusBar: View {
onSubscribeTap?() onSubscribeTap?()
}) { }) {
Text("Subscribe") Text("Subscribe")
.font(.system(size: 14, weight: .semibold)) .font(Typography.font(for: .title, family: .quicksandRegular, size: 16))
.foregroundColor(Theme.Colors.textPrimary) .foregroundColor(Theme.Colors.textPrimary)
.padding(.horizontal, 20) .padding(.horizontal, 12)
.padding(.vertical, 8) .padding(.vertical, 6)
.background(Theme.Colors.subscribeButton) .background(Theme.Gradients.backgroundGradient)
.cornerRadius(Theme.CornerRadius.large) .cornerRadius(Theme.CornerRadius.extraLarge)
} }
} }
} }

View File

@ -38,7 +38,7 @@ struct SubscriptionFeature {
} }
struct SubscribeView: View { struct SubscribeView: View {
@State private var selectedPlan: SubscriptionPlan = .free @State private var selectedPlan: SubscriptionPlan? = .pioneer
@State private var isLoading = false @State private var isLoading = false
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@ -62,11 +62,18 @@ struct SubscribeView: View {
// //
creditsSection creditsSection
// VStack {
subscriptionPlansSection //
subscriptionPlansSection
//
specialOfferBanner //
specialOfferBanner
}
.background(Theme.Colors.cardBackground)
.cornerRadius(Theme.CornerRadius.medium)
.padding(.horizontal, Theme.Spacing.xl)
.padding(.vertical, Theme.Spacing.xl)
// //
featureComparisonTable featureComparisonTable
@ -80,7 +87,7 @@ struct SubscribeView: View {
Spacer(minLength: 100) Spacer(minLength: 100)
} }
} }
.background(Color(.systemGroupedBackground)) .background(Theme.Colors.background)
.navigationBarHidden(true) .navigationBarHidden(true)
} }
} }
@ -94,103 +101,73 @@ struct SubscribeView: View {
// MARK: - // MARK: -
private var currentSubscriptionCard: some View { private var currentSubscriptionCard: some View {
VStack(spacing: 0) { SubscriptionStatusBar(
HStack { status: .pioneer(expiryDate: Date()) ,
VStack(alignment: .leading, spacing: 8) { onSubscribeTap: {
Text("Free") //
.font(Typography.font(for: .headline, family: .quicksand)) handleSubscribe()
.fontWeight(.bold)
Button(action: {
//
}) {
Text("Subscribe")
.font(Typography.font(for: .subtitle, family: .quicksand))
.fontWeight(.medium)
.foregroundColor(.black)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Color.orange)
.cornerRadius(20)
}
}
Spacer()
//
Circle()
.fill(Color.black)
.frame(width: 60, height: 60)
.overlay(
Image(systemName: "play.fill")
.foregroundColor(.white)
.font(.title2)
)
} }
.padding(20) )
.background(Color.orange.opacity(0.2)) .padding(.horizontal, Theme.Spacing.xl)
.cornerRadius(16)
.padding(.horizontal, 20)
}
} }
// MARK: - // MARK: -
private var creditsSection: some View { private var creditsSection: some View {
HStack { VStack(spacing: 16) {
Text("Credits: 3290") CreditsInfoCard(
.font(Typography.font(for: .body, family: .quicksand)) totalCredits: 3290,
.fontWeight(.medium) onInfoTap: {
//
},
onDetailTap: {
//
}
)
Button(action: {
//
}) {
Image(systemName: "info.circle")
.foregroundColor(.gray)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
.font(.caption)
} }
.padding(.horizontal, 20) .padding(.horizontal, Theme.Spacing.xl)
.padding(.vertical, 16) .padding(.top, Theme.Spacing.xl)
.background(Color(.systemBackground))
.cornerRadius(12)
.padding(.horizontal, 20)
.padding(.top, 20)
} }
// MARK: - // MARK: -
private var subscriptionPlansSection: some View { private var subscriptionPlansSection: some View {
HStack(spacing: 16) { PlanSelector(
// Free selectedPlan: $selectedPlan,
SubscriptionPlanCard( onPlanSelected: { plan in
plan: .free, print("Selected plan: \(plan.displayName)")
isSelected: selectedPlan == .free, }
onTap: { selectedPlan = .free } )
) .padding(.horizontal, Theme.Spacing.xl)
.padding(.top, Theme.Spacing.xl)
// Pioneer
SubscriptionPlanCard(
plan: .pioneer,
isSelected: selectedPlan == .pioneer,
onTap: { selectedPlan = .pioneer }
)
}
.padding(.horizontal, 20)
.padding(.top, 20)
} }
// MARK: - // MARK: -
private var specialOfferBanner: some View { private var specialOfferBanner: some View {
Text("First 100 users get a special deal: just $1 for your first month!") HStack(spacing: 0) {
.font(Typography.font(for: .caption, family: .quicksand)) Text("First")
.multilineTextAlignment(.center) .font(Typography.font(for: .footnote, family: .quicksandRegular))
.padding(.horizontal, 20) .foregroundColor(Theme.Colors.textPrimary)
.padding(.top, 12)
.foregroundColor(.secondary) 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: - // MARK: -