1// OnboardingView.swift2// SophaxChat3//4// First-launch onboarding: 3 intro slides + username setup.5// No email, phone number, or account registration required.67import SwiftUI89// MARK: - Onboarding Page Model1011private struct OnboardingPage {12 let icon: String13 let color: Color14 let title: String15 let body: String16}1718private let pages:[OnboardingPage]=[19.init(20 icon:"lock.shield.fill", color:.accentColor,21 title:"Private by design",22 body:"No servers. No accounts. Messages travel directly between devices, encrypted end-to-end."23),24.init(25 icon:"antenna.radiowaves.left.and.right", color:.green,26 title:"Find people nearby",27 body:"Open the app on the same WiFi or Bluetooth range. Nearby devices appear instantly."28),29.init(30 icon:"globe", color:.orange,31 title:"Chat globally",32 body:"Enable Tor in Settings to reach anyone in the world — no phone number, no VPN."33),34.init(35 icon:"externaldrive.badge.exclamationmark", color:.orange,36 title:"Your keys, your device",37 body:"Your identity and messages are stored only on this device. No backup exists — if you lose your phone, your history cannot be recovered."38),39.init(40 icon:"checkmark.shield.fill", color:.green,41 title:"Verify your contacts",42 body:"Always compare Safety Numbers with people you trust. Tap \"Verify Identity\" in any conversation to confirm you're talking to the right person."43),44]4546// MARK: - Onboarding View4748struct OnboardingView: View {49@EnvironmentObject var appState: AppState5051@State private var currentPage = 052@State private var username =""53@FocusState private var isTextFieldFocused: Bool5455 private let totalPages = pages.count + 1 // slides + username page5657 var body: some View {58 ZStack(alignment:.topTrailing){59 TabView(selection: $currentPage){60 ForEach(Array(pages.enumerated()), id: \.offset){ index, page in61 SlidePage(page: page)62.tag(index)63}64 UsernamePage(username: $username, focused: $isTextFieldFocused, onSubmit: createIdentity)65.tag(pages.count)66}67.tabViewStyle(.page)68.indexViewStyle(.page(backgroundDisplayMode:.always))69.animation(.easeInOut, value: currentPage)7071// Skip button — only visible on intro slides72 if currentPage < pages.count {73 Button("Skip"){74 withAnimation { currentPage = pages.count }75}76.font(.subheadline)77.foregroundStyle(.secondary)78.padding()79}80}81}8283 private func createIdentity(){84 let name = username.trimmingCharacters(in:.whitespaces)85 guard name.isValidUsername else { return }86 isTextFieldFocused = false87 appState.createIdentity(username: name)88}89}9091// MARK: - Slide Page9293private struct SlidePage: View {94 let page: OnboardingPage9596 var body: some View {97 VStack(spacing: 32){98 Spacer()99 ZStack {100 Circle()101.fill(page.color.opacity(0.12))102.frame(width: 120, height: 120)103 Image(systemName: page.icon)104.font(.system(size: 52))105.foregroundStyle(page.color)106}107 VStack(spacing: 12){108 Text(page.title)109.font(.title.bold())110.multilineTextAlignment(.center)111 Text(page.body)112.font(.body)113.foregroundStyle(.secondary)114.multilineTextAlignment(.center)115.padding(.horizontal, 40)116}117 Spacer()118 Spacer()// extra bottom space for page dots119}120}121}122123// MARK: - Username Page124125private struct UsernamePage: View {126@Binding var username: String127 var focused: FocusState<Bool>.Binding128 let onSubmit:()-> Void129130 var body: some View {131 VStack(spacing: 32){132 Spacer()133134 ZStack {135 Circle()136.fill(Color.accentColor.opacity(0.12))137.frame(width: 120, height: 120)138 Image(systemName:"person.crop.circle.fill")139.font(.system(size: 52))140.foregroundStyle(Color.accentColor)141}142143 VStack(spacing: 8){144 Text("Choose your name")145.font(.title.bold())146 Text("Visible to nearby peers. No account required.")147.font(.body)148.foregroundStyle(.secondary)149.multilineTextAlignment(.center)150.padding(.horizontal, 40)151}152153 VStack(alignment:.leading, spacing: 8){154 TextField("e.g. alice", text: $username)155.textFieldStyle(.plain)156.font(.body)157.padding(14)158.background(Color(.secondarySystemGroupedBackground))159.clipShape(RoundedRectangle(cornerRadius: 12))160.focused(focused)161.autocorrectionDisabled()162.textInputAutocapitalization(.never)163.submitLabel(.go)164.onSubmit(onSubmit)165}166.padding(.horizontal, 32)167168 Button(action: onSubmit){169 Label("Start Chatting", systemImage:"arrow.right.circle.fill")170.font(.body.weight(.semibold))171.frame(maxWidth:.infinity)172.padding(16)173.background(username.isValidUsername ? Color.accentColor : Color.accentColor.opacity(0.3))174.foregroundStyle(.white)175.clipShape(RoundedRectangle(cornerRadius: 14))176}177.disabled(!username.isValidUsername)178.padding(.horizontal, 32)179.animation(.easeInOut, value: username.isValidUsername)180181 Spacer()182 Spacer()183}184}185}186187// MARK: - Helpers188189private extension String {190 var isValidUsername: Bool {191 let trimmed = trimmingCharacters(in:.whitespaces)192 return trimmed.count >= 1 && trimmed.count <= 64193}194}195196// MARK: - Security Info (kept for use elsewhere)197198struct SecurityInfoView: View {199@Environment(\.dismiss) private var dismiss200201 var body: some View {202 NavigationStack {203 List {204 Section("Security Features"){205 SecurityFeatureRow(icon:"lock.fill", color:.blue,206 title:"End-to-End Encryption",207 description:"Every message is encrypted on your device before sending. Only the recipient can decrypt it.")208 SecurityFeatureRow(icon:"arrow.triangle.2.circlepath", color:.green,209 title:"Double Ratchet Algorithm",210 description:"The same protocol used by Signal. Each message uses a new key — forward secrecy + break-in recovery.")211 SecurityFeatureRow(icon:"key.fill", color:.orange,212 title:"X3DH Key Agreement",213 description:"Sessions established with Extended Triple Diffie-Hellman. No server stores your keys.")214 SecurityFeatureRow(icon:"wifi.slash", color:.purple,215 title:"No Servers",216 description:"Messages travel directly between devices via Bluetooth and WiFi Direct.")217 SecurityFeatureRow(icon:"person.slash", color:.red,218 title:"No Identity Required",219 description:"No phone number, email, or account. Your identity is a cryptographic key pair.")220 SecurityFeatureRow(icon:"eye.slash", color:.pink,221 title:"Open Source",222 description:"The full source code is publicly auditable.")223}224}225.navigationTitle("How It Works")226.navigationBarTitleDisplayMode(.inline)227.toolbar {228 ToolbarItem(placement:.confirmationAction){229 Button("Done"){ dismiss()}230}231}232}233}234}235236struct SecurityFeatureRow: View {237 let icon: String238 let color: Color239 let title: String240 let description: String241242 var body: some View {243 HStack(alignment:.top, spacing: 12){244 Image(systemName: icon)245.font(.body.weight(.semibold))246.foregroundStyle(color)247.frame(width: 28)248 VStack(alignment:.leading, spacing: 2){249 Text(title).font(.subheadline.weight(.semibold))250 Text(description).font(.caption).foregroundStyle(.secondary)251}252}253.padding(.vertical, 4)254}255}256257#Preview {258 OnboardingView().environmentObject(AppState())259}