Andromida/Andromida/App/Views/Onboarding/NotificationStepView.swift

205 lines
7.2 KiB
Swift

import SwiftUI
import Bedrock
/// The notification permission screen where users can enable reminders.
/// Shown after the first check-in to maximize conversion after experiencing value.
struct NotificationStepView: View {
let selectedTimes: Set<OnboardingTimePreference>
let reminderScheduler: ReminderScheduler
let onComplete: () -> Void
@State private var animateIcon = false
@State private var animateContent = false
@State private var animateButtons = false
@State private var isRequestingPermission = false
/// Personalized description based on the user's selected time preference
private var personalizedDescription: String {
if selectedTimes.isEmpty {
return String(localized: "Get a gentle reminder when it's time for your rituals")
}
if selectedTimes.count == 1, let first = selectedTimes.first {
switch first {
case .morning:
return String(localized: "Get a gentle nudge each morning to start your day right")
case .midday:
return String(localized: "Stay centered with a reminder when it's time for your midday ritual")
case .afternoon:
return String(localized: "Beat the slump with a reminder when it's time for your afternoon ritual")
case .evening:
return String(localized: "Wind down with a reminder when it's time for your evening ritual")
case .night:
return String(localized: "Prepare for rest with a reminder when it's time for your night ritual")
}
}
return String(localized: "We'll remind you at the perfect times for your rituals throughout the day")
}
var body: some View {
VStack(spacing: Design.Spacing.xxLarge) {
Spacer()
// Animated bell icon
animatedBellView
.frame(width: 160, height: 160)
.opacity(animateIcon ? 1 : 0)
.scaleEffect(animateIcon ? 1 : 0.8)
// Text content
VStack(spacing: Design.Spacing.medium) {
Text(String(localized: "Stay on track"))
.typography(.heroBold)
.foregroundStyle(AppTextColors.primary)
.multilineTextAlignment(.center)
Text(personalizedDescription)
.typography(.body)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, Design.Spacing.xxLarge)
}
.opacity(animateContent ? 1 : 0)
.offset(y: animateContent ? 0 : 20)
Spacer()
// Buttons
VStack(spacing: Design.Spacing.medium) {
// Primary CTA
Button(action: handleEnableNotifications) {
HStack(spacing: Design.Spacing.small) {
if isRequestingPermission {
ProgressView()
.tint(AppTextColors.inverse)
} else {
Image(systemName: "bell.fill")
Text(String(localized: "Enable Notifications"))
}
}
.typography(.heading)
.foregroundStyle(AppTextColors.inverse)
.frame(maxWidth: .infinity)
.frame(height: AppMetrics.Size.buttonHeight)
.background(AppAccent.primary)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
}
.disabled(isRequestingPermission)
// Secondary skip option
Button(action: onComplete) {
Text(String(localized: "Maybe Later"))
.typography(.body)
.foregroundStyle(AppTextColors.secondary)
}
.disabled(isRequestingPermission)
}
.padding(.horizontal, Design.Spacing.xxLarge)
.opacity(animateButtons ? 1 : 0)
.offset(y: animateButtons ? 0 : 20)
Spacer()
.frame(height: Design.Spacing.xxLarge)
}
.onAppear {
startAnimations()
}
}
// MARK: - Animated Bell
private var animatedBellView: some View {
ZStack {
// Outer pulse circle
Circle()
.fill(AppAccent.primary.opacity(0.1))
.frame(width: 160, height: 160)
.scaleEffect(animateIcon ? 1.1 : 1.0)
.animation(
.easeInOut(duration: 2).repeatForever(autoreverses: true),
value: animateIcon
)
// Middle circle
Circle()
.fill(AppAccent.primary.opacity(0.15))
.frame(width: 120, height: 120)
.scaleEffect(animateIcon ? 1.05 : 1.0)
.animation(
.easeInOut(duration: 1.5).repeatForever(autoreverses: true).delay(0.2),
value: animateIcon
)
// Inner circle with bell
Circle()
.fill(AppAccent.primary.opacity(0.2))
.frame(width: 80, height: 80)
// Bell icon with ring animation
Image(systemName: "bell.fill")
.font(.system(size: 36, weight: .medium))
.foregroundStyle(AppAccent.primary)
.rotationEffect(.degrees(animateIcon ? 10 : -10))
.animation(
.easeInOut(duration: 0.5).repeatForever(autoreverses: true),
value: animateIcon
)
}
}
// MARK: - Actions
private func handleEnableNotifications() {
isRequestingPermission = true
Task {
let granted = await reminderScheduler.requestAuthorization()
if granted {
// Enable reminders in the scheduler
reminderScheduler.remindersEnabled = true
}
await MainActor.run {
isRequestingPermission = false
onComplete()
}
}
}
// MARK: - Animations
private func startAnimations() {
// Stagger the animations
withAnimation(.easeOut(duration: 0.6)) {
animateIcon = true
}
withAnimation(.easeOut(duration: 0.6).delay(0.3)) {
animateContent = true
}
withAnimation(.easeOut(duration: 0.6).delay(0.5)) {
animateButtons = true
}
}
}
#Preview {
ZStack {
LinearGradient(
colors: [AppSurface.primary, AppSurface.secondary],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
NotificationStepView(
selectedTimes: [.morning],
reminderScheduler: ReminderScheduler(),
onComplete: {}
)
}
}