Files
Reflect/Shared/Views/FeelsSubscriptionStoreView.swift
Trey t 53eb953b77 Add Neon/Synthwave style and 4 paywall themes
- Add 4 distinct paywall themes (Celestial, Garden, Neon, Minimal) with
  preview/switcher in debug settings
- Add Neon voting layout with synthwave equalizer bar design
- Upgrade Neon entry style with grid background, cyan/magenta gradients,
  scanline effects, and mini equalizer visualization
- Add PaywallPreviewSettingsView for testing different paywall styles
- Use consistent synthwave color palette (cyan #00FFD0, magenta #FF00CC)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 23:05:45 -06:00

1143 lines
38 KiB
Swift

//
// FeelsSubscriptionStoreView.swift
// Feels
//
// Premium subscription experience with multiple theme options.
//
import SwiftUI
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) {
marketingContent
}
.subscriptionStoreControlStyle(.prominentPicker)
.storeButton(.visible, for: .restorePurchases)
.subscriptionStoreButtonLabel(.multiline)
.tint(tintColor)
.onInAppPurchaseCompletion { _, result in
if case .success(.success(_)) = result {
dismiss()
}
}
}
@ViewBuilder
private var marketingContent: some View {
switch style {
case .celestial:
CelestialMarketingContent()
case .garden:
GardenMarketingContent()
case .neon:
NeonMarketingContent()
case .minimal:
MinimalMarketingContent()
}
}
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)
}
}
}
// 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..<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
}
}
}
}
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: - 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]] = [
// 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)],
// 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)],
// 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)],
// 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)]
]
return colors[style.rawValue][accentIndex]
}
private var titleFont: Font {
switch style {
case .neon:
return .system(size: 14, weight: .bold, design: .monospaced)
case .minimal:
return .system(size: 14, weight: .medium, design: .serif)
default:
return .system(size: 14, weight: .bold, design: .rounded)
}
}
private var titleColor: Color {
style == .minimal ? Color(red: 0.2, green: 0.15, blue: 0.1) : .white
}
private var subtitleColor: Color {
style == .minimal ? Color(red: 0.5, green: 0.45, blue: 0.4) : .white.opacity(0.6)
}
@ViewBuilder
private var cardBackground: some View {
switch style {
case .celestial, .garden:
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:
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:
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)
}
}
}
// 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: return Color(red: 0.02, green: 0.02, blue: 0.05)
case .minimal: return Color(red: 0.95, green: 0.93, blue: 0.9)
}
}
private var textFont: Font {
switch style {
case .neon:
return .system(size: 12, weight: .medium, design: .monospaced)
case .minimal:
return .system(size: 12, weight: .regular, design: .serif)
default:
return .system(size: 12, weight: .medium, design: .rounded)
}
}
private var textColor: Color {
style == .minimal ? Color(red: 0.5, green: 0.45, blue: 0.4) : .white.opacity(0.7)
}
@ViewBuilder
private var badgeBackground: some View {
switch style {
case .minimal:
Capsule()
.fill(Color.white.opacity(0.8))
.overlay(Capsule().stroke(Color(red: 0.85, green: 0.82, blue: 0.78), lineWidth: 1))
case .neon:
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
)
)
default:
Capsule()
.fill(.ultraThinMaterial)
.overlay(Capsule().stroke(.white.opacity(0.1), lineWidth: 1))
}
}
private func avatarColors(for index: Int) -> [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)
}