Replace HStack layout with LazyVGrid for uniform card sizing, remove lineLimit(1) to allow multiline subtitles, and add 1:1 aspect ratio so all four feature cards are identical squares across all 12 themes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2427 lines
85 KiB
Swift
2427 lines
85 KiB
Swift
//
|
|
// ReflectSubscriptionStoreView.swift
|
|
// Reflect
|
|
//
|
|
// Premium subscription experience with multiple theme options.
|
|
//
|
|
|
|
import SwiftUI
|
|
import StoreKit
|
|
import os.log
|
|
|
|
struct ReflectSubscriptionStoreView: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@EnvironmentObject var iapManager: IAPManager
|
|
|
|
// Read from AppStorage to match current theme, with optional override for previews
|
|
@AppStorage(UserDefaultsStore.Keys.paywallStyle.rawValue, store: GroupUserDefaults.groupDefaults)
|
|
private var paywallStyleRaw: Int = 0
|
|
|
|
var source: String = "unknown"
|
|
var style: PaywallStyle?
|
|
|
|
private var currentStyle: PaywallStyle {
|
|
if let override = style {
|
|
return override
|
|
}
|
|
return PaywallStyle(rawValue: paywallStyleRaw) ?? .celestial
|
|
}
|
|
|
|
var body: some View {
|
|
SubscriptionStoreView(groupID: IAPManager.subscriptionGroupID) {
|
|
marketingContent
|
|
}
|
|
.subscriptionStoreControlStyle(.prominentPicker)
|
|
.storeButton(.visible, for: .restorePurchases)
|
|
.subscriptionStoreButtonLabel(.multiline)
|
|
.tint(tintColor)
|
|
.overlay(alignment: .topTrailing) {
|
|
Button {
|
|
dismiss()
|
|
} label: {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(.title2)
|
|
.symbolRenderingMode(.hierarchical)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(16)
|
|
.accessibilityLabel("Close")
|
|
}
|
|
.onAppear {
|
|
AppLogger.iap.info("SubscriptionStoreView appeared — source: \(source), productIDs: \(IAPManager.productIdentifiers.sorted().joined(separator: ", ")), groupID: \(IAPManager.subscriptionGroupID)")
|
|
AppLogger.iap.info("IAPManager state — isLoading: \(iapManager.isLoading), products loaded: \(iapManager.availableProducts.count), state: \(String(describing: iapManager.state))")
|
|
|
|
// Also try loading products directly to log what StoreKit returns
|
|
Task {
|
|
do {
|
|
let products = try await Product.products(for: IAPManager.productIdentifiers)
|
|
AppLogger.iap.info("Direct Product.products() returned \(products.count) products: \(products.map { "\($0.id) (\($0.displayName))" }.joined(separator: ", "))")
|
|
} catch {
|
|
AppLogger.iap.error("Direct Product.products() FAILED: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
AnalyticsManager.shared.trackPaywallViewed(source: source)
|
|
}
|
|
.onInAppPurchaseStart { product in
|
|
AnalyticsManager.shared.trackPurchaseStarted(productId: product.id, source: source)
|
|
}
|
|
.onInAppPurchaseCompletion { product, result in
|
|
switch result {
|
|
case .success(.success(_)):
|
|
AnalyticsManager.shared.trackPurchaseCompleted(productId: product.id, source: source)
|
|
Task { @MainActor in
|
|
await iapManager.checkSubscriptionStatus()
|
|
iapManager.trackSubscriptionAnalytics(source: "purchase_success")
|
|
}
|
|
dismiss()
|
|
case .success(.userCancelled):
|
|
AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: "user_cancelled")
|
|
case .success(.pending):
|
|
AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: "pending")
|
|
case .failure(let error):
|
|
AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: error.localizedDescription)
|
|
@unknown default:
|
|
AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: "unknown_result")
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var marketingContent: some View {
|
|
Group {
|
|
switch currentStyle {
|
|
case .celestial:
|
|
CelestialMarketingContent()
|
|
case .garden:
|
|
GardenMarketingContent()
|
|
case .neon:
|
|
NeonMarketingContent()
|
|
case .minimal:
|
|
MinimalMarketingContent()
|
|
case .zen:
|
|
ZenMarketingContent()
|
|
case .editorial:
|
|
EditorialMarketingContent()
|
|
case .mixtape:
|
|
MixtapeMarketingContent()
|
|
case .heartfelt:
|
|
HeartfeltMarketingContent()
|
|
case .luxe:
|
|
LuxeMarketingContent()
|
|
case .forecast:
|
|
ForecastMarketingContent()
|
|
case .playful:
|
|
PlayfulMarketingContent()
|
|
case .journal:
|
|
JournalMarketingContent()
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
|
|
private var tintColor: Color {
|
|
switch currentStyle {
|
|
case .celestial: return Color(red: 1.0, green: 0.4, blue: 0.5)
|
|
case .garden: return Color(red: 0.4, green: 0.75, blue: 0.45)
|
|
case .neon: return Color(red: 0.0, green: 1.0, blue: 0.8)
|
|
case .minimal: return Color(red: 0.95, green: 0.6, blue: 0.5)
|
|
case .zen: return Color(red: 0.4, green: 0.6, blue: 0.5)
|
|
case .editorial: return Color(red: 0.2, green: 0.2, blue: 0.3)
|
|
case .mixtape: return Color(red: 0.8, green: 0.5, blue: 0.3)
|
|
case .heartfelt: return Color(red: 1.0, green: 0.4, blue: 0.5)
|
|
case .luxe: return Color(red: 0.6, green: 0.7, blue: 0.9)
|
|
case .forecast: return Color(red: 0.3, green: 0.6, blue: 0.9)
|
|
case .playful: return Color(red: 0.2, green: 1.0, blue: 0.4)
|
|
case .journal: return Color(red: 0.6, green: 0.5, blue: 0.4)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 1. Celestial Theme (Aurora & Floating Orbs)
|
|
|
|
struct CelestialMarketingContent: View {
|
|
@State private var animateGradient = false
|
|
@State private var animateOrbs = false
|
|
@State private var showContent = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
CelestialBackground(animate: $animateGradient)
|
|
|
|
VStack(spacing: 0) {
|
|
EmotionOrbsView(animate: $animateOrbs)
|
|
.frame(height: 140)
|
|
.padding(.top, 20)
|
|
|
|
VStack(spacing: 16) {
|
|
Text("Understand\nYourself Deeper")
|
|
.font(.system(size: 34, weight: .bold, design: .serif))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [.white, .white.opacity(0.85)],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.shadow(color: .black.opacity(0.3), radius: 10, x: 0, y: 5)
|
|
|
|
Text("Your emotions tell a story.\nPremium helps you read it.")
|
|
.font(.system(size: 16, weight: .medium, design: .rounded))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(.white.opacity(0.7))
|
|
.lineSpacing(4)
|
|
}
|
|
.padding(.horizontal, 24)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 20)
|
|
|
|
FeatureCardsGrid(style: .celestial)
|
|
.padding(.top, 32)
|
|
.padding(.horizontal, 28)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 30)
|
|
|
|
SocialProofBadge(style: .celestial)
|
|
.padding(.top, 24)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
Spacer().frame(height: 20)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
|
|
animateGradient = true
|
|
}
|
|
withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) {
|
|
animateOrbs = true
|
|
}
|
|
withAnimation(.easeOut(duration: 0.8).delay(0.2)) {
|
|
showContent = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
animateGradient = false
|
|
animateOrbs = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 2. Garden Theme (Organic Growth & Blooming)
|
|
|
|
struct GardenMarketingContent: View {
|
|
@State private var bloomPhase = false
|
|
@State private var showContent = false
|
|
@State private var swayPhase = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
GardenBackground(bloom: $bloomPhase, sway: $swayPhase)
|
|
|
|
VStack(spacing: 0) {
|
|
// Blooming flower illustration
|
|
BloomingFlowerView(bloom: $bloomPhase)
|
|
.frame(height: 160)
|
|
.padding(.top, 10)
|
|
|
|
VStack(spacing: 16) {
|
|
Text("Watch Yourself\nBloom")
|
|
.font(.system(size: 34, weight: .bold, design: .serif))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.95, green: 0.95, blue: 0.9),
|
|
Color(red: 0.85, green: 0.9, blue: 0.8)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.shadow(color: .black.opacity(0.2), radius: 8, x: 0, y: 4)
|
|
|
|
Text("Every feeling is a seed.\nPremium helps you grow.")
|
|
.font(.system(size: 16, weight: .medium, design: .rounded))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(.white.opacity(0.75))
|
|
.lineSpacing(4)
|
|
}
|
|
.padding(.horizontal, 24)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 20)
|
|
|
|
FeatureCardsGrid(style: .garden)
|
|
.padding(.top, 28)
|
|
.padding(.horizontal, 28)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 30)
|
|
|
|
SocialProofBadge(style: .garden)
|
|
.padding(.top, 24)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
Spacer().frame(height: 20)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) {
|
|
bloomPhase = true
|
|
}
|
|
withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
|
|
swayPhase = true
|
|
}
|
|
withAnimation(.easeOut(duration: 0.8).delay(0.3)) {
|
|
showContent = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
bloomPhase = false
|
|
swayPhase = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 3. Neon Theme (Synthwave & Energy)
|
|
|
|
struct NeonMarketingContent: View {
|
|
@State private var pulsePhase = false
|
|
@State private var glowPhase = false
|
|
@State private var showContent = false
|
|
@State private var scanlineOffset: CGFloat = 0
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
NeonBackground(pulse: $pulsePhase, glow: $glowPhase)
|
|
|
|
// Scanlines overlay
|
|
NeonScanlines(offset: $scanlineOffset)
|
|
.opacity(0.03)
|
|
|
|
VStack(spacing: 0) {
|
|
// Glowing mood meter
|
|
NeonMoodMeter(pulse: $pulsePhase)
|
|
.frame(height: 140)
|
|
.padding(.top, 20)
|
|
|
|
VStack(spacing: 16) {
|
|
Text("UNLOCK YOUR\nFULL SIGNAL")
|
|
.font(.system(size: 32, weight: .black, design: .monospaced))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.0, green: 1.0, blue: 0.8),
|
|
Color(red: 1.0, green: 0.0, blue: 0.8)
|
|
],
|
|
startPoint: .leading,
|
|
endPoint: .trailing
|
|
)
|
|
)
|
|
.shadow(color: Color(red: 0.0, green: 1.0, blue: 0.8).opacity(0.5), radius: 20, x: 0, y: 0)
|
|
|
|
Text("Amplify your emotional intelligence.\nGo premium. Go limitless.")
|
|
.font(.system(size: 15, weight: .medium, design: .monospaced))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(.white.opacity(0.7))
|
|
.lineSpacing(4)
|
|
}
|
|
.padding(.horizontal, 24)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 20)
|
|
|
|
FeatureCardsGrid(style: .neon)
|
|
.padding(.top, 28)
|
|
.padding(.horizontal, 28)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 30)
|
|
|
|
SocialProofBadge(style: .neon)
|
|
.padding(.top, 24)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
Spacer().frame(height: 20)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
|
|
pulsePhase = true
|
|
}
|
|
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
|
|
glowPhase = true
|
|
}
|
|
withAnimation(.linear(duration: 8).repeatForever(autoreverses: false)) {
|
|
scanlineOffset = 400
|
|
}
|
|
withAnimation(.easeOut(duration: 0.6).delay(0.2)) {
|
|
showContent = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
pulsePhase = false
|
|
glowPhase = false
|
|
scanlineOffset = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 4. Minimal Theme (Clean & Sophisticated)
|
|
|
|
struct MinimalMarketingContent: View {
|
|
@State private var showContent = false
|
|
@State private var breathe = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
MinimalBackground()
|
|
|
|
VStack(spacing: 0) {
|
|
// Elegant breathing circle
|
|
MinimalBreathingCircle(breathe: $breathe)
|
|
.frame(height: 160)
|
|
.padding(.top, 10)
|
|
|
|
VStack(spacing: 20) {
|
|
Text("Simply\nKnow Yourself")
|
|
.font(.system(size: 36, weight: .light, design: .serif))
|
|
.italic()
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(Color(red: 0.2, green: 0.15, blue: 0.1))
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 15)
|
|
|
|
Text("Clarity through simplicity.\nPremium unlocks understanding.")
|
|
.font(.system(size: 15, weight: .regular, design: .serif))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(Color(red: 0.4, green: 0.35, blue: 0.3))
|
|
.lineSpacing(6)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 15)
|
|
}
|
|
.padding(.horizontal, 32)
|
|
|
|
FeatureCardsGrid(style: .minimal)
|
|
.padding(.top, 32)
|
|
.padding(.horizontal, 32)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 20)
|
|
|
|
SocialProofBadge(style: .minimal)
|
|
.padding(.top, 28)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
Spacer().frame(height: 20)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) {
|
|
breathe = true
|
|
}
|
|
withAnimation(.easeOut(duration: 1.0).delay(0.2)) {
|
|
showContent = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
breathe = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 5. Zen Theme (Ink Brushstrokes & Meditation)
|
|
|
|
struct ZenMarketingContent: View {
|
|
@State private var showContent = false
|
|
@State private var inkFlow = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
ZenBackground(inkFlow: $inkFlow)
|
|
|
|
VStack(spacing: 0) {
|
|
// Zen circle (ensō)
|
|
ZenEnsoView(animate: $inkFlow)
|
|
.frame(height: 160)
|
|
.padding(.top, 10)
|
|
|
|
VStack(spacing: 20) {
|
|
Text("Find Your\nInner Calm")
|
|
.font(.system(size: 34, weight: .light, design: .serif))
|
|
.italic()
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(Color(red: 0.95, green: 0.93, blue: 0.88))
|
|
.shadow(color: .black.opacity(0.3), radius: 8)
|
|
|
|
Text("Like ink on paper, each mood\nleaves its mark. Premium reveals the pattern.")
|
|
.font(.system(size: 15, weight: .regular, design: .serif))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(Color(red: 0.8, green: 0.75, blue: 0.7))
|
|
.lineSpacing(4)
|
|
}
|
|
.padding(.horizontal, 32)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 20)
|
|
|
|
FeatureCardsGrid(style: .zen)
|
|
.padding(.top, 28)
|
|
.padding(.horizontal, 28)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
SocialProofBadge(style: .zen)
|
|
.padding(.top, 24)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
Spacer().frame(height: 20)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) {
|
|
inkFlow = true
|
|
}
|
|
withAnimation(.easeOut(duration: 1.0).delay(0.3)) {
|
|
showContent = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
inkFlow = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 6. Editorial Theme (Magazine Typography)
|
|
|
|
struct EditorialMarketingContent: View {
|
|
@State private var showContent = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
EditorialBackground()
|
|
|
|
VStack(spacing: 0) {
|
|
// Large typographic element
|
|
VStack(spacing: 4) {
|
|
Text("THE")
|
|
.font(.system(size: 14, weight: .regular, design: .serif))
|
|
.tracking(8)
|
|
.foregroundColor(Color(red: 0.4, green: 0.4, blue: 0.45))
|
|
|
|
Text("REFLECT")
|
|
.font(.system(size: 52, weight: .bold, design: .serif))
|
|
.tracking(2)
|
|
.foregroundColor(Color(red: 0.15, green: 0.15, blue: 0.2))
|
|
|
|
Text("JOURNAL")
|
|
.font(.system(size: 14, weight: .light, design: .serif))
|
|
.tracking(12)
|
|
.foregroundColor(Color(red: 0.4, green: 0.4, blue: 0.45))
|
|
}
|
|
.padding(.top, 30)
|
|
.padding(.bottom, 20)
|
|
|
|
VStack(spacing: 16) {
|
|
Rectangle()
|
|
.fill(Color(red: 0.2, green: 0.2, blue: 0.25))
|
|
.frame(width: 60, height: 1)
|
|
|
|
Text("Premium Edition")
|
|
.font(.system(size: 13, weight: .medium, design: .serif))
|
|
.italic()
|
|
.foregroundColor(Color(red: 0.5, green: 0.5, blue: 0.55))
|
|
|
|
Text("Unlock the complete story\nof your emotional landscape.")
|
|
.font(.system(size: 16, weight: .regular, design: .serif))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(Color(red: 0.3, green: 0.3, blue: 0.35))
|
|
.lineSpacing(6)
|
|
}
|
|
.padding(.horizontal, 40)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
FeatureCardsGrid(style: .editorial)
|
|
.padding(.top, 28)
|
|
.padding(.horizontal, 28)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
SocialProofBadge(style: .editorial)
|
|
.padding(.top, 24)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
Spacer().frame(height: 20)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeOut(duration: 0.8).delay(0.2)) {
|
|
showContent = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 7. Mixtape Theme (Cassette & Retro Analog)
|
|
|
|
struct MixtapeMarketingContent: View {
|
|
@State private var showContent = false
|
|
@State private var tapeRotation = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
MixtapeBackground()
|
|
|
|
VStack(spacing: 0) {
|
|
// Cassette tape illustration
|
|
CassetteTapeView(rotating: $tapeRotation)
|
|
.frame(height: 150)
|
|
.padding(.top, 15)
|
|
|
|
VStack(spacing: 16) {
|
|
Text("YOUR MOOD\nMIXTAPE")
|
|
.font(.system(size: 30, weight: .bold, design: .rounded))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 0.85, blue: 0.6),
|
|
Color(red: 0.9, green: 0.6, blue: 0.4)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
|
|
Text("Every feeling is a track.\nPremium gives you the full album.")
|
|
.font(.system(size: 15, weight: .medium, design: .rounded))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(Color(red: 0.85, green: 0.75, blue: 0.65))
|
|
.lineSpacing(4)
|
|
}
|
|
.padding(.horizontal, 32)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 20)
|
|
|
|
FeatureCardsGrid(style: .mixtape)
|
|
.padding(.top, 28)
|
|
.padding(.horizontal, 28)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
SocialProofBadge(style: .mixtape)
|
|
.padding(.top, 24)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
Spacer().frame(height: 20)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.linear(duration: 3).repeatForever(autoreverses: false)) {
|
|
tapeRotation = true
|
|
}
|
|
withAnimation(.easeOut(duration: 0.8).delay(0.2)) {
|
|
showContent = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
tapeRotation = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 8. Heartfelt Theme (Hearts & Emotion)
|
|
|
|
struct HeartfeltMarketingContent: View {
|
|
@State private var showContent = false
|
|
@State private var heartbeat = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
HeartfeltBackground(beat: $heartbeat)
|
|
|
|
VStack(spacing: 0) {
|
|
// Floating hearts
|
|
FloatingHeartsView(beat: $heartbeat)
|
|
.frame(height: 150)
|
|
.padding(.top, 15)
|
|
|
|
VStack(spacing: 16) {
|
|
Text("Feel It All")
|
|
.font(.system(size: 38, weight: .bold, design: .rounded))
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 0.6, blue: 0.7),
|
|
Color(red: 1.0, green: 0.4, blue: 0.5)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.shadow(color: Color(red: 1.0, green: 0.4, blue: 0.5).opacity(0.3), radius: 15)
|
|
|
|
Text("Your heart knows the way.\nPremium helps you listen.")
|
|
.font(.system(size: 16, weight: .medium, design: .rounded))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(.white.opacity(0.8))
|
|
.lineSpacing(4)
|
|
}
|
|
.padding(.horizontal, 32)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 20)
|
|
|
|
FeatureCardsGrid(style: .heartfelt)
|
|
.padding(.top, 28)
|
|
.padding(.horizontal, 28)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
SocialProofBadge(style: .heartfelt)
|
|
.padding(.top, 24)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
Spacer().frame(height: 20)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) {
|
|
heartbeat = true
|
|
}
|
|
withAnimation(.easeOut(duration: 0.8).delay(0.2)) {
|
|
showContent = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
heartbeat = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 9. Luxe Theme (Premium Glass Materials)
|
|
|
|
struct LuxeMarketingContent: View {
|
|
@State private var showContent = false
|
|
@State private var shimmer = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
LuxeBackground(shimmer: $shimmer)
|
|
|
|
VStack(spacing: 0) {
|
|
// Diamond/gem icon
|
|
LuxeGemView(shimmer: $shimmer)
|
|
.frame(height: 150)
|
|
.padding(.top, 15)
|
|
|
|
VStack(spacing: 16) {
|
|
Text("ELEVATE YOUR\nEXPERIENCE")
|
|
.font(.system(size: 28, weight: .semibold, design: .rounded))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [.white, Color(red: 0.85, green: 0.9, blue: 1.0)],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.shadow(color: .white.opacity(0.3), radius: 10)
|
|
|
|
Text("Premium refinement for those\nwho expect the finest.")
|
|
.font(.system(size: 15, weight: .regular, design: .rounded))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(.white.opacity(0.7))
|
|
.lineSpacing(4)
|
|
}
|
|
.padding(.horizontal, 32)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 20)
|
|
|
|
FeatureCardsGrid(style: .luxe)
|
|
.padding(.top, 28)
|
|
.padding(.horizontal, 28)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
SocialProofBadge(style: .luxe)
|
|
.padding(.top, 24)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
Spacer().frame(height: 20)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
|
|
shimmer = true
|
|
}
|
|
withAnimation(.easeOut(duration: 0.8).delay(0.2)) {
|
|
showContent = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
shimmer = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 10. Forecast Theme (Weather Metaphors)
|
|
|
|
struct ForecastMarketingContent: View {
|
|
@State private var showContent = false
|
|
@State private var cloudDrift = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
ForecastBackground(drift: $cloudDrift)
|
|
|
|
VStack(spacing: 0) {
|
|
// Weather icons
|
|
WeatherIconsView(drift: $cloudDrift)
|
|
.frame(height: 150)
|
|
.padding(.top, 15)
|
|
|
|
VStack(spacing: 16) {
|
|
Text("Your Emotional\nForecast")
|
|
.font(.system(size: 34, weight: .bold, design: .rounded))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.95, green: 0.95, blue: 1.0),
|
|
Color(red: 0.7, green: 0.85, blue: 1.0)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
|
|
Text("Predict your patterns.\nPrepare for any weather.")
|
|
.font(.system(size: 16, weight: .medium, design: .rounded))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(.white.opacity(0.75))
|
|
.lineSpacing(4)
|
|
}
|
|
.padding(.horizontal, 32)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 20)
|
|
|
|
FeatureCardsGrid(style: .forecast)
|
|
.padding(.top, 28)
|
|
.padding(.horizontal, 28)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
SocialProofBadge(style: .forecast)
|
|
.padding(.top, 24)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
Spacer().frame(height: 20)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) {
|
|
cloudDrift = true
|
|
}
|
|
withAnimation(.easeOut(duration: 0.8).delay(0.2)) {
|
|
showContent = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
cloudDrift = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 11. Playful Theme (Vibrant & Fun)
|
|
|
|
struct PlayfulMarketingContent: View {
|
|
@State private var showContent = false
|
|
@State private var bounce = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
PlayfulBackground(bounce: $bounce)
|
|
|
|
VStack(spacing: 0) {
|
|
// Bouncing emoji faces
|
|
PlayfulEmojisView(bounce: $bounce)
|
|
.frame(height: 150)
|
|
.padding(.top, 15)
|
|
|
|
VStack(spacing: 16) {
|
|
Text("MOOD UNLOCKED! 🎮")
|
|
.font(.system(size: 30, weight: .black, design: .rounded))
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.2, green: 1.0, blue: 0.4),
|
|
Color(red: 1.0, green: 1.0, blue: 0.2)
|
|
],
|
|
startPoint: .leading,
|
|
endPoint: .trailing
|
|
)
|
|
)
|
|
.shadow(color: Color(red: 0.2, green: 1.0, blue: 0.4).opacity(0.5), radius: 15)
|
|
|
|
Text("Level up your self-awareness.\nPremium = MAX POWER!")
|
|
.font(.system(size: 15, weight: .bold, design: .rounded))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(.white.opacity(0.85))
|
|
.lineSpacing(4)
|
|
}
|
|
.padding(.horizontal, 32)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 20)
|
|
|
|
FeatureCardsGrid(style: .playful)
|
|
.padding(.top, 28)
|
|
.padding(.horizontal, 28)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
SocialProofBadge(style: .playful)
|
|
.padding(.top, 24)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
Spacer().frame(height: 20)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true)) {
|
|
bounce = true
|
|
}
|
|
withAnimation(.easeOut(duration: 0.6).delay(0.1)) {
|
|
showContent = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
bounce = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 12. Journal Theme (Handwritten Paper)
|
|
|
|
struct JournalMarketingContent: View {
|
|
@State private var showContent = false
|
|
@State private var pageFlip = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
JournalBackground()
|
|
|
|
VStack(spacing: 0) {
|
|
// Stacked paper pages
|
|
JournalPagesView(flip: $pageFlip)
|
|
.frame(height: 150)
|
|
.padding(.top, 15)
|
|
|
|
VStack(spacing: 16) {
|
|
Text("Your Personal\nDiary")
|
|
.font(.custom("Georgia", size: 34))
|
|
.italic()
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(Color(red: 0.35, green: 0.25, blue: 0.2))
|
|
|
|
Text("Every entry tells your story.\nPremium unlocks the full narrative.")
|
|
.font(.custom("Georgia", size: 15))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(Color(red: 0.5, green: 0.4, blue: 0.35))
|
|
.lineSpacing(6)
|
|
}
|
|
.padding(.horizontal, 40)
|
|
.opacity(showContent ? 1 : 0)
|
|
.offset(y: showContent ? 0 : 15)
|
|
|
|
FeatureCardsGrid(style: .journal)
|
|
.padding(.top, 28)
|
|
.padding(.horizontal, 28)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
SocialProofBadge(style: .journal)
|
|
.padding(.top, 24)
|
|
.opacity(showContent ? 1 : 0)
|
|
|
|
Spacer().frame(height: 20)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
|
|
pageFlip = true
|
|
}
|
|
withAnimation(.easeOut(duration: 1.0).delay(0.2)) {
|
|
showContent = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
pageFlip = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Background Views
|
|
|
|
struct CelestialBackground: View {
|
|
@Binding var animate: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.05, green: 0.05, blue: 0.12),
|
|
Color(red: 0.08, green: 0.06, blue: 0.15),
|
|
Color(red: 0.04, green: 0.04, blue: 0.1)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 0.4, blue: 0.3).opacity(0.4),
|
|
Color(red: 1.0, green: 0.6, blue: 0.4).opacity(0.2),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.8
|
|
)
|
|
.frame(width: 400, height: 300)
|
|
.offset(x: animate ? 30 : -30, y: animate ? -50 : -80)
|
|
.blur(radius: 60)
|
|
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color(red: 0.4, green: 0.3, blue: 0.9).opacity(0.3),
|
|
Color(red: 0.3, green: 0.5, blue: 0.8).opacity(0.15),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.7
|
|
)
|
|
.frame(width: 350, height: 250)
|
|
.offset(x: animate ? -40 : 20, y: animate ? 100 : 60)
|
|
.blur(radius: 50)
|
|
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color(red: 0.9, green: 0.3, blue: 0.5).opacity(0.25),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.6
|
|
)
|
|
.frame(width: 300, height: 200)
|
|
.offset(x: animate ? 60 : -20, y: animate ? -20 : 40)
|
|
.blur(radius: 40)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct GardenBackground: View {
|
|
@Binding var bloom: Bool
|
|
@Binding var sway: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Deep forest gradient
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.05, green: 0.12, blue: 0.08),
|
|
Color(red: 0.08, green: 0.18, blue: 0.1),
|
|
Color(red: 0.04, green: 0.1, blue: 0.06)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Soft green glow
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color(red: 0.3, green: 0.7, blue: 0.4).opacity(0.25),
|
|
Color(red: 0.2, green: 0.5, blue: 0.3).opacity(0.1),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.8
|
|
)
|
|
.frame(width: 400, height: 400)
|
|
.offset(y: bloom ? -20 : 20)
|
|
.blur(radius: 80)
|
|
|
|
// Warm accent
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 0.8, blue: 0.5).opacity(0.15),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.5
|
|
)
|
|
.frame(width: 300, height: 200)
|
|
.offset(x: sway ? 40 : -40, y: -100)
|
|
.blur(radius: 60)
|
|
|
|
// Floating leaves particles
|
|
ForEach(0..<8, id: \.self) { i in
|
|
LeafParticle(index: i, sway: sway)
|
|
}
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct LeafParticle: View {
|
|
let index: Int
|
|
let sway: Bool
|
|
|
|
var body: some View {
|
|
Circle()
|
|
.fill(Color(red: 0.4, green: 0.7, blue: 0.4).opacity(0.15))
|
|
.frame(width: CGFloat.random(in: 4...12), height: CGFloat.random(in: 4...12))
|
|
.offset(
|
|
x: CGFloat(index * 40 - 140) + (sway ? 10 : -10),
|
|
y: CGFloat(index * 30 - 100)
|
|
)
|
|
.blur(radius: 2)
|
|
}
|
|
}
|
|
|
|
struct NeonBackground: View {
|
|
@Binding var pulse: Bool
|
|
@Binding var glow: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Deep dark base
|
|
Color(red: 0.02, green: 0.02, blue: 0.05)
|
|
|
|
// Grid lines
|
|
NeonGrid()
|
|
.opacity(0.3)
|
|
|
|
// Cyan glow
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color(red: 0.0, green: 1.0, blue: 0.8).opacity(pulse ? 0.3 : 0.15),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.6
|
|
)
|
|
.frame(width: 400, height: 300)
|
|
.offset(y: -80)
|
|
.blur(radius: 60)
|
|
|
|
// Magenta glow
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 0.0, blue: 0.8).opacity(glow ? 0.25 : 0.1),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.5
|
|
)
|
|
.frame(width: 350, height: 250)
|
|
.offset(x: 50, y: 100)
|
|
.blur(radius: 50)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct NeonGrid: View {
|
|
var body: some View {
|
|
Canvas { context, size in
|
|
let gridSpacing: CGFloat = 30
|
|
let lineColor = Color(red: 0.0, green: 0.8, blue: 0.8).opacity(0.15)
|
|
|
|
// Horizontal lines
|
|
for y in stride(from: 0, to: size.height, by: gridSpacing) {
|
|
var path = Path()
|
|
path.move(to: CGPoint(x: 0, y: y))
|
|
path.addLine(to: CGPoint(x: size.width, y: y))
|
|
context.stroke(path, with: .color(lineColor), lineWidth: 0.5)
|
|
}
|
|
|
|
// Vertical lines
|
|
for x in stride(from: 0, to: size.width, by: gridSpacing) {
|
|
var path = Path()
|
|
path.move(to: CGPoint(x: x, y: 0))
|
|
path.addLine(to: CGPoint(x: x, y: size.height))
|
|
context.stroke(path, with: .color(lineColor), lineWidth: 0.5)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct NeonScanlines: View {
|
|
@Binding var offset: CGFloat
|
|
|
|
var body: some View {
|
|
GeometryReader { geo in
|
|
ForEach(0..<20, id: \.self) { i in
|
|
Rectangle()
|
|
.fill(Color.white)
|
|
.frame(height: 1)
|
|
.offset(y: CGFloat(i * 20) + offset.truncatingRemainder(dividingBy: 400))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MinimalBackground: View {
|
|
var body: some View {
|
|
ZStack {
|
|
// Warm cream gradient
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.98, green: 0.96, blue: 0.92),
|
|
Color(red: 0.95, green: 0.93, blue: 0.88),
|
|
Color(red: 0.92, green: 0.90, blue: 0.85)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Subtle warm accent
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color(red: 0.95, green: 0.85, blue: 0.75).opacity(0.4),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.6
|
|
)
|
|
.frame(width: 500, height: 400)
|
|
.offset(y: -50)
|
|
.blur(radius: 100)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct ZenBackground: View {
|
|
@Binding var inkFlow: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Dark paper texture
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.12, green: 0.1, blue: 0.08),
|
|
Color(red: 0.08, green: 0.07, blue: 0.06),
|
|
Color(red: 0.1, green: 0.08, blue: 0.06)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Subtle ink wash
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color(red: 0.3, green: 0.4, blue: 0.35).opacity(inkFlow ? 0.2 : 0.1),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.7
|
|
)
|
|
.frame(width: 400, height: 400)
|
|
.blur(radius: 80)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct EditorialBackground: View {
|
|
var body: some View {
|
|
ZStack {
|
|
// Clean off-white
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.98, green: 0.97, blue: 0.95),
|
|
Color(red: 0.96, green: 0.95, blue: 0.93),
|
|
Color(red: 0.94, green: 0.93, blue: 0.91)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct MixtapeBackground: View {
|
|
var body: some View {
|
|
ZStack {
|
|
// Warm brown gradient
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.15, green: 0.1, blue: 0.08),
|
|
Color(red: 0.12, green: 0.08, blue: 0.06),
|
|
Color(red: 0.1, green: 0.07, blue: 0.05)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Warm orange glow
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color(red: 0.8, green: 0.4, blue: 0.2).opacity(0.15),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.6
|
|
)
|
|
.frame(width: 400, height: 300)
|
|
.offset(y: -50)
|
|
.blur(radius: 60)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct HeartfeltBackground: View {
|
|
@Binding var beat: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Warm pink gradient
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.15, green: 0.05, blue: 0.08),
|
|
Color(red: 0.12, green: 0.04, blue: 0.06),
|
|
Color(red: 0.1, green: 0.03, blue: 0.05)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Pink glow
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 0.3, blue: 0.5).opacity(beat ? 0.25 : 0.15),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.7
|
|
)
|
|
.frame(width: 400, height: 400)
|
|
.blur(radius: 80)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct LuxeBackground: View {
|
|
@Binding var shimmer: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Deep blue-black
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.06, green: 0.08, blue: 0.12),
|
|
Color(red: 0.04, green: 0.05, blue: 0.08),
|
|
Color(red: 0.03, green: 0.04, blue: 0.06)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Subtle blue shimmer
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color(red: 0.4, green: 0.5, blue: 0.8).opacity(shimmer ? 0.2 : 0.1),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.6
|
|
)
|
|
.frame(width: 400, height: 300)
|
|
.offset(y: shimmer ? -30 : -60)
|
|
.blur(radius: 60)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct ForecastBackground: View {
|
|
@Binding var drift: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Sky gradient
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.15, green: 0.25, blue: 0.4),
|
|
Color(red: 0.1, green: 0.18, blue: 0.3),
|
|
Color(red: 0.08, green: 0.12, blue: 0.2)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Cloud-like glow
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color.white.opacity(0.1),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.5
|
|
)
|
|
.frame(width: 350, height: 200)
|
|
.offset(x: drift ? 30 : -30, y: -80)
|
|
.blur(radius: 50)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct PlayfulBackground: View {
|
|
@Binding var bounce: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Deep purple-black
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.08, green: 0.05, blue: 0.12),
|
|
Color(red: 0.05, green: 0.03, blue: 0.08),
|
|
Color(red: 0.04, green: 0.02, blue: 0.06)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Green glow
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color(red: 0.2, green: 1.0, blue: 0.4).opacity(bounce ? 0.2 : 0.1),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.5
|
|
)
|
|
.frame(width: 300, height: 300)
|
|
.offset(x: -50, y: -50)
|
|
.blur(radius: 60)
|
|
|
|
// Yellow glow
|
|
EllipticalGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 1.0, blue: 0.2).opacity(0.1),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadiusFraction: 0,
|
|
endRadiusFraction: 0.4
|
|
)
|
|
.frame(width: 250, height: 250)
|
|
.offset(x: 60, y: 100)
|
|
.blur(radius: 50)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct JournalBackground: View {
|
|
var body: some View {
|
|
ZStack {
|
|
// Warm paper
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 0.98, blue: 0.94),
|
|
Color(red: 0.98, green: 0.95, blue: 0.88),
|
|
Color(red: 0.95, green: 0.92, blue: 0.85)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Paper texture lines
|
|
VStack(spacing: 28) {
|
|
ForEach(0..<20, id: \.self) { _ in
|
|
Rectangle()
|
|
.fill(Color(red: 0.85, green: 0.82, blue: 0.78).opacity(0.3))
|
|
.frame(height: 1)
|
|
}
|
|
}
|
|
.padding(.horizontal, 40)
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
// MARK: - Decorative Elements
|
|
|
|
struct EmotionOrbsView: View {
|
|
@Binding var animate: Bool
|
|
|
|
private let emotions: [(color: Color, size: CGFloat, xOffset: CGFloat, yOffset: CGFloat)] = [
|
|
(Color(red: 1.0, green: 0.8, blue: 0.3), 56, -90, 20),
|
|
(Color(red: 0.4, green: 0.8, blue: 0.6), 44, -30, -30),
|
|
(Color(red: 1.0, green: 0.5, blue: 0.5), 52, 40, 10),
|
|
(Color(red: 0.6, green: 0.5, blue: 0.9), 40, 95, -20),
|
|
(Color(red: 0.3, green: 0.7, blue: 1.0), 36, 60, 50),
|
|
]
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
ForEach(0..<emotions.count, id: \.self) { index in
|
|
let emotion = emotions[index]
|
|
EmotionOrb(
|
|
color: emotion.color,
|
|
size: emotion.size,
|
|
delay: Double(index) * 0.15
|
|
)
|
|
.offset(
|
|
x: emotion.xOffset + (animate ? CGFloat.random(in: -8...8) : 0),
|
|
y: emotion.yOffset + (animate ? CGFloat.random(in: -8...8) : 0)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct EmotionOrb: View {
|
|
let color: Color
|
|
let size: CGFloat
|
|
let delay: Double
|
|
|
|
@State private var pulse = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [color.opacity(0.6), color.opacity(0.2), Color.clear],
|
|
center: .center,
|
|
startRadius: 0,
|
|
endRadius: size
|
|
)
|
|
)
|
|
.frame(width: size * 2, height: size * 2)
|
|
.blur(radius: 15)
|
|
.scaleEffect(pulse ? 1.2 : 1.0)
|
|
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [color, color.opacity(0.8)],
|
|
center: .topLeading,
|
|
startRadius: 0,
|
|
endRadius: size * 0.6
|
|
)
|
|
)
|
|
.frame(width: size, height: size)
|
|
.shadow(color: color.opacity(0.8), radius: 15, x: 0, y: 5)
|
|
|
|
Circle()
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [.white.opacity(0.5), .clear],
|
|
startPoint: .topLeading,
|
|
endPoint: .center
|
|
)
|
|
)
|
|
.frame(width: size * 0.4, height: size * 0.4)
|
|
.offset(x: -size * 0.15, y: -size * 0.15)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 2.5).repeatForever(autoreverses: true).delay(delay)) {
|
|
pulse = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
pulse = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct BloomingFlowerView: View {
|
|
@Binding var bloom: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Petals
|
|
ForEach(0..<8, id: \.self) { i in
|
|
Petal(index: i, bloom: bloom)
|
|
}
|
|
|
|
// Center
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 0.9, blue: 0.6),
|
|
Color(red: 0.9, green: 0.7, blue: 0.4)
|
|
],
|
|
center: .center,
|
|
startRadius: 0,
|
|
endRadius: 20
|
|
)
|
|
)
|
|
.frame(width: 40, height: 40)
|
|
.shadow(color: Color(red: 1.0, green: 0.8, blue: 0.4).opacity(0.5), radius: 15)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Petal: View {
|
|
let index: Int
|
|
let bloom: Bool
|
|
|
|
var body: some View {
|
|
let angle = Double(index) * .pi / 4
|
|
let petalColor = petalColors[index % petalColors.count]
|
|
|
|
Ellipse()
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [petalColor, petalColor.opacity(0.7)],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.frame(width: 28, height: bloom ? 55 : 40)
|
|
.offset(y: bloom ? -45 : -35)
|
|
.rotationEffect(.radians(angle))
|
|
.shadow(color: petalColor.opacity(0.4), radius: 8)
|
|
}
|
|
|
|
private var petalColors: [Color] {
|
|
[
|
|
Color(red: 1.0, green: 0.6, blue: 0.7),
|
|
Color(red: 0.9, green: 0.5, blue: 0.6),
|
|
Color(red: 1.0, green: 0.7, blue: 0.75),
|
|
Color(red: 0.95, green: 0.55, blue: 0.65),
|
|
Color(red: 1.0, green: 0.65, blue: 0.7),
|
|
Color(red: 0.9, green: 0.6, blue: 0.65),
|
|
Color(red: 1.0, green: 0.7, blue: 0.8),
|
|
Color(red: 0.95, green: 0.5, blue: 0.6)
|
|
]
|
|
}
|
|
}
|
|
|
|
struct NeonMoodMeter: View {
|
|
@Binding var pulse: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Outer ring glow
|
|
Circle()
|
|
.stroke(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.0, green: 1.0, blue: 0.8),
|
|
Color(red: 1.0, green: 0.0, blue: 0.8)
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
),
|
|
lineWidth: 4
|
|
)
|
|
.frame(width: 100, height: 100)
|
|
.shadow(color: Color(red: 0.0, green: 1.0, blue: 0.8).opacity(0.6), radius: pulse ? 20 : 10)
|
|
.shadow(color: Color(red: 1.0, green: 0.0, blue: 0.8).opacity(0.4), radius: pulse ? 25 : 15)
|
|
|
|
// Inner pulsing core
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [
|
|
Color(red: 0.0, green: 1.0, blue: 0.8).opacity(0.8),
|
|
Color(red: 0.0, green: 0.5, blue: 0.4).opacity(0.4),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadius: 0,
|
|
endRadius: 40
|
|
)
|
|
)
|
|
.frame(width: 80, height: 80)
|
|
.scaleEffect(pulse ? 1.1 : 0.9)
|
|
|
|
// Center icon
|
|
Image(systemName: "waveform.path.ecg")
|
|
.font(.system(size: 32, weight: .bold))
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.0, green: 1.0, blue: 0.8),
|
|
Color(red: 1.0, green: 0.0, blue: 0.8)
|
|
],
|
|
startPoint: .leading,
|
|
endPoint: .trailing
|
|
)
|
|
)
|
|
.shadow(color: Color(red: 0.0, green: 1.0, blue: 0.8), radius: 10)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MinimalBreathingCircle: View {
|
|
@Binding var breathe: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Outer ring
|
|
Circle()
|
|
.stroke(
|
|
Color(red: 0.8, green: 0.7, blue: 0.6).opacity(0.3),
|
|
lineWidth: 1
|
|
)
|
|
.frame(width: breathe ? 120 : 100, height: breathe ? 120 : 100)
|
|
|
|
// Middle ring
|
|
Circle()
|
|
.stroke(
|
|
Color(red: 0.7, green: 0.6, blue: 0.5).opacity(0.4),
|
|
lineWidth: 1
|
|
)
|
|
.frame(width: breathe ? 90 : 75, height: breathe ? 90 : 75)
|
|
|
|
// Inner filled circle
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [
|
|
Color(red: 0.95, green: 0.6, blue: 0.5).opacity(0.6),
|
|
Color(red: 0.9, green: 0.5, blue: 0.4).opacity(0.3),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadius: 0,
|
|
endRadius: 30
|
|
)
|
|
)
|
|
.frame(width: 60, height: 60)
|
|
.scaleEffect(breathe ? 1.1 : 0.95)
|
|
|
|
// Center dot
|
|
Circle()
|
|
.fill(Color(red: 0.85, green: 0.5, blue: 0.4))
|
|
.frame(width: 12, height: 12)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Theme-Specific Decorative Elements
|
|
|
|
struct ZenEnsoView: View {
|
|
@Binding var animate: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Ensō circle (imperfect zen circle)
|
|
Circle()
|
|
.trim(from: 0, to: animate ? 0.85 : 0.8)
|
|
.stroke(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.6, green: 0.55, blue: 0.5),
|
|
Color(red: 0.4, green: 0.35, blue: 0.3)
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
),
|
|
style: StrokeStyle(lineWidth: 8, lineCap: .round)
|
|
)
|
|
.frame(width: 100, height: 100)
|
|
.rotationEffect(.degrees(-90))
|
|
.shadow(color: Color(red: 0.6, green: 0.55, blue: 0.5).opacity(0.3), radius: 10)
|
|
|
|
// Inner dot
|
|
Circle()
|
|
.fill(Color(red: 0.5, green: 0.45, blue: 0.4))
|
|
.frame(width: 8, height: 8)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CassetteTapeView: View {
|
|
@Binding var rotating: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Tape body
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.fill(Color(red: 0.25, green: 0.2, blue: 0.15))
|
|
.frame(width: 140, height: 90)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.stroke(Color(red: 0.4, green: 0.3, blue: 0.2), lineWidth: 2)
|
|
)
|
|
|
|
// Tape window
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.fill(Color(red: 0.15, green: 0.12, blue: 0.1))
|
|
.frame(width: 100, height: 40)
|
|
|
|
// Reels
|
|
HStack(spacing: 40) {
|
|
Circle()
|
|
.fill(Color(red: 0.3, green: 0.25, blue: 0.2))
|
|
.frame(width: 30, height: 30)
|
|
.overlay(
|
|
Circle()
|
|
.stroke(Color(red: 0.5, green: 0.4, blue: 0.3), lineWidth: 1)
|
|
)
|
|
.rotationEffect(.degrees(rotating ? 360 : 0))
|
|
|
|
Circle()
|
|
.fill(Color(red: 0.3, green: 0.25, blue: 0.2))
|
|
.frame(width: 30, height: 30)
|
|
.overlay(
|
|
Circle()
|
|
.stroke(Color(red: 0.5, green: 0.4, blue: 0.3), lineWidth: 1)
|
|
)
|
|
.rotationEffect(.degrees(rotating ? 360 : 0))
|
|
}
|
|
|
|
// Label
|
|
Text("REFLECT MIXTAPE")
|
|
.font(.system(size: 8, weight: .bold, design: .monospaced))
|
|
.foregroundColor(Color(red: 0.9, green: 0.8, blue: 0.6))
|
|
.offset(y: 32)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct FloatingHeartsView: View {
|
|
@Binding var beat: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
ForEach(0..<5, id: \.self) { index in
|
|
Image(systemName: "heart.fill")
|
|
.font(.system(size: CGFloat(20 + index * 8)))
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 0.5 + Double(index) * 0.1, blue: 0.6 + Double(index) * 0.05),
|
|
Color(red: 1.0, green: 0.3, blue: 0.4)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.offset(
|
|
x: CGFloat(index * 25 - 50),
|
|
y: CGFloat(index % 2 == 0 ? -10 : 10) + (beat ? -5 : 5)
|
|
)
|
|
.shadow(color: Color(red: 1.0, green: 0.4, blue: 0.5).opacity(0.4), radius: 10)
|
|
.scaleEffect(beat ? 1.1 : 0.95)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct LuxeGemView: View {
|
|
@Binding var shimmer: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Outer glow
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [
|
|
Color(red: 0.6, green: 0.7, blue: 1.0).opacity(shimmer ? 0.3 : 0.15),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadius: 0,
|
|
endRadius: 60
|
|
)
|
|
)
|
|
.frame(width: 120, height: 120)
|
|
|
|
// Diamond shape
|
|
Image(systemName: "diamond.fill")
|
|
.font(.system(size: 60))
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [
|
|
Color.white,
|
|
Color(red: 0.8, green: 0.85, blue: 1.0),
|
|
Color(red: 0.6, green: 0.7, blue: 0.9)
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
)
|
|
.shadow(color: Color(red: 0.6, green: 0.7, blue: 1.0).opacity(0.5), radius: shimmer ? 20 : 10)
|
|
.scaleEffect(shimmer ? 1.05 : 1.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct WeatherIconsView: View {
|
|
@Binding var drift: Bool
|
|
|
|
var body: some View {
|
|
HStack(spacing: 20) {
|
|
Image(systemName: "cloud.sun.fill")
|
|
.font(.system(size: 40))
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [Color.yellow, Color.orange],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.offset(y: drift ? -5 : 5)
|
|
|
|
Image(systemName: "sun.max.fill")
|
|
.font(.system(size: 50))
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [Color.yellow, Color.orange],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.shadow(color: Color.yellow.opacity(0.5), radius: 15)
|
|
|
|
Image(systemName: "cloud.rain.fill")
|
|
.font(.system(size: 40))
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [Color.white, Color.blue.opacity(0.7)],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.offset(y: drift ? 5 : -5)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PlayfulEmojisView: View {
|
|
@Binding var bounce: Bool
|
|
|
|
var body: some View {
|
|
HStack(spacing: 15) {
|
|
Text("😄")
|
|
.font(.system(size: 40))
|
|
.offset(y: bounce ? -10 : 0)
|
|
|
|
Text("🎉")
|
|
.font(.system(size: 35))
|
|
.offset(y: bounce ? 0 : -10)
|
|
|
|
Text("✨")
|
|
.font(.system(size: 45))
|
|
.offset(y: bounce ? -15 : 5)
|
|
|
|
Text("🌈")
|
|
.font(.system(size: 35))
|
|
.offset(y: bounce ? 5 : -5)
|
|
|
|
Text("💫")
|
|
.font(.system(size: 40))
|
|
.offset(y: bounce ? -5 : 10)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct JournalPagesView: View {
|
|
@Binding var flip: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Back pages
|
|
ForEach(0..<3, id: \.self) { index in
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.fill(Color(red: 1.0, green: 0.98, blue: 0.94))
|
|
.frame(width: 100 - CGFloat(index * 5), height: 120 - CGFloat(index * 5))
|
|
.offset(x: CGFloat(index * 3), y: CGFloat(index * 3))
|
|
.shadow(color: Color.black.opacity(0.1), radius: 2, x: 1, y: 1)
|
|
}
|
|
|
|
// Front page with lines
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.fill(Color(red: 1.0, green: 0.98, blue: 0.94))
|
|
.frame(width: 100, height: 120)
|
|
.overlay(
|
|
VStack(spacing: 10) {
|
|
ForEach(0..<6, id: \.self) { _ in
|
|
Rectangle()
|
|
.fill(Color(red: 0.8, green: 0.75, blue: 0.7).opacity(0.5))
|
|
.frame(height: 1)
|
|
}
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 15)
|
|
)
|
|
.shadow(color: Color.black.opacity(0.15), radius: 4, x: 2, y: 2)
|
|
.rotation3DEffect(
|
|
.degrees(flip ? 5 : -5),
|
|
axis: (x: 0, y: 1, z: 0)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Feature Cards
|
|
|
|
struct FeatureCardsGrid: View {
|
|
let style: PaywallStyle
|
|
|
|
private let columns = [
|
|
GridItem(.flexible(), spacing: 12),
|
|
GridItem(.flexible(), spacing: 12)
|
|
]
|
|
|
|
var body: some View {
|
|
LazyVGrid(columns: columns, spacing: 12) {
|
|
FeatureCard(
|
|
icon: "chart.line.uptrend.xyaxis",
|
|
title: "See Patterns",
|
|
subtitle: "Month & year views",
|
|
style: style,
|
|
accentIndex: 0
|
|
)
|
|
FeatureCard(
|
|
icon: "sparkles",
|
|
title: "AI Insights",
|
|
subtitle: "Understand your moods",
|
|
style: style,
|
|
accentIndex: 1
|
|
)
|
|
FeatureCard(
|
|
icon: "heart.circle",
|
|
title: "Health Sync",
|
|
subtitle: "Connect your data",
|
|
style: style,
|
|
accentIndex: 2
|
|
)
|
|
FeatureCard(
|
|
icon: "rectangle.grid.2x2",
|
|
title: "Widgets",
|
|
subtitle: "Always visible",
|
|
style: style,
|
|
accentIndex: 3
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct FeatureCard: View {
|
|
let icon: String
|
|
let title: String
|
|
let subtitle: String
|
|
let style: PaywallStyle
|
|
let accentIndex: Int
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(accentColor.opacity(style == .minimal ? 0.15 : 0.2))
|
|
.frame(width: 36, height: 36)
|
|
|
|
Image(systemName: icon)
|
|
.font(.system(size: 16, weight: .semibold))
|
|
.foregroundColor(accentColor)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(title)
|
|
.font(titleFont)
|
|
.foregroundColor(titleColor)
|
|
|
|
Text(subtitle)
|
|
.font(.system(size: 11, weight: .medium))
|
|
.foregroundColor(subtitleColor)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
Spacer(minLength: 0)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(14)
|
|
.aspectRatio(1, contentMode: .fill)
|
|
.background(cardBackground)
|
|
}
|
|
|
|
private var accentColor: Color {
|
|
let colors: [[Color]] = [
|
|
// 0: Celestial
|
|
[Color(red: 1.0, green: 0.6, blue: 0.4), Color(red: 0.6, green: 0.5, blue: 1.0),
|
|
Color(red: 1.0, green: 0.4, blue: 0.5), Color(red: 0.4, green: 0.8, blue: 0.7)],
|
|
// 1: Garden
|
|
[Color(red: 0.5, green: 0.8, blue: 0.5), Color(red: 0.7, green: 0.6, blue: 0.9),
|
|
Color(red: 1.0, green: 0.7, blue: 0.7), Color(red: 0.6, green: 0.75, blue: 0.5)],
|
|
// 2: Neon
|
|
[Color(red: 0.0, green: 1.0, blue: 0.8), Color(red: 1.0, green: 0.0, blue: 0.8),
|
|
Color(red: 1.0, green: 1.0, blue: 0.0), Color(red: 0.5, green: 0.0, blue: 1.0)],
|
|
// 3: Minimal
|
|
[Color(red: 0.85, green: 0.55, blue: 0.45), Color(red: 0.6, green: 0.5, blue: 0.45),
|
|
Color(red: 0.75, green: 0.5, blue: 0.5), Color(red: 0.65, green: 0.6, blue: 0.5)],
|
|
// 4: Zen
|
|
[Color(red: 0.5, green: 0.6, blue: 0.55), Color(red: 0.6, green: 0.55, blue: 0.5),
|
|
Color(red: 0.55, green: 0.5, blue: 0.45), Color(red: 0.5, green: 0.55, blue: 0.5)],
|
|
// 5: Editorial
|
|
[Color(red: 0.3, green: 0.3, blue: 0.35), Color(red: 0.4, green: 0.35, blue: 0.3),
|
|
Color(red: 0.35, green: 0.3, blue: 0.35), Color(red: 0.3, green: 0.35, blue: 0.4)],
|
|
// 6: Mixtape
|
|
[Color(red: 0.9, green: 0.6, blue: 0.3), Color(red: 0.8, green: 0.5, blue: 0.3),
|
|
Color(red: 0.85, green: 0.55, blue: 0.35), Color(red: 0.75, green: 0.5, blue: 0.25)],
|
|
// 7: Heartfelt
|
|
[Color(red: 1.0, green: 0.5, blue: 0.6), Color(red: 1.0, green: 0.4, blue: 0.5),
|
|
Color(red: 1.0, green: 0.55, blue: 0.65), Color(red: 0.95, green: 0.45, blue: 0.55)],
|
|
// 8: Luxe
|
|
[Color(red: 0.7, green: 0.8, blue: 1.0), Color(red: 0.8, green: 0.85, blue: 0.95),
|
|
Color(red: 0.75, green: 0.8, blue: 0.9), Color(red: 0.65, green: 0.75, blue: 0.95)],
|
|
// 9: Forecast
|
|
[Color(red: 0.4, green: 0.7, blue: 1.0), Color(red: 1.0, green: 0.85, blue: 0.3),
|
|
Color(red: 0.5, green: 0.8, blue: 0.9), Color(red: 0.9, green: 0.6, blue: 0.4)],
|
|
// 10: Playful
|
|
[Color(red: 0.2, green: 1.0, blue: 0.4), Color(red: 1.0, green: 1.0, blue: 0.2),
|
|
Color(red: 1.0, green: 0.4, blue: 0.2), Color(red: 0.6, green: 0.2, blue: 1.0)],
|
|
// 11: Journal
|
|
[Color(red: 0.6, green: 0.45, blue: 0.35), Color(red: 0.5, green: 0.4, blue: 0.35),
|
|
Color(red: 0.55, green: 0.45, blue: 0.4), Color(red: 0.5, green: 0.4, blue: 0.3)]
|
|
]
|
|
let safeIndex = min(style.rawValue, colors.count - 1)
|
|
return colors[safeIndex][accentIndex]
|
|
}
|
|
|
|
private var titleFont: Font {
|
|
switch style {
|
|
case .neon, .playful:
|
|
return .system(size: 14, weight: .bold, design: .monospaced)
|
|
case .minimal, .editorial, .zen, .journal:
|
|
return .system(size: 14, weight: .medium, design: .serif)
|
|
default:
|
|
return .system(size: 14, weight: .bold, design: .rounded)
|
|
}
|
|
}
|
|
|
|
private var titleColor: Color {
|
|
switch style {
|
|
case .minimal, .editorial, .journal:
|
|
return Color(red: 0.2, green: 0.15, blue: 0.1)
|
|
default:
|
|
return .white
|
|
}
|
|
}
|
|
|
|
private var subtitleColor: Color {
|
|
switch style {
|
|
case .minimal, .editorial, .journal:
|
|
return Color(red: 0.5, green: 0.45, blue: 0.4)
|
|
default:
|
|
return .white.opacity(0.6)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var cardBackground: some View {
|
|
switch style {
|
|
case .celestial, .garden, .heartfelt, .forecast, .luxe:
|
|
RoundedRectangle(cornerRadius: 16)
|
|
.fill(.ultraThinMaterial)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 16)
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [accentColor.opacity(0.1), Color.clear],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 16)
|
|
.stroke(.white.opacity(0.15), lineWidth: 1)
|
|
)
|
|
case .neon, .playful:
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(Color.black.opacity(0.5))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.stroke(
|
|
LinearGradient(
|
|
colors: [
|
|
accentColor.opacity(0.6),
|
|
accentColor.opacity(0.2)
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
),
|
|
lineWidth: 1
|
|
)
|
|
)
|
|
case .minimal, .editorial:
|
|
RoundedRectangle(cornerRadius: 14)
|
|
.fill(Color.white.opacity(0.7))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 14)
|
|
.stroke(Color(red: 0.85, green: 0.82, blue: 0.78), lineWidth: 1)
|
|
)
|
|
.shadow(color: Color.black.opacity(0.04), radius: 8, x: 0, y: 4)
|
|
case .zen:
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(Color.black.opacity(0.3))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.stroke(Color(red: 0.5, green: 0.45, blue: 0.4).opacity(0.3), lineWidth: 1)
|
|
)
|
|
case .mixtape:
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.fill(Color(red: 0.2, green: 0.15, blue: 0.1).opacity(0.8))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.stroke(Color(red: 0.5, green: 0.35, blue: 0.2), lineWidth: 1)
|
|
)
|
|
case .journal:
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.fill(Color(red: 1.0, green: 0.98, blue: 0.94))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.stroke(Color(red: 0.8, green: 0.75, blue: 0.7), lineWidth: 1)
|
|
)
|
|
.shadow(color: Color.black.opacity(0.08), radius: 4, x: 1, y: 2)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Social Proof Badge
|
|
|
|
struct SocialProofBadge: View {
|
|
let style: PaywallStyle
|
|
|
|
var body: some View {
|
|
HStack(spacing: 8) {
|
|
HStack(spacing: -8) {
|
|
ForEach(0..<4, id: \.self) { index in
|
|
Circle()
|
|
.fill(
|
|
LinearGradient(
|
|
colors: avatarColors(for: index),
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
)
|
|
.frame(width: 24, height: 24)
|
|
.overlay(
|
|
Circle()
|
|
.stroke(borderColor, lineWidth: 2)
|
|
)
|
|
}
|
|
}
|
|
|
|
Text("Join 50,000+ on their journey")
|
|
.font(textFont)
|
|
.foregroundColor(textColor)
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 10)
|
|
.background(badgeBackground)
|
|
}
|
|
|
|
private var borderColor: Color {
|
|
switch style {
|
|
case .celestial: return Color(red: 0.08, green: 0.06, blue: 0.15)
|
|
case .garden: return Color(red: 0.05, green: 0.12, blue: 0.08)
|
|
case .neon, .playful: return Color(red: 0.02, green: 0.02, blue: 0.05)
|
|
case .minimal, .editorial: return Color(red: 0.95, green: 0.93, blue: 0.9)
|
|
case .zen: return Color(red: 0.1, green: 0.08, blue: 0.06)
|
|
case .mixtape: return Color(red: 0.12, green: 0.08, blue: 0.06)
|
|
case .heartfelt: return Color(red: 0.12, green: 0.04, blue: 0.06)
|
|
case .luxe: return Color(red: 0.04, green: 0.05, blue: 0.08)
|
|
case .forecast: return Color(red: 0.1, green: 0.18, blue: 0.3)
|
|
case .journal: return Color(red: 0.98, green: 0.95, blue: 0.88)
|
|
}
|
|
}
|
|
|
|
private var textFont: Font {
|
|
switch style {
|
|
case .neon, .playful:
|
|
return .system(size: 12, weight: .medium, design: .monospaced)
|
|
case .minimal, .editorial, .zen, .journal:
|
|
return .system(size: 12, weight: .regular, design: .serif)
|
|
default:
|
|
return .system(size: 12, weight: .medium, design: .rounded)
|
|
}
|
|
}
|
|
|
|
private var textColor: Color {
|
|
switch style {
|
|
case .minimal, .editorial, .journal:
|
|
return Color(red: 0.5, green: 0.45, blue: 0.4)
|
|
default:
|
|
return .white.opacity(0.7)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var badgeBackground: some View {
|
|
switch style {
|
|
case .minimal, .editorial:
|
|
Capsule()
|
|
.fill(Color.white.opacity(0.8))
|
|
.overlay(Capsule().stroke(Color(red: 0.85, green: 0.82, blue: 0.78), lineWidth: 1))
|
|
case .neon, .playful:
|
|
Capsule()
|
|
.fill(Color.black.opacity(0.6))
|
|
.overlay(
|
|
Capsule()
|
|
.stroke(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.0, green: 1.0, blue: 0.8).opacity(0.5),
|
|
Color(red: 1.0, green: 0.0, blue: 0.8).opacity(0.5)
|
|
],
|
|
startPoint: .leading,
|
|
endPoint: .trailing
|
|
),
|
|
lineWidth: 1
|
|
)
|
|
)
|
|
case .journal:
|
|
Capsule()
|
|
.fill(Color(red: 1.0, green: 0.98, blue: 0.94))
|
|
.overlay(Capsule().stroke(Color(red: 0.8, green: 0.75, blue: 0.7), lineWidth: 1))
|
|
case .zen:
|
|
Capsule()
|
|
.fill(Color.black.opacity(0.3))
|
|
.overlay(Capsule().stroke(Color(red: 0.5, green: 0.45, blue: 0.4).opacity(0.3), lineWidth: 1))
|
|
case .mixtape:
|
|
Capsule()
|
|
.fill(Color(red: 0.2, green: 0.15, blue: 0.1).opacity(0.8))
|
|
.overlay(Capsule().stroke(Color(red: 0.5, green: 0.35, blue: 0.2), lineWidth: 1))
|
|
default:
|
|
Capsule()
|
|
.fill(.ultraThinMaterial)
|
|
.overlay(Capsule().stroke(.white.opacity(0.1), lineWidth: 1))
|
|
}
|
|
}
|
|
|
|
private func avatarColors(for index: Int) -> [Color] {
|
|
let colorSets: [[[Color]]] = [
|
|
// 0: Celestial
|
|
[
|
|
[Color(red: 1.0, green: 0.6, blue: 0.4), Color(red: 1.0, green: 0.4, blue: 0.3)],
|
|
[Color(red: 0.4, green: 0.7, blue: 0.9), Color(red: 0.3, green: 0.5, blue: 0.8)],
|
|
[Color(red: 0.6, green: 0.8, blue: 0.5), Color(red: 0.4, green: 0.7, blue: 0.4)],
|
|
[Color(red: 0.8, green: 0.5, blue: 0.9), Color(red: 0.6, green: 0.3, blue: 0.8)]
|
|
],
|
|
// 1: Garden
|
|
[
|
|
[Color(red: 0.6, green: 0.8, blue: 0.5), Color(red: 0.4, green: 0.7, blue: 0.4)],
|
|
[Color(red: 1.0, green: 0.7, blue: 0.7), Color(red: 0.9, green: 0.5, blue: 0.5)],
|
|
[Color(red: 0.7, green: 0.6, blue: 0.9), Color(red: 0.5, green: 0.4, blue: 0.7)],
|
|
[Color(red: 1.0, green: 0.85, blue: 0.5), Color(red: 0.9, green: 0.7, blue: 0.3)]
|
|
],
|
|
// 2: Neon
|
|
[
|
|
[Color(red: 0.0, green: 1.0, blue: 0.8), Color(red: 0.0, green: 0.7, blue: 0.6)],
|
|
[Color(red: 1.0, green: 0.0, blue: 0.8), Color(red: 0.7, green: 0.0, blue: 0.6)],
|
|
[Color(red: 1.0, green: 1.0, blue: 0.0), Color(red: 0.8, green: 0.8, blue: 0.0)],
|
|
[Color(red: 0.5, green: 0.0, blue: 1.0), Color(red: 0.3, green: 0.0, blue: 0.7)]
|
|
],
|
|
// 3: Minimal
|
|
[
|
|
[Color(red: 0.85, green: 0.7, blue: 0.65), Color(red: 0.75, green: 0.6, blue: 0.55)],
|
|
[Color(red: 0.7, green: 0.65, blue: 0.6), Color(red: 0.6, green: 0.55, blue: 0.5)],
|
|
[Color(red: 0.8, green: 0.65, blue: 0.6), Color(red: 0.7, green: 0.55, blue: 0.5)],
|
|
[Color(red: 0.75, green: 0.7, blue: 0.65), Color(red: 0.65, green: 0.6, blue: 0.55)]
|
|
],
|
|
// 4: Zen
|
|
[
|
|
[Color(red: 0.5, green: 0.55, blue: 0.5), Color(red: 0.4, green: 0.45, blue: 0.4)],
|
|
[Color(red: 0.55, green: 0.5, blue: 0.45), Color(red: 0.45, green: 0.4, blue: 0.35)],
|
|
[Color(red: 0.5, green: 0.5, blue: 0.5), Color(red: 0.4, green: 0.4, blue: 0.4)],
|
|
[Color(red: 0.55, green: 0.55, blue: 0.5), Color(red: 0.45, green: 0.45, blue: 0.4)]
|
|
],
|
|
// 5: Editorial
|
|
[
|
|
[Color(red: 0.4, green: 0.4, blue: 0.45), Color(red: 0.3, green: 0.3, blue: 0.35)],
|
|
[Color(red: 0.45, green: 0.4, blue: 0.4), Color(red: 0.35, green: 0.3, blue: 0.3)],
|
|
[Color(red: 0.4, green: 0.45, blue: 0.45), Color(red: 0.3, green: 0.35, blue: 0.35)],
|
|
[Color(red: 0.45, green: 0.45, blue: 0.4), Color(red: 0.35, green: 0.35, blue: 0.3)]
|
|
],
|
|
// 6: Mixtape
|
|
[
|
|
[Color(red: 0.8, green: 0.5, blue: 0.3), Color(red: 0.6, green: 0.35, blue: 0.2)],
|
|
[Color(red: 0.7, green: 0.45, blue: 0.25), Color(red: 0.5, green: 0.3, blue: 0.15)],
|
|
[Color(red: 0.85, green: 0.55, blue: 0.35), Color(red: 0.65, green: 0.4, blue: 0.25)],
|
|
[Color(red: 0.75, green: 0.5, blue: 0.3), Color(red: 0.55, green: 0.35, blue: 0.2)]
|
|
],
|
|
// 7: Heartfelt
|
|
[
|
|
[Color(red: 1.0, green: 0.5, blue: 0.6), Color(red: 0.9, green: 0.4, blue: 0.5)],
|
|
[Color(red: 1.0, green: 0.6, blue: 0.65), Color(red: 0.9, green: 0.5, blue: 0.55)],
|
|
[Color(red: 1.0, green: 0.45, blue: 0.55), Color(red: 0.9, green: 0.35, blue: 0.45)],
|
|
[Color(red: 1.0, green: 0.55, blue: 0.6), Color(red: 0.9, green: 0.45, blue: 0.5)]
|
|
],
|
|
// 8: Luxe
|
|
[
|
|
[Color(red: 0.7, green: 0.8, blue: 1.0), Color(red: 0.5, green: 0.6, blue: 0.8)],
|
|
[Color(red: 0.8, green: 0.85, blue: 0.95), Color(red: 0.6, green: 0.65, blue: 0.75)],
|
|
[Color(red: 0.75, green: 0.8, blue: 0.9), Color(red: 0.55, green: 0.6, blue: 0.7)],
|
|
[Color(red: 0.65, green: 0.75, blue: 0.95), Color(red: 0.45, green: 0.55, blue: 0.75)]
|
|
],
|
|
// 9: Forecast
|
|
[
|
|
[Color(red: 0.4, green: 0.7, blue: 1.0), Color(red: 0.3, green: 0.5, blue: 0.8)],
|
|
[Color(red: 1.0, green: 0.85, blue: 0.3), Color(red: 0.8, green: 0.65, blue: 0.2)],
|
|
[Color(red: 0.5, green: 0.8, blue: 0.9), Color(red: 0.4, green: 0.6, blue: 0.7)],
|
|
[Color(red: 0.9, green: 0.6, blue: 0.4), Color(red: 0.7, green: 0.4, blue: 0.3)]
|
|
],
|
|
// 10: Playful
|
|
[
|
|
[Color(red: 0.2, green: 1.0, blue: 0.4), Color(red: 0.1, green: 0.8, blue: 0.3)],
|
|
[Color(red: 1.0, green: 1.0, blue: 0.2), Color(red: 0.8, green: 0.8, blue: 0.1)],
|
|
[Color(red: 1.0, green: 0.4, blue: 0.2), Color(red: 0.8, green: 0.3, blue: 0.1)],
|
|
[Color(red: 0.6, green: 0.2, blue: 1.0), Color(red: 0.4, green: 0.1, blue: 0.8)]
|
|
],
|
|
// 11: Journal
|
|
[
|
|
[Color(red: 0.6, green: 0.45, blue: 0.35), Color(red: 0.5, green: 0.35, blue: 0.25)],
|
|
[Color(red: 0.55, green: 0.4, blue: 0.3), Color(red: 0.45, green: 0.3, blue: 0.2)],
|
|
[Color(red: 0.5, green: 0.45, blue: 0.4), Color(red: 0.4, green: 0.35, blue: 0.3)],
|
|
[Color(red: 0.55, green: 0.45, blue: 0.35), Color(red: 0.45, green: 0.35, blue: 0.25)]
|
|
]
|
|
]
|
|
let safeIndex = min(style.rawValue, colorSets.count - 1)
|
|
return colorSets[safeIndex][index % 4]
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview("Celestial") {
|
|
ReflectSubscriptionStoreView(style: .celestial)
|
|
.environmentObject(IAPManager())
|
|
.preferredColorScheme(.dark)
|
|
}
|
|
|
|
#Preview("Garden") {
|
|
ReflectSubscriptionStoreView(style: .garden)
|
|
.environmentObject(IAPManager())
|
|
.preferredColorScheme(.dark)
|
|
}
|
|
|
|
#Preview("Neon") {
|
|
ReflectSubscriptionStoreView(style: .neon)
|
|
.environmentObject(IAPManager())
|
|
.preferredColorScheme(.dark)
|
|
}
|
|
|
|
#Preview("Minimal") {
|
|
ReflectSubscriptionStoreView(style: .minimal)
|
|
.environmentObject(IAPManager())
|
|
.preferredColorScheme(.light)
|
|
}
|