Exhaustive file-by-file audit of every Swift file in the project (iOS app, Watch app, Widget extension). Every interactive UI element — buttons, toggles, pickers, links, menus, tap gestures, text editors, color pickers, photo pickers — now has an accessibilityIdentifier for XCUITest automation. 46 files changed across Shared/, Onboarding/, Watch App/, and Widget targets. Added ~100 new ID definitions covering settings debug controls, export/photo views, sharing templates, customization subviews, onboarding flows, tip modals, widget voting buttons, and watch mood buttons.
932 lines
30 KiB
Swift
932 lines
30 KiB
Swift
//
|
|
// PaywallPreviewSettingsView.swift
|
|
// Reflect
|
|
//
|
|
// 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()
|
|
}
|
|
.accessibilityIdentifier(AccessibilityID.Debug.paywallPreviewDoneButton)
|
|
}
|
|
}
|
|
.sheet(isPresented: $showFullPreview) {
|
|
ReflectSubscriptionStoreView(source: "paywall_preview", 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()
|
|
case .zen:
|
|
ZenMiniPreview()
|
|
case .editorial:
|
|
EditorialMiniPreview()
|
|
case .mixtape:
|
|
MixtapeMiniPreview()
|
|
case .heartfelt:
|
|
HeartfeltMiniPreview()
|
|
case .luxe:
|
|
LuxeMiniPreview()
|
|
case .forecast:
|
|
ForecastMiniPreview()
|
|
case .playful:
|
|
PlayfulMiniPreview()
|
|
case .journal:
|
|
JournalMiniPreview()
|
|
}
|
|
}
|
|
|
|
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))
|
|
}
|
|
.accessibilityIdentifier(AccessibilityID.Debug.viewFullPaywallButton)
|
|
}
|
|
|
|
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)]
|
|
case .zen:
|
|
return [Color(red: 0.6, green: 0.7, blue: 0.6), Color(red: 0.5, green: 0.6, blue: 0.55)]
|
|
case .editorial:
|
|
return [Color(red: 0.15, green: 0.15, blue: 0.15), Color(red: 0.3, green: 0.3, blue: 0.3)]
|
|
case .mixtape:
|
|
return [Color(red: 0.95, green: 0.45, blue: 0.35), Color(red: 0.95, green: 0.65, blue: 0.25)]
|
|
case .heartfelt:
|
|
return [Color(red: 0.9, green: 0.45, blue: 0.55), Color(red: 0.95, green: 0.6, blue: 0.65)]
|
|
case .luxe:
|
|
return [Color(red: 0.75, green: 0.6, blue: 0.35), Color(red: 0.55, green: 0.45, blue: 0.25)]
|
|
case .forecast:
|
|
return [Color(red: 0.4, green: 0.65, blue: 0.85), Color(red: 0.3, green: 0.5, blue: 0.75)]
|
|
case .playful:
|
|
return [Color(red: 0.95, green: 0.55, blue: 0.35), Color(red: 0.95, green: 0.75, blue: 0.35)]
|
|
case .journal:
|
|
return [Color(red: 0.55, green: 0.45, blue: 0.35), Color(red: 0.4, green: 0.35, blue: 0.3)]
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
)
|
|
}
|
|
.accessibilityIdentifier(AccessibilityID.Debug.paywallStyleOption(style.displayName))
|
|
.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"
|
|
case .zen: return "circle"
|
|
case .editorial: return "textformat"
|
|
case .mixtape: return "opticaldisc.fill"
|
|
case .heartfelt: return "heart.fill"
|
|
case .luxe: return "diamond.fill"
|
|
case .forecast: return "cloud.fill"
|
|
case .playful: return "face.smiling.fill"
|
|
case .journal: return "book.closed.fill"
|
|
}
|
|
}
|
|
|
|
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
|
|
)
|
|
case .zen:
|
|
return LinearGradient(
|
|
colors: [Color(red: 0.6, green: 0.7, blue: 0.6), Color(red: 0.5, green: 0.6, blue: 0.55)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
case .editorial:
|
|
return LinearGradient(
|
|
colors: [Color(red: 0.15, green: 0.15, blue: 0.15), Color(red: 0.3, green: 0.3, blue: 0.3)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
case .mixtape:
|
|
return LinearGradient(
|
|
colors: [Color(red: 0.95, green: 0.45, blue: 0.35), Color(red: 0.95, green: 0.65, blue: 0.25)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
case .heartfelt:
|
|
return LinearGradient(
|
|
colors: [Color(red: 0.9, green: 0.45, blue: 0.55), Color(red: 0.95, green: 0.6, blue: 0.65)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
case .luxe:
|
|
return LinearGradient(
|
|
colors: [Color(red: 0.75, green: 0.6, blue: 0.35), Color(red: 0.55, green: 0.45, blue: 0.25)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
case .forecast:
|
|
return LinearGradient(
|
|
colors: [Color(red: 0.4, green: 0.65, blue: 0.85), Color(red: 0.3, green: 0.5, blue: 0.75)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
case .playful:
|
|
return LinearGradient(
|
|
colors: [Color(red: 0.95, green: 0.55, blue: 0.35), Color(red: 0.95, green: 0.75, blue: 0.35)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
case .journal:
|
|
return LinearGradient(
|
|
colors: [Color(red: 0.55, green: 0.45, blue: 0.35), Color(red: 0.4, green: 0.35, blue: 0.3)],
|
|
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
|
|
}
|
|
}
|
|
.onDisappear {
|
|
animate = false
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
.onDisappear {
|
|
bloom = false
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
.onDisappear {
|
|
pulse = false
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
.onDisappear {
|
|
breathe = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ZenMiniPreview: View {
|
|
@State private var breathe = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Warm paper background
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.96, green: 0.94, blue: 0.90),
|
|
Color(red: 0.92, green: 0.90, blue: 0.86)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Content
|
|
VStack(spacing: 16) {
|
|
// Enso circle
|
|
Circle()
|
|
.stroke(
|
|
Color(red: 0.3, green: 0.35, blue: 0.3),
|
|
style: StrokeStyle(lineWidth: 3, lineCap: .round)
|
|
)
|
|
.frame(width: breathe ? 55 : 50, height: breathe ? 55 : 50)
|
|
.rotationEffect(.degrees(-30))
|
|
|
|
Text("Find Your\nInner Peace")
|
|
.font(.system(size: 18, weight: .light, design: .serif))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(Color(red: 0.25, green: 0.25, blue: 0.2))
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 4).repeatForever(autoreverses: true)) {
|
|
breathe = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
breathe = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct EditorialMiniPreview: View {
|
|
var body: some View {
|
|
ZStack {
|
|
// Deep black background
|
|
Color.black
|
|
|
|
// Content
|
|
VStack(spacing: 16) {
|
|
// Simple geometric element
|
|
Rectangle()
|
|
.fill(Color.white)
|
|
.frame(width: 40, height: 2)
|
|
|
|
Text("THE ART\nOF FEELING")
|
|
.font(.system(size: 16, weight: .bold, design: .serif))
|
|
.tracking(3)
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(.white)
|
|
|
|
Rectangle()
|
|
.fill(Color.white)
|
|
.frame(width: 40, height: 2)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MixtapeMiniPreview: View {
|
|
@State private var spin = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Warm gradient background
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.95, green: 0.45, blue: 0.35),
|
|
Color(red: 0.95, green: 0.65, blue: 0.25)
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
|
|
// Content
|
|
VStack(spacing: 12) {
|
|
// Mini cassette
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.fill(Color.black.opacity(0.8))
|
|
.frame(width: 50, height: 32)
|
|
|
|
HStack(spacing: 10) {
|
|
Circle()
|
|
.fill(Color.white.opacity(0.9))
|
|
.frame(width: 14, height: 14)
|
|
.rotationEffect(.degrees(spin ? 360 : 0))
|
|
Circle()
|
|
.fill(Color.white.opacity(0.9))
|
|
.frame(width: 14, height: 14)
|
|
.rotationEffect(.degrees(spin ? 360 : 0))
|
|
}
|
|
}
|
|
|
|
Text("YOUR MOOD\nMIXTAPE")
|
|
.font(.system(size: 14, weight: .black))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(.white)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.linear(duration: 3).repeatForever(autoreverses: false)) {
|
|
spin = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
spin = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct HeartfeltMiniPreview: View {
|
|
@State private var beat = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Soft pink gradient
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 0.95, blue: 0.95),
|
|
Color(red: 0.98, green: 0.9, blue: 0.92)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Content
|
|
VStack(spacing: 12) {
|
|
// Floating hearts
|
|
HStack(spacing: -8) {
|
|
ForEach(0..<3, id: \.self) { i in
|
|
Image(systemName: "heart.fill")
|
|
.font(.system(size: 20 - CGFloat(i * 4)))
|
|
.foregroundColor(Color(red: 0.9, green: 0.45, blue: 0.55))
|
|
.scaleEffect(beat ? 1.1 : 0.95)
|
|
}
|
|
}
|
|
|
|
Text("Feel With\nAll Your Heart")
|
|
.font(.system(size: 17, weight: .medium, design: .serif))
|
|
.italic()
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(Color(red: 0.4, green: 0.25, blue: 0.3))
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) {
|
|
beat = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
beat = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct LuxeMiniPreview: View {
|
|
@State private var shimmer = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Deep rich background
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.12, green: 0.1, blue: 0.08),
|
|
Color(red: 0.08, green: 0.06, blue: 0.04)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Content
|
|
VStack(spacing: 14) {
|
|
// Diamond icon
|
|
Image(systemName: "diamond.fill")
|
|
.font(.system(size: 36))
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.85, green: 0.7, blue: 0.45),
|
|
Color(red: 0.65, green: 0.5, blue: 0.3)
|
|
],
|
|
startPoint: shimmer ? .topLeading : .bottomTrailing,
|
|
endPoint: shimmer ? .bottomTrailing : .topLeading
|
|
)
|
|
)
|
|
|
|
Text("Elevate Your\nEmotional Life")
|
|
.font(.system(size: 16, weight: .light, design: .serif))
|
|
.tracking(1)
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(Color(red: 0.85, green: 0.8, blue: 0.7))
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
|
|
shimmer = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
shimmer = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ForecastMiniPreview: View {
|
|
@State private var drift = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Sky gradient
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.55, green: 0.75, blue: 0.95),
|
|
Color(red: 0.4, green: 0.6, blue: 0.85)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Floating clouds
|
|
HStack(spacing: 20) {
|
|
Image(systemName: "cloud.fill")
|
|
.font(.system(size: 28))
|
|
.foregroundColor(.white.opacity(0.8))
|
|
.offset(x: drift ? 5 : -5)
|
|
|
|
Image(systemName: "sun.max.fill")
|
|
.font(.system(size: 24))
|
|
.foregroundColor(Color(red: 1.0, green: 0.85, blue: 0.4))
|
|
}
|
|
.offset(y: -30)
|
|
|
|
// Content
|
|
VStack(spacing: 8) {
|
|
Text("Your Emotional\nForecast")
|
|
.font(.system(size: 17, weight: .semibold, design: .rounded))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(.white)
|
|
}
|
|
.offset(y: 30)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
|
|
drift = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
drift = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PlayfulMiniPreview: View {
|
|
@State private var bounce = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Warm playful gradient
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 1.0, green: 0.98, blue: 0.94),
|
|
Color(red: 0.98, green: 0.95, blue: 0.9)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Bouncing emojis
|
|
HStack(spacing: 8) {
|
|
Text("😊")
|
|
.font(.system(size: 28))
|
|
.offset(y: bounce ? -8 : 0)
|
|
Text("🎉")
|
|
.font(.system(size: 24))
|
|
.offset(y: bounce ? 0 : -8)
|
|
Text("✨")
|
|
.font(.system(size: 20))
|
|
.offset(y: bounce ? -8 : 0)
|
|
}
|
|
.offset(y: -30)
|
|
|
|
// Content
|
|
VStack(spacing: 8) {
|
|
Text("Make Tracking\nFun Again!")
|
|
.font(.system(size: 17, weight: .bold, design: .rounded))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(Color(red: 0.3, green: 0.25, blue: 0.2))
|
|
}
|
|
.offset(y: 35)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true)) {
|
|
bounce = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
bounce = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct JournalMiniPreview: View {
|
|
var body: some View {
|
|
ZStack {
|
|
// Paper texture background
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.95, green: 0.92, blue: 0.88),
|
|
Color(red: 0.92, green: 0.88, blue: 0.82)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Horizontal lines like notebook paper
|
|
VStack(spacing: 18) {
|
|
ForEach(0..<6, id: \.self) { _ in
|
|
Rectangle()
|
|
.fill(Color(red: 0.7, green: 0.65, blue: 0.6).opacity(0.3))
|
|
.frame(height: 1)
|
|
}
|
|
}
|
|
.padding(.horizontal, 30)
|
|
|
|
// Content
|
|
VStack(spacing: 12) {
|
|
Image(systemName: "book.closed.fill")
|
|
.font(.system(size: 32))
|
|
.foregroundColor(Color(red: 0.5, green: 0.4, blue: 0.35))
|
|
|
|
Text("Write Your\nEmotional Story")
|
|
.font(.system(size: 16, weight: .medium, design: .serif))
|
|
.italic()
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(Color(red: 0.35, green: 0.3, blue: 0.25))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
PaywallPreviewSettingsView()
|
|
.environmentObject(IAPManager())
|
|
}
|
|
}
|
|
#endif
|