field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | LICENSE

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 }