Files
Reflect/Shared/Views/ReflectSubscriptionStoreView.swift
Trey t 0442eab1f8 Rebrand entire project from Feels to Reflect
Complete rename across all bundle IDs, App Groups, CloudKit containers,
StoreKit product IDs, data store filenames, URL schemes, logger subsystems,
Swift identifiers, user-facing strings (7 languages), file names, directory
names, Xcode project, schemes, assets, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:47:16 -06:00

2398 lines
84 KiB
Swift

//
// ReflectSubscriptionStoreView.swift
// Reflect
//
// Premium subscription experience with multiple theme options.
//
import SwiftUI
import StoreKit
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)
.onAppear {
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()
}
}
.containerRelativeFrame(.horizontal)
}
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
var body: some View {
VStack(spacing: 12) {
HStack(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
)
}
HStack(spacing: 12) {
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)
.lineLimit(1)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(14)
.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)
}