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:
@@ -1183,6 +1183,10 @@
|
||||
"comment" : "An accessibility label for the section of the settings view that indicates that Apple Health is not available on the user's device.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"are safe here" : {
|
||||
"comment" : "A description of the safety of the app's \"Aurora background\".",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Are you sure you want to delete this mood entry? This cannot be undone." : {
|
||||
"comment" : "An alert message displayed when the user attempts to delete a mood entry.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -1269,6 +1273,7 @@
|
||||
},
|
||||
"Authenticate to access your mood data" : {
|
||||
"comment" : "A description of the purpose of the authentication process in the lock screen.",
|
||||
"extractionState" : "stale",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
@@ -1309,6 +1314,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Authenticate to continue your journey" : {
|
||||
"comment" : "A description of the action required to unlock the app.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Authentication Failed" : {
|
||||
"comment" : "An alert title when authentication fails.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -4310,6 +4319,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Discover your emotional rhythm across the year. Spot trends and celebrate how far you've come." : {
|
||||
"comment" : "A description of what the \"See Your Year at a Glance\" feature does.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Don't break your streak!" : {
|
||||
"comment" : "A description of the current streak or a motivational message.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -4692,6 +4705,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Explore Your Mood History" : {
|
||||
"comment" : "A title for a feature that allows users to explore their mood history.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Export" : {
|
||||
"comment" : "A button label that triggers data export.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -5076,6 +5093,7 @@
|
||||
},
|
||||
"Feels is Locked" : {
|
||||
"comment" : "The title of the lock screen.",
|
||||
"extractionState" : "stale",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
@@ -10781,6 +10799,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"See your complete monthly journey. Track patterns and understand what shapes your days." : {
|
||||
|
||||
},
|
||||
"See Your Year at a Glance" : {
|
||||
"comment" : "A title for a feature that lets users see their year's emotional trends.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Select this mood" : {
|
||||
"comment" : "A hint that appears when a user taps on a mood button.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@@ -11653,6 +11678,7 @@
|
||||
},
|
||||
"Subscribe to see your full year" : {
|
||||
"comment" : "A button label that appears when the user is subscribed to Feels.",
|
||||
"extractionState" : "stale",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
@@ -13088,6 +13114,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Unlock Full History" : {
|
||||
"comment" : "A button label that appears when a user is on a free trial and wants to unlock all of their mood history.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Unlock Premium" : {
|
||||
"comment" : "A button label that says \"Unlock Premium\".",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -13174,6 +13204,7 @@
|
||||
},
|
||||
"Unlock with %@" : {
|
||||
"comment" : "A button label that instructs the user to unlock their device using their biometric authentication method. The text inside the parentheses is replaced with the name of the biometric authentication method available on the user's device (e.g",
|
||||
"extractionState" : "stale",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
@@ -13214,6 +13245,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Unlock Year Overview" : {
|
||||
"comment" : "A button label that appears when the user is not a premium subscriber, encouraging them to subscribe to unlock more features.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Use Siri to Log Moods" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
@@ -14186,6 +14221,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Your Feelings" : {
|
||||
"comment" : "The title of the main screen in the lock screen.",
|
||||
"isCommentAutoGenerated" : true
|
||||
}
|
||||
},
|
||||
"version" : "1.1"
|
||||
|
||||
@@ -33,8 +33,9 @@ class IAPManager: ObservableObject {
|
||||
// MARK: - Debug Toggle
|
||||
|
||||
/// Set to `true` to bypass all subscription checks and grant full access (for development only)
|
||||
/// Set to `false` to test trial/subscription behavior in DEBUG builds
|
||||
#if DEBUG
|
||||
static let bypassSubscription = true
|
||||
static let bypassSubscription = false
|
||||
#else
|
||||
static let bypassSubscription = false
|
||||
#endif
|
||||
@@ -59,8 +60,16 @@ class IAPManager: ObservableObject {
|
||||
|
||||
// MARK: - Storage
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
private var firstLaunchDate = Date()
|
||||
/// Reads firstLaunchDate directly from UserDefaults to ensure we always get the latest value
|
||||
/// (Using @AppStorage in a class doesn't auto-sync when other components update the same key)
|
||||
private var firstLaunchDate: Date {
|
||||
get {
|
||||
GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.firstLaunchDate.rawValue) as? Date ?? Date()
|
||||
}
|
||||
set {
|
||||
GroupUserDefaults.groupDefaults.set(newValue, forKey: UserDefaultsStore.Keys.firstLaunchDate.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
@@ -21,15 +21,20 @@ struct IAPWarningView: View {
|
||||
Image(systemName: "clock")
|
||||
.foregroundColor(.orange)
|
||||
|
||||
Text(String(localized: "iap_warning_view_title"))
|
||||
.font(.body)
|
||||
.foregroundColor(textColor)
|
||||
if let expirationDate = iapManager.trialExpirationDate, expirationDate > Date() {
|
||||
Text(String(localized: "iap_warning_view_title"))
|
||||
.font(.body)
|
||||
.foregroundColor(textColor)
|
||||
|
||||
if let expirationDate = iapManager.trialExpirationDate {
|
||||
Text(expirationDate, style: .relative)
|
||||
.font(.body)
|
||||
.bold()
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text(String(localized: "purchase_view_trial_expired"))
|
||||
.font(.body)
|
||||
.bold()
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -109,6 +109,21 @@ struct MonthView: View {
|
||||
)
|
||||
}
|
||||
.scrollDisabled(iapManager.shouldShowPaywall)
|
||||
.mask(
|
||||
// Fade effect when paywall should show: 100% at top, 0% halfway down
|
||||
iapManager.shouldShowPaywall ?
|
||||
AnyView(
|
||||
LinearGradient(
|
||||
gradient: Gradient(stops: [
|
||||
.init(color: .black, location: 0),
|
||||
.init(color: .black, location: 0.3),
|
||||
.init(color: .clear, location: 0.5)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
) : AnyView(Color.black)
|
||||
)
|
||||
}
|
||||
|
||||
// Hidden text to trigger updates when custom tint changes
|
||||
@@ -116,27 +131,72 @@ struct MonthView: View {
|
||||
.hidden()
|
||||
|
||||
if iapManager.shouldShowPaywall {
|
||||
// Paywall overlay - tap to show subscription store
|
||||
Color.black.opacity(0.3)
|
||||
.ignoresSafeArea()
|
||||
.onTapGesture {
|
||||
showSubscriptionStore = true
|
||||
// Premium month history prompt - bottom half
|
||||
VStack(spacing: 20) {
|
||||
// Icon
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [.purple.opacity(0.2), .pink.opacity(0.2)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Image(systemName: "calendar.badge.clock")
|
||||
.font(.title)
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [.purple, .pink],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
// Text
|
||||
VStack(spacing: 10) {
|
||||
Text("Explore Your Mood History")
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("See your complete monthly journey. Track patterns and understand what shapes your days.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(textColor.opacity(0.7))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
|
||||
// Subscribe button
|
||||
Button {
|
||||
showSubscriptionStore = true
|
||||
} label: {
|
||||
Text(String(localized: "subscription_required_button"))
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill(Color.pink))
|
||||
HStack {
|
||||
Image(systemName: "calendar")
|
||||
Text("Unlock Full History")
|
||||
}
|
||||
.font(.headline.weight(.bold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [.purple, .pink],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
}
|
||||
.padding()
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
.padding(.vertical, 24)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(theme.currentTheme.bg)
|
||||
.frame(maxHeight: .infinity, alignment: .bottom)
|
||||
} else if iapManager.shouldShowTrialWarning {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
@@ -180,8 +180,12 @@ struct PurchaseButtonView: View {
|
||||
Image(systemName: "clock")
|
||||
.foregroundColor(.orange)
|
||||
|
||||
if let expirationDate = iapManager.trialExpirationDate {
|
||||
if let expirationDate = iapManager.trialExpirationDate, expirationDate > Date() {
|
||||
Text("\(Text(String(localized: "purchase_view_trial_expires_in")).foregroundColor(textColor)) \(Text(expirationDate, style: .relative).foregroundColor(.orange).bold())")
|
||||
} else {
|
||||
Text(String(localized: "purchase_view_trial_expired"))
|
||||
.foregroundColor(.orange)
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
.font(.body)
|
||||
|
||||
@@ -95,7 +95,7 @@ struct UpgradeBannerView: View {
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(.orange)
|
||||
|
||||
if let expirationDate = trialExpirationDate {
|
||||
if let expirationDate = trialExpirationDate, expirationDate > Date() {
|
||||
Text("\(Text("Trial expires in ").font(.subheadline.weight(.medium)).foregroundColor(textColor.opacity(0.8)))\(Text(expirationDate, style: .relative).font(.subheadline.weight(.bold)).foregroundColor(.orange))")
|
||||
} else {
|
||||
Text("Trial expired")
|
||||
|
||||
@@ -66,14 +66,14 @@ struct YearView: View {
|
||||
}
|
||||
.scrollDisabled(iapManager.shouldShowPaywall)
|
||||
.mask(
|
||||
// Fade effect when paywall should show: 100% at top, 0% at bottom
|
||||
// Fade effect when paywall should show: 100% at top, 0% halfway down
|
||||
iapManager.shouldShowPaywall ?
|
||||
AnyView(
|
||||
LinearGradient(
|
||||
gradient: Gradient(stops: [
|
||||
.init(color: .black, location: 0),
|
||||
.init(color: .black, location: 0.3),
|
||||
.init(color: .clear, location: 1.0)
|
||||
.init(color: .clear, location: 0.5)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
@@ -83,26 +83,72 @@ struct YearView: View {
|
||||
}
|
||||
|
||||
if iapManager.shouldShowPaywall {
|
||||
VStack {
|
||||
Spacer()
|
||||
VStack(spacing: 16) {
|
||||
Text("Subscribe to see your full year")
|
||||
.font(.headline)
|
||||
.foregroundColor(textColor)
|
||||
// Premium year overview prompt - bottom half
|
||||
VStack(spacing: 20) {
|
||||
// Icon
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [.orange.opacity(0.2), .pink.opacity(0.2)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Button {
|
||||
showSubscriptionStore = true
|
||||
} label: {
|
||||
Text(String(localized: "subscription_required_button"))
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill(Color.pink))
|
||||
}
|
||||
Image(systemName: "chart.bar.xaxis")
|
||||
.font(.title)
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [.orange, .pink],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
|
||||
// Text
|
||||
VStack(spacing: 10) {
|
||||
Text("See Your Year at a Glance")
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("Discover your emotional rhythm across the year. Spot trends and celebrate how far you've come.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(textColor.opacity(0.7))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
|
||||
// Subscribe button
|
||||
Button {
|
||||
showSubscriptionStore = true
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "chart.bar.fill")
|
||||
Text("Unlock Year Overview")
|
||||
}
|
||||
.font(.headline.weight(.bold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [.orange, .pink],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
.padding(.vertical, 24)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(theme.currentTheme.bg)
|
||||
.frame(maxHeight: .infinity, alignment: .bottom)
|
||||
} else if iapManager.shouldShowTrialWarning {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
Reference in New Issue
Block a user