SetupView.swift (7364B)
1 import SwiftUI 2 3 struct SetupView: View { 4 @EnvironmentObject private var app: AppState 5 6 var onSuccess: (() -> Void)? = nil 7 8 @State private var secretKey = "" 9 @State private var isWorking = false 10 @State private var errorMessage: String? 11 @State private var showImport = false 12 @FocusState private var secretFocused: Bool 13 14 var body: some View { 15 ScrollView { 16 VStack(spacing: 20) { 17 Spacer(minLength: 40) 18 19 Image(systemName: app.hasKey ? "lock.open.fill" : "key.radiowaves.forward.fill") 20 .font(.system(size: 64, weight: .semibold)) 21 .foregroundStyle(.green) 22 .frame(width: 112, height: 112) 23 24 VStack(spacing: 8) { 25 Text(app.hasKey ? "Identity saved on this iPhone" : "Create a Nostr identity") 26 .font(.title.weight(.semibold)) 27 .multilineTextAlignment(.center) 28 29 if let npub = app.npub { 30 Text(npub) 31 .font(.footnote.monospaced()) 32 .foregroundStyle(.secondary) 33 .multilineTextAlignment(.center) 34 .textSelection(.enabled) 35 } else { 36 Text("Radroots uses a local Nostr identity to publish and read field events.") 37 .font(.subheadline) 38 .foregroundStyle(.secondary) 39 .multilineTextAlignment(.center) 40 } 41 } 42 43 SetupErrorText(errorMessage) 44 UserPresenceStatusText(app.userPresenceStatus) 45 46 if isWorking { 47 ProgressView() 48 .controlSize(.large) 49 } 50 51 if app.hasKey { 52 Button { 53 continueWithIdentity() 54 } label: { 55 Label("Unlock Identity", systemImage: "lock.open.fill") 56 .frame(maxWidth: .infinity) 57 } 58 .buttonStyle(.borderedProminent) 59 .controlSize(.large) 60 .disabled(isWorking) 61 .accessibilityIdentifier("field_ios.setup.continue") 62 } else { 63 Button { 64 createIdentity() 65 } label: { 66 Label("Create Identity", systemImage: "plus.circle.fill") 67 .frame(maxWidth: .infinity) 68 } 69 .buttonStyle(.borderedProminent) 70 .controlSize(.large) 71 .disabled(isWorking) 72 .accessibilityIdentifier("field_ios.setup.create_identity") 73 74 Button { 75 withAnimation(.easeInOut(duration: 0.2)) { 76 showImport.toggle() 77 secretFocused = showImport 78 } 79 } label: { 80 Label("Import Secret Key", systemImage: "square.and.arrow.down") 81 .frame(maxWidth: .infinity) 82 } 83 .buttonStyle(.bordered) 84 .controlSize(.large) 85 .disabled(isWorking) 86 .accessibilityIdentifier("field_ios.setup.import_identity") 87 88 if showImport { 89 VStack(spacing: 12) { 90 SecureField("nsec or hex secret key", text: $secretKey) 91 .textInputAutocapitalization(.never) 92 .autocorrectionDisabled() 93 .focused($secretFocused) 94 .padding(14) 95 .background(Color(.secondarySystemGroupedBackground)) 96 .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) 97 .accessibilityIdentifier("field_ios.setup.secret_key") 98 99 Button { 100 importIdentity() 101 } label: { 102 Label("Import Identity", systemImage: "checkmark.circle.fill") 103 .frame(maxWidth: .infinity) 104 } 105 .buttonStyle(.borderedProminent) 106 .controlSize(.large) 107 .disabled( 108 isWorking || 109 secretKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty 110 ) 111 .accessibilityIdentifier("field_ios.setup.use_secret_key") 112 } 113 .transition(.opacity.combined(with: .move(edge: .top))) 114 } 115 } 116 117 Spacer(minLength: 24) 118 } 119 .padding() 120 .frame(maxWidth: 560) 121 .frame(maxWidth: .infinity) 122 } 123 .background(Color(.systemGroupedBackground)) 124 .toolbar(.hidden, for: .navigationBar) 125 .accessibilityIdentifier("field_ios.setup") 126 } 127 128 private func continueWithIdentity() { 129 errorMessage = nil 130 isWorking = true 131 Task { 132 do { 133 try await app.continueWithLocalIdentity() 134 onSuccess?() 135 } catch { 136 errorMessage = error.fieldRuntimeMessage 137 } 138 isWorking = false 139 } 140 } 141 142 private func createIdentity() { 143 errorMessage = nil 144 isWorking = true 145 Task { 146 do { 147 try await app.createLocalIdentity() 148 onSuccess?() 149 } catch { 150 errorMessage = error.fieldRuntimeMessage 151 } 152 isWorking = false 153 } 154 } 155 156 private func importIdentity() { 157 errorMessage = nil 158 isWorking = true 159 let submittedSecret = secretKey 160 secretKey = "" 161 Task { 162 do { 163 try await app.importNostrSecret(submittedSecret) 164 onSuccess?() 165 } catch { 166 errorMessage = error.fieldRuntimeMessage 167 } 168 isWorking = false 169 } 170 } 171 } 172 173 private struct SetupErrorText: View { 174 let message: String? 175 176 init(_ message: String?) { 177 self.message = message 178 } 179 180 var body: some View { 181 if let message { 182 Text(message) 183 .foregroundStyle(.red) 184 .multilineTextAlignment(.center) 185 .padding(.horizontal) 186 .accessibilityIdentifier("field_ios.setup.error") 187 } 188 } 189 } 190 191 private struct UserPresenceStatusText: View { 192 let message: String? 193 194 init(_ message: String?) { 195 self.message = message 196 } 197 198 var body: some View { 199 if let message { 200 Text(message) 201 .font(.footnote) 202 .foregroundStyle(.secondary) 203 .multilineTextAlignment(.center) 204 .padding(.horizontal) 205 .accessibilityIdentifier("field_ios.user_presence.status") 206 } 207 } 208 }