Files
Reflect/Shared/Views/ReflectSubscriptionStoreView.swift
Trey t 83cca395cf Fix feature card truncation and unequal sizing on subscription screens
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>
2026-02-28 13:50:47 -06:00

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)
}