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>
This commit is contained in:
Trey t
2025-12-26 23:05:45 -06:00
parent f45f52ccbf
commit 53eb953b77
9 changed files with 2144 additions and 117 deletions

View File

@@ -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

View File

@@ -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