| @@ -90,9 +90,9 @@ struct AccountView: View { |
| 90 | scopes: ["full", "git"], |
90 | scopes: ["full", "git"], |
| 91 | working: model.working, |
91 | working: model.working, |
| 92 | errorMessage: model.actionError |
92 | errorMessage: model.actionError |
| 93 | ) { text, scope, label in |
93 | ) { text, scope, label, ttl in |
| 94 | Task { |
94 | Task { |
| 95 | await model.addSSHKey(text, scope: scope ?? "full", label: label) |
95 | await model.addSSHKey(text, scope: scope ?? "full", label: label, ttl: ttl) |
| 96 | if model.actionError == nil { addingSSH = false } |
96 | if model.actionError == nil { addingSSH = false } |
| 97 | } |
97 | } |
| 98 | } |
98 | } |
| @@ -104,7 +104,7 @@ struct AccountView: View { |
| 104 | scopes: nil, |
104 | scopes: nil, |
| 105 | working: model.working, |
105 | working: model.working, |
| 106 | errorMessage: model.actionError |
106 | errorMessage: model.actionError |
| 107 | ) { text, _, _ in |
107 | ) { text, _, _, _ in |
| 108 | Task { |
108 | Task { |
| 109 | await model.addPGPKey(text) |
109 | await model.addPGPKey(text) |
| 110 | if model.actionError == nil { addingPGP = false } |
110 | if model.actionError == nil { addingPGP = false } |
| @@ -209,9 +209,13 @@ struct AccountView: View { |
| 209 | HStack(spacing: 6) { |
209 | HStack(spacing: 6) { |
| 210 | Text(key.algo) |
210 | Text(key.algo) |
| 211 | GBChip(key.scope, .secondary) |
211 | GBChip(key.scope, .secondary) |
| |
212 | if let by = key.createdBy { |
| |
213 | Text("added by \(by)") |
| |
214 | } |
| 212 | } |
215 | } |
| 213 | .font(.gbSans(.caption2)) |
216 | .font(.gbSans(.caption2)) |
| 214 | .foregroundStyle(.secondary) |
217 | .foregroundStyle(.secondary) |
| |
218 | keyDates(key) |
| 215 | } |
219 | } |
| 216 | .swipeActions { |
220 | .swipeActions { |
| 217 | Button("Remove", role: .destructive) { |
221 | Button("Remove", role: .destructive) { |
| @@ -239,6 +243,29 @@ struct AccountView: View { |
| 239 | } |
243 | } |
| 240 | } |
244 | } |
| 241 | |
245 | |
| |
246 | /// When the key was last used and when it stops working, as the CLI's |
| |
247 | /// key list says it. |
| |
248 | private func keyDates(_ key: AccountViewModel.SSHKey) -> some View { |
| |
249 | HStack(spacing: 4) { |
| |
250 | if let used = key.lastUsedAt { |
| |
251 | Text("used \(used.formatted(.relative(presentation: .named)))") |
| |
252 | } else { |
| |
253 | Text("never used") |
| |
254 | } |
| |
255 | if let expires = key.expiresAt { |
| |
256 | Text("·") |
| |
257 | if key.isExpired(at: .now) { |
| |
258 | Text("expired \(expires.formatted(date: .abbreviated, time: .omitted))") |
| |
259 | .foregroundStyle(Color.gbBad) |
| |
260 | } else { |
| |
261 | Text("expires \(expires.formatted(date: .abbreviated, time: .omitted))") |
| |
262 | } |
| |
263 | } |
| |
264 | } |
| |
265 | .font(.gbSans(.caption2)) |
| |
266 | .foregroundStyle(.secondary) |
| |
267 | } |
| |
268 | |
| 242 | private func pgpSection(_ keys: [AccountViewModel.PGPKey]) -> some View { |
269 | private func pgpSection(_ keys: [AccountViewModel.PGPKey]) -> some View { |
| 243 | Section { |
270 | Section { |
| 244 | ForEach(keys) { key in |
271 | ForEach(keys) { key in |
| @@ -345,13 +372,15 @@ private struct KeyPasteSheet: View { |
| 345 | let scopes: [String]? |
372 | let scopes: [String]? |
| 346 | let working: Bool |
373 | let working: Bool |
| 347 | let errorMessage: String? |
374 | let errorMessage: String? |
| 348 | /// Text, scope (nil without scopes), label (empty without scopes). |
375 | /// Text, scope (nil without scopes), label (empty without scopes), |
| 349 | let onSubmit: (String, String?, String) -> Void |
376 | /// and the key's lifetime (nil when it does not expire, or without scopes). |
| |
377 | let onSubmit: (String, String?, String, String?) -> Void |
| 350 | |
378 | |
| 351 | @Environment(\.dismiss) private var dismiss |
379 | @Environment(\.dismiss) private var dismiss |
| 352 | @State private var text = "" |
380 | @State private var text = "" |
| 353 | @State private var scope = "full" |
381 | @State private var scope = "full" |
| 354 | @State private var label = "" |
382 | @State private var label = "" |
| |
383 | @State private var ttl: String? |
| 355 | |
384 | |
| 356 | var body: some View { |
385 | var body: some View { |
| 357 | NavigationStack { |
386 | NavigationStack { |
| @@ -376,6 +405,13 @@ private struct KeyPasteSheet: View { |
| 376 | .autocorrectionDisabled() |
405 | .autocorrectionDisabled() |
| 377 | .textInputAutocapitalization(.never) |
406 | .textInputAutocapitalization(.never) |
| 378 | .accessibilityIdentifier("key-paste-label") |
407 | .accessibilityIdentifier("key-paste-label") |
| |
408 | Picker("Expires", selection: $ttl) { |
| |
409 | Text("Never").tag(String?.none) |
| |
410 | Text("In 30 days").tag(String?.some("30d")) |
| |
411 | Text("In 90 days").tag(String?.some("90d")) |
| |
412 | Text("In a year").tag(String?.some("365d")) |
| |
413 | } |
| |
414 | .accessibilityIdentifier("key-paste-ttl") |
| 379 | } |
415 | } |
| 380 | } |
416 | } |
| 381 | if let errorMessage { |
417 | if let errorMessage { |
| @@ -395,7 +431,7 @@ private struct KeyPasteSheet: View { |
| 395 | ProgressView() |
431 | ProgressView() |
| 396 | } else { |
432 | } else { |
| 397 | Button("Add") { |
433 | Button("Add") { |
| 398 | onSubmit(text, scopes != nil ? scope : nil, scopes != nil ? label : "") |
434 | onSubmit(text, scopes != nil ? scope : nil, scopes != nil ? label : "", scopes != nil ? ttl : nil) |
| 399 | } |
435 | } |
| 400 | .disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) |
436 | .disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) |
| 401 | .accessibilityIdentifier("key-paste-submit") |
437 | .accessibilityIdentifier("key-paste-submit") |