Improve subscription UI and fix trial date handling

Lock Screen:
- Add light/dark mode support with adaptive colors
- Make passcode text tappable to trigger authentication

Trial Date Fixes:
- Fix IAPManager to read firstLaunchDate directly from UserDefaults
- Add expiration date check (> Date()) before showing "expires in" text
- Show "Trial expired" when trial end date is in the past
- Disable subscription bypass in DEBUG mode for testing

Month/Year Subscribe Prompts:
- Redesign with gradient icons and compelling copy
- Add fade mask (100% at top to 0% at 50%) for content behind
- Position subscribe overlay on bottom half of screen
- Month: purple/pink theme with calendar icon
- Year: orange/pink theme with chart icon

🤖 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-24 10:54:01 -06:00
parent 745e226ecb
commit cc9f9f9427
8 changed files with 275 additions and 78 deletions

View File

@@ -23,6 +23,7 @@ struct MoodParticle: Identifiable {
// MARK: - Aurora Gradient Background
struct AuroraBackground: View {
@Environment(\.colorScheme) private var colorScheme
@State private var animateGradient = false
private let moodColors: [Color] = [
@@ -33,14 +34,20 @@ struct AuroraBackground: View {
Color(hex: "ff453a"), // horrible - red
]
private var isDark: Bool { colorScheme == .dark }
var body: some View {
ZStack {
// Base dark gradient
// Base gradient - adapts to color scheme
LinearGradient(
colors: [
colors: isDark ? [
Color(hex: "0a0a0f"),
Color(hex: "12121a"),
Color(hex: "0d0d14")
] : [
Color(hex: "f8f9fa"),
Color(hex: "e9ecef"),
Color(hex: "f1f3f5")
],
startPoint: .topLeading,
endPoint: .bottomTrailing
@@ -49,8 +56,8 @@ struct AuroraBackground: View {
// Aurora layer 1 - green/blue
EllipticalGradient(
colors: [
moodColors[0].opacity(0.3),
moodColors[2].opacity(0.15),
moodColors[0].opacity(isDark ? 0.3 : 0.2),
moodColors[2].opacity(isDark ? 0.15 : 0.1),
.clear
],
center: .topLeading,
@@ -63,8 +70,8 @@ struct AuroraBackground: View {
// Aurora layer 2 - yellow/orange
EllipticalGradient(
colors: [
moodColors[1].opacity(0.2),
moodColors[3].opacity(0.1),
moodColors[1].opacity(isDark ? 0.2 : 0.15),
moodColors[3].opacity(isDark ? 0.1 : 0.08),
.clear
],
center: .bottomTrailing,
@@ -77,7 +84,7 @@ struct AuroraBackground: View {
// Aurora layer 3 - subtle red accent
RadialGradient(
colors: [
moodColors[4].opacity(0.15),
moodColors[4].opacity(isDark ? 0.15 : 0.1),
.clear
],
center: UnitPoint(x: 0.8, y: 0.3),
@@ -89,7 +96,7 @@ struct AuroraBackground: View {
// Noise texture overlay
Rectangle()
.fill(.white.opacity(0.015))
.fill((isDark ? Color.white : Color.black).opacity(0.015))
.background(
Canvas { context, size in
for _ in 0..<1000 {
@@ -98,7 +105,7 @@ struct AuroraBackground: View {
let opacity = Double.random(in: 0.01...0.04)
context.fill(
Path(ellipseIn: CGRect(x: x, y: y, width: 1, height: 1)),
with: .color(.white.opacity(opacity))
with: .color((isDark ? Color.white : Color.black).opacity(opacity))
)
}
}
@@ -190,6 +197,7 @@ struct FloatingAnimation: ViewModifier {
// MARK: - Central Breathing Orb
struct BreathingOrb: View {
@Environment(\.colorScheme) private var colorScheme
@State private var breathe = false
@State private var rotate = false
@@ -201,6 +209,8 @@ struct BreathingOrb: View {
Color(hex: "ff453a"),
]
private var isDark: Bool { colorScheme == .dark }
var body: some View {
ZStack {
// Outer glow
@@ -215,7 +225,7 @@ struct BreathingOrb: View {
)
.frame(width: 180, height: 180)
.blur(radius: 40)
.opacity(0.6)
.opacity(isDark ? 0.6 : 0.5)
.scaleEffect(breathe ? 1.2 : 0.9)
.rotationEffect(.degrees(rotate ? 360 : 0))
@@ -229,7 +239,7 @@ struct BreathingOrb: View {
)
.frame(width: 120, height: 120)
.blur(radius: 20)
.opacity(0.8)
.opacity(isDark ? 0.8 : 0.6)
.scaleEffect(breathe ? 1.1 : 0.95)
.rotationEffect(.degrees(rotate ? -360 : 0))
@@ -237,10 +247,14 @@ struct BreathingOrb: View {
Circle()
.fill(
RadialGradient(
colors: [
colors: isDark ? [
.white.opacity(0.9),
.white.opacity(0.3),
.clear
] : [
.white,
.white.opacity(0.6),
.clear
],
center: .center,
startRadius: 0,
@@ -249,12 +263,13 @@ struct BreathingOrb: View {
)
.frame(width: 80, height: 80)
.scaleEffect(breathe ? 1.05 : 0.98)
.shadow(color: .black.opacity(isDark ? 0 : 0.1), radius: 10)
// Glossy highlight
Ellipse()
.fill(
LinearGradient(
colors: [.white.opacity(0.4), .clear],
colors: [.white.opacity(isDark ? 0.4 : 0.8), .clear],
startPoint: .top,
endPoint: .center
)
@@ -283,6 +298,7 @@ struct BreathingOrb: View {
// MARK: - Glassmorphic Button
struct GlassButton: View {
@Environment(\.colorScheme) private var colorScheme
let icon: String
let title: String
let action: () -> Void
@@ -290,6 +306,10 @@ struct GlassButton: View {
@State private var isPressed = false
@State private var pulse = false
private var isDark: Bool { colorScheme == .dark }
private var foregroundColor: Color { isDark ? .white : .primary }
private var accentOpacity: Double { isDark ? 0.15 : 0.1 }
var body: some View {
Button(action: action) {
HStack(spacing: 14) {
@@ -297,24 +317,24 @@ struct GlassButton: View {
// Pulse ring
Circle()
.stroke(lineWidth: 2)
.foregroundColor(.white.opacity(0.3))
.foregroundColor(foregroundColor.opacity(0.3))
.frame(width: 44, height: 44)
.scaleEffect(pulse ? 1.3 : 1)
.opacity(pulse ? 0 : 0.6)
// Icon background
Circle()
.fill(.white.opacity(0.15))
.fill(foregroundColor.opacity(accentOpacity))
.frame(width: 44, height: 44)
Image(systemName: icon)
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
.foregroundColor(foregroundColor)
}
Text(title)
.font(.system(size: 17, weight: .semibold, design: .rounded))
.foregroundColor(.white)
.foregroundColor(foregroundColor)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 18)
@@ -324,16 +344,15 @@ struct GlassButton: View {
// Glass effect
RoundedRectangle(cornerRadius: 20)
.fill(.ultraThinMaterial)
.opacity(0.8)
// Border gradient
RoundedRectangle(cornerRadius: 20)
.stroke(
LinearGradient(
colors: [
.white.opacity(0.4),
.white.opacity(0.1),
.white.opacity(0.2)
foregroundColor.opacity(0.3),
foregroundColor.opacity(0.1),
foregroundColor.opacity(0.15)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
@@ -341,11 +360,11 @@ struct GlassButton: View {
lineWidth: 1
)
// Inner shadow
// Inner highlight
RoundedRectangle(cornerRadius: 20)
.fill(
LinearGradient(
colors: [.white.opacity(0.1), .clear],
colors: [foregroundColor.opacity(0.08), .clear],
startPoint: .top,
endPoint: .bottom
)
@@ -382,11 +401,16 @@ struct GlassButton: View {
// MARK: - Main Lock Screen View
struct LockScreenView: View {
@Environment(\.colorScheme) private var colorScheme
@ObservedObject var authManager: BiometricAuthManager
@State private var showError = false
@State private var showContent = false
private var isDark: Bool { colorScheme == .dark }
private var primaryText: Color { isDark ? .white : .primary }
private var secondaryText: Color { isDark ? .white.opacity(0.7) : .secondary }
private var tertiaryText: Color { isDark ? .white.opacity(0.5) : .secondary.opacity(0.8) }
var body: some View {
ZStack {
// Aurora background
@@ -411,11 +435,11 @@ struct LockScreenView: View {
VStack(spacing: 12) {
Text("Your Feelings")
.font(.system(size: 32, weight: .light, design: .serif))
.foregroundColor(.white)
.foregroundColor(primaryText)
Text("are safe here")
.font(.system(size: 32, weight: .ultraLight, design: .serif))
.foregroundColor(.white.opacity(0.7))
.foregroundColor(secondaryText)
}
.opacity(showContent ? 1 : 0)
.offset(y: showContent ? 0 : 20)
@@ -425,7 +449,7 @@ struct LockScreenView: View {
Text("Authenticate to continue your journey")
.font(.system(size: 14, weight: .regular, design: .rounded))
.foregroundColor(.white.opacity(0.5))
.foregroundColor(tertiaryText)
.opacity(showContent ? 1 : 0)
Spacer()
@@ -447,13 +471,23 @@ struct LockScreenView: View {
.offset(y: showContent ? 0 : 30)
.padding(.horizontal, 32)
// Passcode hint
// Passcode button - tappable to authenticate with passcode
if authManager.canUseDevicePasscode {
Text("Or use your device passcode")
.font(.system(size: 12, weight: .regular, design: .rounded))
.foregroundColor(.white.opacity(0.35))
.padding(.top, 16)
.opacity(showContent ? 1 : 0)
Button {
Task {
let success = await authManager.authenticate()
if !success {
showError = true
}
}
} label: {
Text("Or use your device passcode")
.font(.system(size: 13, weight: .medium, design: .rounded))
.foregroundColor(isDark ? .white.opacity(0.5) : .accentColor)
}
.disabled(authManager.isAuthenticating)
.padding(.top, 16)
.opacity(showContent ? 1 : 0)
}
Spacer()
@@ -490,12 +524,12 @@ struct LockScreenView: View {
// MARK: - Preview
#Preview("Lock Screen - Face ID") {
#Preview("Lock Screen - Dark") {
LockScreenView(authManager: BiometricAuthManager())
.preferredColorScheme(.dark)
}
#Preview("Lock Screen - Touch ID") {
#Preview("Lock Screen - Light") {
LockScreenView(authManager: BiometricAuthManager())
.preferredColorScheme(.dark)
.preferredColorScheme(.light)
}