205 lines
7.2 KiB
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: {}
|
|
)
|
|
}
|
|
}
|