diff --git a/Feels/Localizable.xcstrings b/Feels/Localizable.xcstrings index eca39f9..f355547 100644 --- a/Feels/Localizable.xcstrings +++ b/Feels/Localizable.xcstrings @@ -1133,6 +1133,10 @@ } } }, + "Amplify your emotional intelligence.\nGo premium. Go limitless." : { + "comment" : "A description of the premium subscription experience, emphasizing its benefits.", + "isCommentAutoGenerated" : true + }, "Animation Lab" : { }, @@ -1783,6 +1787,10 @@ } } }, + "Clarity through simplicity.\nPremium unlocks understanding." : { + "comment" : "A description of the benefits of the premium subscription.", + "isCommentAutoGenerated" : true + }, "Clear DB" : { "comment" : "A button label that clears the app's database.", "isCommentAutoGenerated" : true, @@ -4666,6 +4674,10 @@ } } }, + "Every feeling is a seed.\nPremium helps you grow." : { + "comment" : "A description of the premium subscription's benefits.", + "isCommentAutoGenerated" : true + }, "Exit" : { "comment" : "A button label that dismisses the current view.", "isCommentAutoGenerated" : true, @@ -5482,6 +5494,7 @@ }, "Get unlimited access to all features" : { "comment" : "A description of the benefits of purchasing the premium version of the app.", + "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -5916,6 +5929,10 @@ } } }, + "Join 50,000+ on their journey" : { + "comment" : "A description of the social proof badge.", + "isCommentAutoGenerated" : true + }, "Journal Note" : { "comment" : "The title of the view that appears in the navigation bar.", "isCommentAutoGenerated" : true, @@ -8657,6 +8674,12 @@ } } } + }, + "Paywall Styles" : { + + }, + "Paywall Theme Lab" : { + }, "Personalize Your Experience" : { "comment" : "A title for a tip that encourages users to customize their mood tracking experience.", @@ -8996,6 +9019,16 @@ "comment" : "A description of a premium feature that requires a subscription.", "isCommentAutoGenerated" : true }, + "Preview" : { + + }, + "Preview and test different subscription paywall designs" : { + + }, + "Preview subscription themes" : { + "comment" : "A description of the paywall preview feature.", + "isCommentAutoGenerated" : true + }, "Privacy Lock" : { "comment" : "A title for a toggle that controls whether or not biometric authentication is enabled.", "isCommentAutoGenerated" : true, @@ -10815,6 +10848,9 @@ "See Your Year at a Glance" : { "comment" : "A title for a feature that lets users see their year's emotional trends.", "isCommentAutoGenerated" : true + }, + "Select Style" : { + }, "Select this mood" : { "comment" : "A hint that appears when a user taps on a mood button.", @@ -11598,6 +11634,10 @@ } } }, + "Simply\nKnow Yourself" : { + "comment" : "The title of the first section in the Minimal theme marketing content.", + "isCommentAutoGenerated" : true + }, "Skip subscription and complete setup" : { "comment" : "A button label that says \"Skip subscription and complete setup\". It's used in the \"OnboardingSubscription\" view.", "isCommentAutoGenerated" : true @@ -13085,6 +13125,10 @@ } } }, + "Understand\nYourself Deeper" : { + "comment" : "A headline in the premium subscription marketing content.", + "isCommentAutoGenerated" : true + }, "Unlock AI-Powered Insights" : { "comment" : "A title for a button that allows users to unlock premium insights.", "isCommentAutoGenerated" : true, @@ -13262,6 +13306,10 @@ "comment" : "A button label that appears when the user is not a premium subscriber, encouraging them to subscribe to unlock more features.", "isCommentAutoGenerated" : true }, + "UNLOCK YOUR\nFULL SIGNAL" : { + "comment" : "A title displayed in the neon marketing content view.", + "isCommentAutoGenerated" : true + }, "Use Siri to Log Moods" : { "localizations" : { "de" : { @@ -13343,6 +13391,9 @@ } } } + }, + "View Full Paywall" : { + }, "View the app introduction again" : { "comment" : "A button that allows a user to view the app's introductory screen again.", @@ -13605,6 +13656,10 @@ } } }, + "Watch Yourself\nBloom" : { + "comment" : "A title describing the premium subscription experience.", + "isCommentAutoGenerated" : true + }, "Wednesday - 10th" : { "comment" : "A label displayed above the date of a mood entry.", "isCommentAutoGenerated" : true, @@ -14235,6 +14290,10 @@ } } }, + "Your emotions tell a story.\nPremium helps you read it." : { + "comment" : "A subheadline describing the benefits of the premium subscription.", + "isCommentAutoGenerated" : true + }, "Your Feelings" : { "comment" : "The title of the main screen in the lock screen.", "isCommentAutoGenerated" : true diff --git a/Shared/Models/UserDefaultsStore.swift b/Shared/Models/UserDefaultsStore.swift index 291b575..5f5f6ba 100644 --- a/Shared/Models/UserDefaultsStore.swift +++ b/Shared/Models/UserDefaultsStore.swift @@ -14,6 +14,7 @@ enum VotingLayoutStyle: Int, CaseIterable { case stacked = 3 // Full-width vertical list case aura = 4 // Atmospheric glowing orbs with flowing layout case orbit = 5 // Celestial orbit with center core + case neon = 6 // Synthwave arcade equalizer with glowing segments var displayName: String { switch self { @@ -23,6 +24,32 @@ enum VotingLayoutStyle: Int, CaseIterable { case .stacked: return "Stacked" case .aura: return "Aura" case .orbit: return "Orbit" + case .neon: return "Neon" + } + } +} + +enum PaywallStyle: Int, CaseIterable { + case celestial = 0 // Celestial Self-Discovery - aurora, floating orbs + case garden = 1 // Garden Growth - organic, blooming nature + case neon = 2 // Neon Pulse - synthwave, energetic + case minimal = 3 // Minimal Zen - clean, sophisticated + + var displayName: String { + switch self { + case .celestial: return "Celestial" + case .garden: return "Garden" + case .neon: return "Neon" + case .minimal: return "Minimal" + } + } + + var description: String { + switch self { + case .celestial: return "Aurora lights & floating emotion orbs" + case .garden: return "Blooming flowers & organic growth" + case .neon: return "Synthwave energy & glowing pulses" + case .minimal: return "Clean typography & subtle elegance" } } } @@ -105,6 +132,7 @@ class UserDefaultsStore { case privacyLockEnabled case healthKitEnabled case healthKitSyncEnabled + case paywallStyle case contentViewCurrentSelectedHeaderViewBackDays case contentViewHeaderTag diff --git a/Shared/Views/AddMoodHeaderView.swift b/Shared/Views/AddMoodHeaderView.swift index c28d6a8..6eb54f9 100644 --- a/Shared/Views/AddMoodHeaderView.swift +++ b/Shared/Views/AddMoodHeaderView.swift @@ -84,6 +84,8 @@ struct AddMoodHeaderView: View { AuraVotingView(moodTint: moodTint, onMoodSelected: addItem) case .orbit: OrbitVotingView(moodTint: moodTint, onMoodSelected: addItem) + case .neon: + NeonVotingView(moodTint: moodTint, onMoodSelected: addItem) } } @@ -549,3 +551,227 @@ struct AddMoodHeaderView_Previews: PreviewProvider { } } } + +// MARK: - Layout 7: Neon (Synthwave Arcade Equalizer) + +struct NeonVotingView: View { + let moodTint: MoodTints + let onMoodSelected: (Mood) -> Void + + @State private var pulsePhase = false + @State private var hoveredMood: Mood? + + // Synthwave color palette + private let neonCyan = Color(red: 0.0, green: 1.0, blue: 0.82) + private let neonMagenta = Color(red: 1.0, green: 0.0, blue: 0.8) + private let neonYellow = Color(red: 1.0, green: 0.9, blue: 0.0) + private let deepBlack = Color(red: 0.02, green: 0.02, blue: 0.04) + + var body: some View { + ZStack { + // Grid background + neonGridBackground + + // Equalizer bars + HStack(spacing: 8) { + ForEach(Mood.allValues, id: \.self) { mood in + NeonEqualizerBar( + mood: mood, + moodTint: moodTint, + isHovered: hoveredMood == mood, + pulsePhase: pulsePhase, + neonCyan: neonCyan, + neonMagenta: neonMagenta, + onTap: { onMoodSelected(mood) } + ) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 20) + } + .frame(height: 200) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke( + LinearGradient( + colors: [neonCyan.opacity(0.6), neonMagenta.opacity(0.6)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 1 + ) + ) + .shadow(color: neonCyan.opacity(0.2), radius: 20, x: 0, y: 0) + .shadow(color: neonMagenta.opacity(0.15), radius: 30, x: 0, y: 10) + .onAppear { + withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { + pulsePhase = true + } + } + } + + private var neonGridBackground: some View { + ZStack { + // Deep black base + deepBlack + + // Grid + Canvas { context, size in + let gridSpacing: CGFloat = 20 + let cyanColor = Color(red: 0.0, green: 0.8, blue: 0.7) + + // 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(cyanColor.opacity(0.08)), 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(cyanColor.opacity(0.08)), lineWidth: 0.5) + } + } + + // Ambient glow at bottom + LinearGradient( + colors: [ + neonMagenta.opacity(0.15), + Color.clear + ], + startPoint: .bottom, + endPoint: .top + ) + .blur(radius: 30) + } + } +} + +struct NeonEqualizerBar: View { + let mood: Mood + let moodTint: MoodTints + let isHovered: Bool + let pulsePhase: Bool + let neonCyan: Color + let neonMagenta: Color + let onTap: () -> Void + + @State private var isPressed = false + + private var barHeight: CGFloat { + switch mood { + case .great: return 140 + case .good: return 115 + case .average: return 90 + case .bad: return 65 + case .horrible: return 45 + default: return 90 + } + } + + private var barColor: Color { + switch mood { + case .great: return neonCyan + case .good: return Color(red: 0.2, green: 1.0, blue: 0.6) + case .average: return Color(red: 1.0, green: 0.9, blue: 0.0) + case .bad: return Color(red: 1.0, green: 0.5, blue: 0.0) + case .horrible: return neonMagenta + default: return Color(red: 1.0, green: 0.9, blue: 0.0) + } + } + + var body: some View { + Button(action: onTap) { + VStack(spacing: 8) { + // The equalizer bar + ZStack(alignment: .bottom) { + // Glow background + RoundedRectangle(cornerRadius: 6) + .fill(barColor.opacity(pulsePhase ? 0.15 : 0.08)) + .frame(height: barHeight + 20) + .blur(radius: 15) + + // Main bar + RoundedRectangle(cornerRadius: 6) + .fill( + LinearGradient( + colors: [ + barColor, + barColor.opacity(0.7) + ], + startPoint: .top, + endPoint: .bottom + ) + ) + .frame(height: isPressed ? barHeight * 0.9 : barHeight) + .shadow(color: barColor.opacity(0.8), radius: pulsePhase ? 12 : 8, x: 0, y: 0) + .shadow(color: barColor.opacity(0.4), radius: pulsePhase ? 20 : 15, x: 0, y: 5) + + // Top highlight + RoundedRectangle(cornerRadius: 6) + .fill( + LinearGradient( + colors: [Color.white.opacity(0.5), Color.clear], + startPoint: .top, + endPoint: .center + ) + ) + .frame(height: isPressed ? barHeight * 0.9 : barHeight) + + // Level indicators (horizontal lines) + VStack(spacing: 8) { + ForEach(0.. some View { + configuration.label + .scaleEffect(configuration.isPressed ? 0.95 : 1.0) + .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) + .onChange(of: configuration.isPressed) { _, newValue in + isPressed = newValue + } + } +} + +private extension Mood { + var shortLabel: String { + switch self { + case .great: return "GRT" + case .good: return "GUD" + case .average: return "AVG" + case .bad: return "BAD" + case .horrible: return "HRB" + default: return "---" + } + } +} diff --git a/Shared/Views/CustomizeView/CustomizeView.swift b/Shared/Views/CustomizeView/CustomizeView.swift index 0d470d4..0cc352a 100644 --- a/Shared/Views/CustomizeView/CustomizeView.swift +++ b/Shared/Views/CustomizeView/CustomizeView.swift @@ -587,6 +587,27 @@ struct VotingLayoutPickerCompact: View { .offset(orbitOffset(index: index, total: 5, radius: 16)) } } + case .neon: + // Equalizer bars + ZStack { + RoundedRectangle(cornerRadius: 4) + .fill(Color.black) + .frame(width: 36, height: 36) + HStack(spacing: 2) { + ForEach(0..<5, id: \.self) { index in + let heights: [CGFloat] = [24, 18, 14, 10, 8] + RoundedRectangle(cornerRadius: 1) + .fill( + LinearGradient( + colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)], + startPoint: .top, + endPoint: .bottom + ) + ) + .frame(width: 4, height: heights[index]) + } + } + } } } diff --git a/Shared/Views/CustomizeView/SubViews/VotingLayoutPickerView.swift b/Shared/Views/CustomizeView/SubViews/VotingLayoutPickerView.swift index c82c843..6061813 100644 --- a/Shared/Views/CustomizeView/SubViews/VotingLayoutPickerView.swift +++ b/Shared/Views/CustomizeView/SubViews/VotingLayoutPickerView.swift @@ -151,6 +151,30 @@ struct VotingLayoutPickerView: View { .offset(orbitOffset(index: index, total: 5, radius: 16)) } } + case .neon: + // Equalizer bars + ZStack { + // Grid background hint + RoundedRectangle(cornerRadius: 4) + .fill(Color.black) + .frame(width: 36, height: 36) + + // Equalizer bars + HStack(spacing: 2) { + ForEach(0..<5, id: \.self) { index in + let heights: [CGFloat] = [24, 18, 14, 10, 8] + RoundedRectangle(cornerRadius: 1) + .fill( + LinearGradient( + colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)], + startPoint: .top, + endPoint: .bottom + ) + ) + .frame(width: 4, height: heights[index]) + } + } + } } } diff --git a/Shared/Views/EntryListView.swift b/Shared/Views/EntryListView.swift index a5d0e71..945e9c9 100644 --- a/Shared/Views/EntryListView.swift +++ b/Shared/Views/EntryListView.swift @@ -590,103 +590,205 @@ struct EntryListView: View { .background(colorScheme == .dark ? Color(.systemGray6) : .white) } - // MARK: - Neon Style (Cyberpunk/Synthwave) + // MARK: - Neon Style (Synthwave Arcade) private var neonStyle: some View { - ZStack { - // Dark base with scanline effect - RoundedRectangle(cornerRadius: 4) - .fill(Color.black) + let neonCyan = Color(red: 0.0, green: 1.0, blue: 0.82) + let neonMagenta = Color(red: 1.0, green: 0.0, blue: 0.8) + let deepBlack = Color(red: 0.02, green: 0.02, blue: 0.04) - // Scanline overlay - VStack(spacing: 2) { - ForEach(0..<20, id: \.self) { _ in - Rectangle() - .fill(Color.white.opacity(0.03)) - .frame(height: 1) - Spacer().frame(height: 3) + // Map mood to synthwave color spectrum + let synthwaveColor: Color = { + switch entry.mood { + case .great: return neonCyan + case .good: return Color(red: 0.0, green: 0.9, blue: 0.6) // Cyan-green + case .average: return Color(red: 0.9, green: 0.9, blue: 0.2) // Neon yellow + case .bad: return Color(red: 1.0, green: 0.5, blue: 0.3) // Neon orange + case .horrible: return neonMagenta + default: return Color(red: 0.9, green: 0.9, blue: 0.2) // Fallback yellow + } + }() + + return ZStack { + // Deep black base + RoundedRectangle(cornerRadius: 6) + .fill(deepBlack) + + // Grid background pattern + Canvas { context, size in + let gridSpacing: CGFloat = 12 + let gridColor = neonCyan.opacity(0.08) + + // Horizontal grid lines + for y in stride(from: 0, through: 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(gridColor), lineWidth: 0.5) + } + + // Vertical grid lines + for x in stride(from: 0, through: 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(gridColor), lineWidth: 0.5) } } - .clipShape(RoundedRectangle(cornerRadius: 4)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + + // Scanline overlay for CRT effect + VStack(spacing: 0) { + ForEach(0..<30, id: \.self) { _ in + Rectangle() + .fill(Color.white.opacity(0.015)) + .frame(height: 1) + Spacer().frame(height: 2) + } + } + .clipShape(RoundedRectangle(cornerRadius: 6)) // Content HStack(spacing: 16) { - // Neon-outlined mood indicator + // Neon equalizer-style mood indicator ZStack { - // Glow effect - RoundedRectangle(cornerRadius: 8) - .stroke(isMissing ? Color.gray : moodColor, lineWidth: 2) + // Outer glow ring + RoundedRectangle(cornerRadius: 10) + .stroke( + LinearGradient( + colors: isMissing + ? [Color.gray.opacity(0.3)] + : [neonCyan.opacity(0.6), neonMagenta.opacity(0.6)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 2 + ) .blur(radius: 4) - .opacity(0.8) - RoundedRectangle(cornerRadius: 8) - .stroke(isMissing ? Color.gray : moodColor, lineWidth: 2) + // Inner border + RoundedRectangle(cornerRadius: 10) + .stroke( + LinearGradient( + colors: isMissing + ? [Color.gray.opacity(0.4)] + : [neonCyan, neonMagenta], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 1.5 + ) + // Mood icon with synthwave glow imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 28, height: 28) - .foregroundColor(isMissing ? .gray : moodColor) - .shadow(color: isMissing ? .clear : moodColor, radius: 8, x: 0, y: 0) + .foregroundColor(isMissing ? .gray : synthwaveColor) + .shadow(color: isMissing ? .clear : synthwaveColor.opacity(0.9), radius: 8, x: 0, y: 0) + .shadow(color: isMissing ? .clear : synthwaveColor.opacity(0.5), radius: 16, x: 0, y: 0) .accessibilityLabel(entry.mood.strValue) } - .frame(width: 52, height: 52) + .frame(width: 54, height: 54) - VStack(alignment: .leading, spacing: 6) { - // Date in monospace terminal style + VStack(alignment: .leading, spacing: 5) { + // Date in cyan monospace Text(entry.forDate, format: .dateTime.year().month(.twoDigits).day(.twoDigits)) - .font(.caption.weight(.medium).monospaced()) - .foregroundColor(Color(red: 0.4, green: 1.0, blue: 0.4)) // Terminal green + .font(.system(.caption, design: .monospaced).weight(.semibold)) + .foregroundColor(neonCyan) + .shadow(color: neonCyan.opacity(0.5), radius: 4, x: 0, y: 0) if isMissing { Text("NO_DATA") - .font(.headline.weight(.bold).monospaced()) - .foregroundColor(.gray) + .font(.system(.headline, design: .monospaced).weight(.black)) + .foregroundColor(.gray.opacity(0.6)) } else { - // Mood in glowing text + // Mood text with synthwave gradient glow Text(entry.moodString.uppercased()) - .font(.headline.weight(.black)) - .foregroundColor(moodColor) - .shadow(color: moodColor.opacity(0.8), radius: 6, x: 0, y: 0) - .shadow(color: moodColor.opacity(0.4), radius: 12, x: 0, y: 0) + .font(.system(.headline, design: .monospaced).weight(.black)) + .foregroundStyle( + LinearGradient( + colors: [neonCyan, synthwaveColor, neonMagenta], + startPoint: .leading, + endPoint: .trailing + ) + ) + .shadow(color: synthwaveColor.opacity(0.8), radius: 6, x: 0, y: 0) + .shadow(color: neonMagenta.opacity(0.3), radius: 12, x: 0, y: 0) } - // Weekday + // Weekday in magenta Text(entry.forDate, format: .dateTime.weekday(.wide)) - .font(.caption2.weight(.medium).monospaced()) - .foregroundColor(.white.opacity(0.4)) + .font(.system(.caption2, design: .monospaced).weight(.medium)) + .foregroundColor(neonMagenta.opacity(0.7)) .textCase(.uppercase) } Spacer() - // Chevron with glow + // Equalizer bars indicator (mini visualization) + if !isMissing { + HStack(spacing: 2) { + ForEach(0..<5, id: \.self) { index in + let barHeight: CGFloat = { + let moodIndex = Mood.allValues.firstIndex(of: entry.mood) ?? 2 + let heights: [[CGFloat]] = [ + [28, 22, 16, 10, 6], // Great + [24, 28, 18, 12, 8], // Good + [16, 20, 28, 20, 16], // Okay + [8, 12, 18, 28, 24], // Bad + [6, 10, 16, 22, 28] // Awful + ] + return heights[moodIndex][index] + }() + + RoundedRectangle(cornerRadius: 1) + .fill( + LinearGradient( + colors: [neonCyan, neonMagenta], + startPoint: .top, + endPoint: .bottom + ) + ) + .frame(width: 3, height: barHeight) + .shadow(color: neonCyan.opacity(0.5), radius: 2, x: 0, y: 0) + } + } + .padding(.trailing, 4) + } + + // Chevron with gradient glow Image(systemName: "chevron.right") .font(.subheadline.weight(.bold)) - .foregroundColor(isMissing ? .gray : moodColor) - .shadow(color: isMissing ? .clear : moodColor, radius: 4, x: 0, y: 0) + .foregroundStyle( + isMissing + ? AnyShapeStyle(Color.gray.opacity(0.4)) + : AnyShapeStyle(LinearGradient( + colors: [neonCyan, neonMagenta], + startPoint: .top, + endPoint: .bottom + )) + ) + .shadow(color: isMissing ? .clear : neonCyan.opacity(0.5), radius: 4, x: 0, y: 0) } .padding(.horizontal, 18) - .padding(.vertical, 16) + .padding(.vertical, 14) - // Neon border - RoundedRectangle(cornerRadius: 4) + // Cyan-to-magenta gradient border + RoundedRectangle(cornerRadius: 6) .stroke( LinearGradient( colors: isMissing - ? [Color.gray.opacity(0.3), Color.gray.opacity(0.1)] - : [moodColor.opacity(0.8), moodColor.opacity(0.3)], + ? [Color.gray.opacity(0.2), Color.gray.opacity(0.1)] + : [neonCyan.opacity(0.7), neonMagenta.opacity(0.7)], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 1 ) } - .shadow( - color: isMissing ? .clear : moodColor.opacity(0.3), - radius: 12, - x: 0, - y: 4 - ) + // Outer glow effect + .shadow(color: isMissing ? .clear : neonCyan.opacity(0.2), radius: 12, x: 0, y: 2) + .shadow(color: isMissing ? .clear : neonMagenta.opacity(0.15), radius: 20, x: 0, y: 4) } // MARK: - Ink Style (Japanese Zen/Calligraphy) diff --git a/Shared/Views/FeelsSubscriptionStoreView.swift b/Shared/Views/FeelsSubscriptionStoreView.swift index 97f2285..6420da6 100644 --- a/Shared/Views/FeelsSubscriptionStoreView.swift +++ b/Shared/Views/FeelsSubscriptionStoreView.swift @@ -2,7 +2,7 @@ // FeelsSubscriptionStoreView.swift // Feels // -// Native StoreKit 2 subscription purchase view. +// Premium subscription experience with multiple theme options. // import SwiftUI @@ -11,88 +11,1132 @@ import StoreKit struct FeelsSubscriptionStoreView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject var iapManager: IAPManager + var style: PaywallStyle = .celestial var body: some View { SubscriptionStoreView(groupID: IAPManager.subscriptionGroupID) { - VStack(spacing: 20) { - // App icon or logo - ZStack { - Circle() - .fill( - LinearGradient( - colors: [.pink.opacity(0.3), .orange.opacity(0.2)], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .frame(width: 100, height: 100) - - Image(systemName: "heart.fill") - .font(.largeTitle) - .foregroundStyle( - LinearGradient( - colors: [.pink, .red], - startPoint: .top, - endPoint: .bottom - ) - ) - } - - VStack(spacing: 8) { - Text("Unlock Premium") - .font(.title.weight(.bold)) - - Text("Get unlimited access to all features") - .font(.body) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - } - - // Feature highlights - VStack(alignment: .leading, spacing: 12) { - FeatureHighlight(icon: "calendar", text: "Month & Year Views") - FeatureHighlight(icon: "lightbulb.fill", text: "AI-Powered Insights") - FeatureHighlight(icon: "heart.text.square.fill", text: "Health Data Correlation") - FeatureHighlight(icon: "square.grid.2x2.fill", text: "Interactive Widgets") - } - .padding(.top, 8) - } - .padding(.horizontal, 20) - .padding(.vertical, 10) + marketingContent } .subscriptionStoreControlStyle(.prominentPicker) .storeButton(.visible, for: .restorePurchases) .subscriptionStoreButtonLabel(.multiline) - .tint(.pink) + .tint(tintColor) .onInAppPurchaseCompletion { _, result in if case .success(.success(_)) = result { dismiss() } } } -} -// MARK: - Feature Highlight Row -struct FeatureHighlight: View { - let icon: String - let text: String + @ViewBuilder + private var marketingContent: some View { + switch style { + case .celestial: + CelestialMarketingContent() + case .garden: + GardenMarketingContent() + case .neon: + NeonMarketingContent() + case .minimal: + MinimalMarketingContent() + } + } - var body: some View { - HStack(spacing: 12) { - Image(systemName: "checkmark.circle.fill") - .font(.headline) - .foregroundColor(.green) - - Text(text) - .font(.subheadline.weight(.medium)) - .foregroundColor(.primary) - - Spacer() + private var tintColor: Color { + switch style { + 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) } } } -#Preview { - FeelsSubscriptionStoreView() - .environmentObject(IAPManager()) +// 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 + } + } + } +} + +// 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 + } + } + } +} + +// 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 + } + } + } +} + +// 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 + } + } + } +} + +// 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() + } +} + +// 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.. [Color] { + let colorSets: [[[Color]]] = [ + // 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)] + ], + // 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)] + ], + // 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)] + ], + // 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)] + ] + ] + return colorSets[style.rawValue][index % 4] + } +} + +// MARK: - Preview + +#Preview("Celestial") { + FeelsSubscriptionStoreView(style: .celestial) + .environmentObject(IAPManager()) + .preferredColorScheme(.dark) +} + +#Preview("Garden") { + FeelsSubscriptionStoreView(style: .garden) + .environmentObject(IAPManager()) + .preferredColorScheme(.dark) +} + +#Preview("Neon") { + FeelsSubscriptionStoreView(style: .neon) + .environmentObject(IAPManager()) + .preferredColorScheme(.dark) +} + +#Preview("Minimal") { + FeelsSubscriptionStoreView(style: .minimal) + .environmentObject(IAPManager()) + .preferredColorScheme(.light) } diff --git a/Shared/Views/SettingsView/PaywallPreviewSettingsView.swift b/Shared/Views/SettingsView/PaywallPreviewSettingsView.swift new file mode 100644 index 0000000..f30f73b --- /dev/null +++ b/Shared/Views/SettingsView/PaywallPreviewSettingsView.swift @@ -0,0 +1,476 @@ +// +// PaywallPreviewSettingsView.swift +// Feels +// +// Debug view for previewing and switching paywall styles. +// + +import SwiftUI + +#if DEBUG +struct PaywallPreviewSettingsView: View { + @Environment(\.dismiss) private var dismiss + @State private var selectedStyle: PaywallStyle = .celestial + @State private var showFullPreview = false + @EnvironmentObject var iapManager: IAPManager + + var body: some View { + ScrollView { + VStack(spacing: 24) { + headerSection + + stylePicker + + previewCard + + fullPreviewButton + } + .padding() + } + .background(Color(.systemGroupedBackground)) + .navigationTitle("Paywall Styles") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + dismiss() + } + } + } + .sheet(isPresented: $showFullPreview) { + FeelsSubscriptionStoreView(style: selectedStyle) + .environmentObject(iapManager) + } + } + + private var headerSection: some View { + VStack(spacing: 8) { + Image(systemName: "paintpalette.fill") + .font(.system(size: 40)) + .foregroundStyle( + LinearGradient( + colors: [.purple, .pink, .orange], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + + Text("Paywall Theme Lab") + .font(.title2.bold()) + + Text("Preview and test different subscription paywall designs") + .font(.subheadline) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + .padding(.vertical) + } + + private var stylePicker: some View { + VStack(alignment: .leading, spacing: 12) { + Text("Select Style") + .font(.headline) + + ForEach(PaywallStyle.allCases, id: \.self) { style in + StyleOptionRow( + style: style, + isSelected: selectedStyle == style, + onTap: { selectedStyle = style } + ) + } + } + } + + private var previewCard: some View { + VStack(spacing: 0) { + // Mini preview header + HStack { + Text("Preview") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + Spacer() + Text(selectedStyle.displayName) + .font(.caption.weight(.medium)) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background(Color(.secondarySystemGroupedBackground)) + + // Mini preview content + miniPreview + .frame(height: 280) + .clipped() + } + .clipShape(RoundedRectangle(cornerRadius: 16)) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color(.separator), lineWidth: 0.5) + ) + } + + @ViewBuilder + private var miniPreview: some View { + switch selectedStyle { + case .celestial: + CelestialMiniPreview() + case .garden: + GardenMiniPreview() + case .neon: + NeonMiniPreview() + case .minimal: + MinimalMiniPreview() + } + } + + private var fullPreviewButton: some View { + Button { + showFullPreview = true + } label: { + HStack { + Image(systemName: "arrow.up.left.and.arrow.down.right") + Text("View Full Paywall") + .fontWeight(.semibold) + } + .frame(maxWidth: .infinity) + .padding() + .background( + LinearGradient( + colors: gradientColors, + startPoint: .leading, + endPoint: .trailing + ) + ) + .foregroundColor(.white) + .clipShape(RoundedRectangle(cornerRadius: 14)) + } + } + + private var gradientColors: [Color] { + switch selectedStyle { + case .celestial: + return [Color(red: 1.0, green: 0.4, blue: 0.5), Color(red: 0.6, green: 0.4, blue: 0.9)] + case .garden: + return [Color(red: 0.4, green: 0.75, blue: 0.45), Color(red: 0.3, green: 0.6, blue: 0.4)] + case .neon: + return [Color(red: 0.0, green: 0.9, blue: 0.7), Color(red: 0.9, green: 0.0, blue: 0.7)] + case .minimal: + return [Color(red: 0.85, green: 0.6, blue: 0.5), Color(red: 0.7, green: 0.5, blue: 0.45)] + } + } +} + +// MARK: - Style Option Row + +struct StyleOptionRow: View { + let style: PaywallStyle + let isSelected: Bool + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack(spacing: 14) { + // Style icon + ZStack { + Circle() + .fill(iconGradient) + .frame(width: 44, height: 44) + + Image(systemName: iconName) + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.white) + } + + // Text + VStack(alignment: .leading, spacing: 2) { + Text(style.displayName) + .font(.body.weight(.medium)) + .foregroundColor(.primary) + + Text(style.description) + .font(.caption) + .foregroundColor(.secondary) + } + + Spacer() + + // Selection indicator + if isSelected { + Image(systemName: "checkmark.circle.fill") + .font(.title3) + .foregroundColor(.accentColor) + } + } + .padding(12) + .background(Color(.secondarySystemGroupedBackground)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 2) + ) + } + .buttonStyle(.plain) + } + + private var iconName: String { + switch style { + case .celestial: return "sparkles" + case .garden: return "leaf.fill" + case .neon: return "bolt.fill" + case .minimal: return "circle.grid.2x2" + } + } + + private var iconGradient: LinearGradient { + switch style { + case .celestial: + return LinearGradient( + colors: [Color(red: 1.0, green: 0.4, blue: 0.5), Color(red: 0.6, green: 0.4, blue: 0.9)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + case .garden: + return LinearGradient( + colors: [Color(red: 0.4, green: 0.75, blue: 0.45), Color(red: 0.3, green: 0.6, blue: 0.4)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + case .neon: + return LinearGradient( + colors: [Color(red: 0.0, green: 0.9, blue: 0.7), Color(red: 0.9, green: 0.0, blue: 0.7)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + case .minimal: + return LinearGradient( + colors: [Color(red: 0.85, green: 0.6, blue: 0.5), Color(red: 0.7, green: 0.5, blue: 0.45)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + } + } +} + +// MARK: - Mini Previews + +struct CelestialMiniPreview: View { + @State private var animate = false + + var body: some View { + ZStack { + // Background + LinearGradient( + colors: [ + Color(red: 0.05, green: 0.05, blue: 0.12), + Color(red: 0.08, green: 0.06, blue: 0.15) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Glow + Circle() + .fill(Color(red: 1.0, green: 0.4, blue: 0.5).opacity(0.3)) + .frame(width: 200, height: 200) + .blur(radius: 60) + .offset(y: animate ? -20 : 0) + + // Content + VStack(spacing: 12) { + // Mini orbs + HStack(spacing: -10) { + ForEach(0..<3, id: \.self) { i in + Circle() + .fill(orbColors[i]) + .frame(width: 24, height: 24) + .shadow(color: orbColors[i].opacity(0.5), radius: 8) + } + } + + Text("Understand\nYourself Deeper") + .font(.system(size: 18, weight: .bold, design: .serif)) + .multilineTextAlignment(.center) + .foregroundColor(.white) + } + } + .onAppear { + withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) { + animate = true + } + } + } + + private var orbColors: [Color] { + [ + Color(red: 1.0, green: 0.8, blue: 0.3), + Color(red: 1.0, green: 0.5, blue: 0.5), + Color(red: 0.6, green: 0.5, blue: 0.9) + ] + } +} + +struct GardenMiniPreview: View { + @State private var bloom = false + + var body: some View { + ZStack { + // Background + LinearGradient( + colors: [ + Color(red: 0.05, green: 0.12, blue: 0.08), + Color(red: 0.08, green: 0.18, blue: 0.1) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Glow + Circle() + .fill(Color(red: 0.3, green: 0.7, blue: 0.4).opacity(0.25)) + .frame(width: 200, height: 200) + .blur(radius: 60) + + // Content + VStack(spacing: 12) { + // Mini flower + ZStack { + ForEach(0..<6, id: \.self) { i in + Ellipse() + .fill(Color(red: 1.0, green: 0.6, blue: 0.7)) + .frame(width: 14, height: bloom ? 28 : 20) + .offset(y: bloom ? -22 : -16) + .rotationEffect(.degrees(Double(i) * 60)) + } + Circle() + .fill(Color(red: 1.0, green: 0.9, blue: 0.6)) + .frame(width: 20, height: 20) + } + + Text("Watch Yourself\nBloom") + .font(.system(size: 18, weight: .bold, design: .serif)) + .multilineTextAlignment(.center) + .foregroundColor(.white) + } + } + .onAppear { + withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) { + bloom = true + } + } + } +} + +struct NeonMiniPreview: View { + @State private var pulse = false + + var body: some View { + ZStack { + // Background + Color(red: 0.02, green: 0.02, blue: 0.05) + + // Grid + Canvas { context, size in + let spacing: CGFloat = 20 + for y in stride(from: 0, to: size.height, by: spacing) { + 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(Color.cyan.opacity(0.1)), lineWidth: 0.5) + } + } + + // Glows + Circle() + .fill(Color.cyan.opacity(pulse ? 0.3 : 0.15)) + .frame(width: 150, height: 150) + .blur(radius: 50) + .offset(y: -40) + + Circle() + .fill(Color.pink.opacity(pulse ? 0.2 : 0.1)) + .frame(width: 120, height: 120) + .blur(radius: 40) + .offset(x: 30, y: 40) + + // Content + VStack(spacing: 12) { + // Neon ring + Circle() + .stroke( + LinearGradient( + colors: [.cyan, .pink], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 3 + ) + .frame(width: 50, height: 50) + .shadow(color: .cyan.opacity(0.5), radius: pulse ? 15 : 8) + + Text("UNLOCK YOUR\nFULL SIGNAL") + .font(.system(size: 14, weight: .black, design: .monospaced)) + .multilineTextAlignment(.center) + .foregroundStyle( + LinearGradient(colors: [.cyan, .pink], startPoint: .leading, endPoint: .trailing) + ) + } + } + .onAppear { + withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { + pulse = true + } + } + } +} + +struct MinimalMiniPreview: View { + @State private var breathe = false + + var body: some View { + ZStack { + // Background + LinearGradient( + colors: [ + Color(red: 0.98, green: 0.96, blue: 0.92), + Color(red: 0.95, green: 0.93, blue: 0.88) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Content + VStack(spacing: 16) { + // Breathing circles + ZStack { + Circle() + .stroke(Color(red: 0.8, green: 0.7, blue: 0.6).opacity(0.3), lineWidth: 1) + .frame(width: breathe ? 60 : 50, height: breathe ? 60 : 50) + + Circle() + .fill(Color(red: 0.95, green: 0.6, blue: 0.5).opacity(0.4)) + .frame(width: 30, height: 30) + .scaleEffect(breathe ? 1.1 : 0.95) + } + + Text("Simply\nKnow Yourself") + .font(.system(size: 18, weight: .light, design: .serif)) + .italic() + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0.2, green: 0.15, blue: 0.1)) + } + } + .onAppear { + withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) { + breathe = true + } + } + } +} + +#Preview { + NavigationStack { + PaywallPreviewSettingsView() + .environmentObject(IAPManager()) + } +} +#endif diff --git a/Shared/Views/SettingsView/SettingsView.swift b/Shared/Views/SettingsView/SettingsView.swift index 6871715..b89607d 100644 --- a/Shared/Views/SettingsView/SettingsView.swift +++ b/Shared/Views/SettingsView/SettingsView.swift @@ -54,6 +54,7 @@ struct SettingsContentView: View { debugSectionHeader trialDateButton animationLabButton + paywallPreviewButton #endif Spacer() @@ -245,6 +246,7 @@ struct SettingsContentView: View { } @State private var showAnimationLab = false + @State private var showPaywallPreview = false private var animationLabButton: some View { ZStack { @@ -284,6 +286,51 @@ struct SettingsContentView: View { } } } + + private var paywallPreviewButton: some View { + ZStack { + theme.currentTheme.secondaryBGColor + Button { + showPaywallPreview = true + } label: { + HStack(spacing: 12) { + Image(systemName: "paintpalette.fill") + .font(.title2) + .foregroundStyle( + LinearGradient( + colors: [.purple, .pink, .orange], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 32) + + VStack(alignment: .leading, spacing: 2) { + Text("Paywall Styles") + .foregroundColor(textColor) + + Text("Preview subscription themes") + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.caption) + .foregroundStyle(.tertiary) + } + .padding() + } + } + .fixedSize(horizontal: false, vertical: true) + .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) + .sheet(isPresented: $showPaywallPreview) { + NavigationStack { + PaywallPreviewSettingsView() + } + } + } #endif // MARK: - Privacy Lock Toggle