feat: move HealthKit permission to onboarding, remove HR write
- Add .health step to onboarding between frequency and ready - HealthStep with non-blocking permission flow (Not Now skips) - Remove requestAuthorization() from PlayerViewModel.startWorkout() - Guard live session start with isAuthorized check - Remove heart rate write from HealthKit authorization popup - Remove HR sample writing from saveWorkout (now without permission) - Add L10n keys: healthAccess, healthAccessSubtitle, allowHealthAccess, notNow - Add EN/DE/ES/FR translations - Track permission decisions through analytics - Entry animation on HealthStep (fade-in + slide-up) HealthKit permission is now asked once during onboarding, never interrupting workouts again.
This commit is contained in:
@@ -12,7 +12,7 @@ struct OnboardingView: View {
|
||||
@Environment(\.modelContext) private var context
|
||||
|
||||
enum Step: Int, CaseIterable {
|
||||
case welcome, name, level, goal, frequency, ready
|
||||
case welcome, name, level, goal, frequency, health, ready
|
||||
var progress: Double { Double(rawValue) / Double(Step.allCases.count - 1) }
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ struct OnboardingView: View {
|
||||
case .level: LevelStep(selection: $fitnessLevel)
|
||||
case .goal: GoalStep(selection: $goal)
|
||||
case .frequency: FrequencyStep(frequency: $weeklyFrequency, barriers: $selectedBarriers, allBarriers: barriers)
|
||||
case .health: HealthStep(onContinue: { advance() })
|
||||
case .ready: ReadyStep(name: name)
|
||||
}
|
||||
}
|
||||
@@ -77,11 +78,13 @@ struct OnboardingView: View {
|
||||
.animation(.spring(duration: 0.45), value: step)
|
||||
|
||||
// ── Pinned bottom button ─────────────────────────
|
||||
PrimaryButton(label: buttonLabel, action: buttonAction)
|
||||
.disabled(step == .name && name.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.bottom, 48)
|
||||
.padding(.top, 16)
|
||||
if step != .health {
|
||||
PrimaryButton(label: buttonLabel, action: buttonAction)
|
||||
.disabled(step == .name && name.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.bottom, 48)
|
||||
.padding(.top, 16)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -411,6 +414,68 @@ private struct FrequencyStep: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct HealthStep: View {
|
||||
let onContinue: () -> Void
|
||||
@State private var isRequesting = false
|
||||
@State private var appeared = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 36) {
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "heart.text.square")
|
||||
.font(.system(size: 80))
|
||||
.foregroundStyle(Theme.brand.gradient)
|
||||
|
||||
OnboardingHeader(title: L10n.onboarding.healthAccess, subtitle: L10n.onboarding.healthAccessSubtitle)
|
||||
|
||||
// Primary button — reuses shared PrimaryButton component
|
||||
PrimaryButton(label: L10n.onboarding.allowHealthAccess, action: requestHealthAccess)
|
||||
.disabled(isRequesting)
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
// Skip option
|
||||
Button {
|
||||
onContinue()
|
||||
} label: {
|
||||
Text(L10n.onboarding.notNow)
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(isRequesting)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.opacity(appeared ? 1 : 0)
|
||||
.offset(y: appeared ? 0 : 14)
|
||||
.onAppear {
|
||||
withAnimation(.spring(duration: 0.45)) { appeared = true }
|
||||
}
|
||||
}
|
||||
|
||||
private func requestHealthAccess() {
|
||||
guard !isRequesting else { return }
|
||||
isRequesting = true
|
||||
Task {
|
||||
do {
|
||||
try await HealthKitService.shared.requestAuthorization()
|
||||
} catch {
|
||||
print("[HealthStep] HealthKit authorization error: \(error)")
|
||||
// Continue — user can try later in Settings
|
||||
}
|
||||
let authorized = await HealthKitService.shared.isAuthorized
|
||||
if authorized {
|
||||
AnalyticsService.shared.healthKitPermissionGranted()
|
||||
} else {
|
||||
AnalyticsService.shared.healthKitPermissionDenied()
|
||||
}
|
||||
isRequesting = false
|
||||
onContinue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ReadyStep: View {
|
||||
let name: String
|
||||
@State private var showContent = false
|
||||
|
||||
Reference in New Issue
Block a user