// // OnboardingView.swift // Focale // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // // The #1 product risk (CLAUDE.md §12): full photo access refused. // We explain *why* before showing the system dialog — a naked dialog // is a refused permission is a lost install. The fact that nothing // leaves the device is the argument, said at exactly this spot. // import SwiftUI struct OnboardingView: View { @Environment(AppModel.self) private var app @State private var isRequesting = false var body: some View { VStack(spacing: 28) { Spacer() Image(systemName: "camera.aperture") .font(.system(size: 64)) .foregroundStyle(DesignTokens.accent) Text("Tout ce que tu photographies\ndevient retrouvable") .font(.title2.bold()) .multilineTextAlignment(.center) VStack(alignment: .leading, spacing: 18) { explanationRow( symbol: "iphone", title: "Rien ne quitte ton iPhone. Jamais.", detail: "L'analyse se fait entièrement sur l'appareil. Aucun serveur, aucun compte, aucune option cloud." ) explanationRow( symbol: "photo.stack", title: "Tes photos restent exactement où elles sont", detail: "Focale lit ta photothèque pour l'indexer. Aucune photo n'est copiée, déplacée ou modifiée." ) explanationRow( symbol: "magnifyingglass", title: "L'accès complet, c'est ce qui rend la recherche possible", detail: "Reçus, tableaux blancs, numéros de série : pour les retrouver, Focale doit pouvoir voir toute ta photothèque." ) } .padding(.horizontal, 8) Spacer() Button { isRequesting = true Task { await app.requestPhotoAccess() isRequesting = false } } label: { Text("Autoriser l'accès à mes photos") .font(.headline) .frame(maxWidth: .infinity) .padding(.vertical, 6) } .buttonStyle(.borderedProminent) .disabled(isRequesting) } .padding(24) .background(DesignTokens.surface) } private func explanationRow(symbol: String, title: String, detail: String) -> some View { HStack(alignment: .top, spacing: 14) { Image(systemName: symbol) .font(.title3) .foregroundStyle(DesignTokens.accent) .frame(width: 30) VStack(alignment: .leading, spacing: 3) { Text(title).font(.subheadline.weight(.semibold)) Text(detail) .font(.footnote) .foregroundStyle(DesignTokens.textSecondary) } } } } /// Dedicated screen for `.limited` — an explanation, not an error message /// (CLAUDE.md §2). struct LimitedAccessView: View { var body: some View { VStack(spacing: 20) { Image(systemName: "photo.badge.exclamationmark") .font(.system(size: 52)) .foregroundStyle(DesignTokens.accent) Text("Focale a besoin de l'accès complet") .font(.title3.bold()) Text(""" Avec un accès limité, Focale ne peut ni indexer ta photothèque \ ni retrouver tes photos. Rien ne quitte ton iPhone — l'analyse \ se fait entièrement sur l'appareil. Dans Réglages → Confidentialité → Photos, choisis « Accès complet » \ pour Focale. """) .font(.callout) .foregroundStyle(DesignTokens.textSecondary) .multilineTextAlignment(.center) Button("Ouvrir les Réglages") { if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url) } } .buttonStyle(.borderedProminent) } .padding(28) .background(DesignTokens.surface) } }