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

View File

@ -60,21 +60,21 @@ struct Typography {
/// - style:
/// - family: nil 使
/// - 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
guard let config = styleConfig[style] else {
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 scaledFont = metrics.scaledFont(for: customFont)
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 scaledFont = metrics.scaledFont(for: systemFont)
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
// wake
//
@ -7,294 +7,93 @@
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: -
struct CreditsInfoCard: View {
let totalCredits: Int
let creditBreakdown: [CreditInfo]
let onInfoTap: (() -> Void)?
let onDetailTap: (() -> Void)?
@State private var showBreakdown = false
@State private var showInfoPopover = false
init(
totalCredits: Int,
creditBreakdown: [CreditInfo] = [],
onInfoTap: (() -> Void)? = nil,
onDetailTap: (() -> Void)? = nil
) {
self.totalCredits = totalCredits
self.creditBreakdown = creditBreakdown
self.onInfoTap = onInfoTap
self.onDetailTap = onDetailTap
}
var body: some View {
VStack(spacing: 0) {
//
Button(action: {
onDetailTap?()
}) {
mainCreditsSection
//
if showBreakdown && !creditBreakdown.isEmpty {
creditsBreakdownSection
}
}
.background(Color(.systemBackground))
.cornerRadius(Theme.CornerRadius.medium)
.buttonStyle(PlainButtonStyle())
.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)
}
// MARK: -
private var mainCreditsSection: some View {
HStack(spacing: Theme.Spacing.lg) {
HStack(spacing: Theme.Spacing.md) {
//
HStack(spacing: Theme.Spacing.sm) {
Circle()
.fill(Theme.Gradients.primaryGradient)
.frame(width: 40, height: 40)
.overlay(
Image(systemName: "star.fill")
.foregroundColor(.white)
.font(.system(size: 18, weight: .semibold))
)
Text("Credits:")
.font(Typography.font(for: .subtitle, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
VStack(alignment: .leading, spacing: 2) {
Text("Credits")
.font(Typography.font(for: .caption, family: .quicksand))
.foregroundColor(Theme.Colors.textSecondary)
Text("\(totalCredits)")
.font(Typography.font(for: .title, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
}
Text("\(totalCredits)")
.font(Typography.font(for: .subtitle, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
}
Spacer()
//
HStack(spacing: Theme.Spacing.sm) {
//
Button(action: {
showInfoPopover = true
onInfoTap?()
}) {
Image(systemName: "info.circle")
Image(systemName: "questionmark.circle")
.foregroundColor(Theme.Colors.textSecondary)
.font(.system(size: 16))
}
// /
if !creditBreakdown.isEmpty {
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
showBreakdown.toggle()
}
}) {
Image(systemName: showBreakdown ? "chevron.up" : "chevron.down")
.foregroundColor(Theme.Colors.textSecondary)
.font(.system(size: 14, weight: .medium))
}
.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)")
.font(Typography.font(for: .caption, family: .quicksandRegular))
.multilineTextAlignment(.center)
.presentationBackground(Theme.Gradients.backgroundGradient)
.frame(minWidth: 240, maxWidth: UIScreen.main.bounds.width * 0.6)
.presentationCompactAdaptation(.popover)
.padding(.horizontal, Theme.Spacing.md)
.padding(.vertical, Theme.Spacing.sm)
}
//
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()
Text("This Period")
.font(Typography.font(for: .caption, family: .quicksand))
.foregroundColor(Theme.Colors.textSecondary)
}
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)
//
Image(systemName: "chevron.right")
.foregroundColor(Theme.Colors.textPrimary)
.font(.system(size: 14, weight: .medium))
}
}
.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: -
#Preview("Credits Info Card") {
VStack(spacing: 20) {
CreditsInfoCard(
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: {
print("Info tapped")
},
@ -302,12 +101,6 @@ struct UsageStatItem: View {
print("Detail tapped")
}
)
CreditsUsageCard(
todayUsed: 45,
weeklyUsed: 280,
monthlyUsed: 1150
)
}
.padding()
.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 {
HStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
VStack(alignment: .leading, spacing: 20) {
//
Text(status.title)
.font(.system(size: 28, weight: .bold, design: .rounded))
.font(Typography.font(for: .headline, family: .quicksandBold, size: 32))
.foregroundColor(status.textColor)
//
if case .pioneer(let expiryDate) = status {
VStack(alignment: .leading, spacing: 4) {
Text("Expires on :")
.font(.system(size: 14, weight: .medium))
.foregroundColor(status.textColor.opacity(0.8))
.font(Typography.font(for: .body, family: .quicksandRegular))
.foregroundColor(status.textColor.opacity(0.7))
Text(formatDate(expiryDate))
.font(.system(size: 16, weight: .semibold))
.font(Typography.font(for: .body, family: .quicksandRegular))
.foregroundColor(status.textColor)
}
} else {
@ -83,12 +83,12 @@ struct SubscriptionStatusBar: View {
onSubscribeTap?()
}) {
Text("Subscribe")
.font(.system(size: 14, weight: .semibold))
.font(Typography.font(for: .title, family: .quicksandRegular, size: 16))
.foregroundColor(Theme.Colors.textPrimary)
.padding(.horizontal, 20)
.padding(.vertical, 8)
.background(Theme.Colors.subscribeButton)
.cornerRadius(Theme.CornerRadius.large)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Theme.Gradients.backgroundGradient)
.cornerRadius(Theme.CornerRadius.extraLarge)
}
}
}

View File

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