Configure iPhone Git commit identity (#76)

This commit is contained in:
2026-08-12 19:03:54 +02:00
parent e45ad07c89
commit 009db5cbcf
10 changed files with 392 additions and 37 deletions

View File

@@ -712,6 +712,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
private let authentication: MobileAuthentication?
private let keyTransfer = try? mobileKeyTransfer()
private var state: MobileAuthenticationState?
private var gitIdentity: MobileGitIdentity?
private var preferenceTask: Task<Void, Never>?
private var loadTask: Task<Void, Never>?
private var loadGeneration = 0
@@ -753,14 +754,14 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
}
override func numberOfSections(in tableView: UITableView) -> Int {
page.state == .ready ? 3 : 0
page.state == .ready ? 4 : 0
}
override func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int
) -> Int {
section == 0 ? 2 : (section == 1 ? 1 : 2)
section == 1 ? 2 : (section == 3 ? 2 : 1)
}
override func tableView(
@@ -768,8 +769,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
titleForHeaderInSection section: Int
) -> String? {
switch section {
case 0: "GPG Key Transfer"
case 1: "Secure Unlock"
case 0: "Git Commit Identity"
case 1: "GPG Key Transfer"
case 2: "Secure Unlock"
default: "Authentication Session"
}
}
@@ -779,9 +781,12 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
titleForFooterInSection section: Int
) -> String? {
if section == 0 {
return "Scan or display ASCII-armored GPG keys. Private-key transfers require explicit confirmation and passphrase validation."
return "Used as the author for commits created by the iPhone app."
}
if section == 1 {
return "Scan or display ASCII-armored GPG keys. Private-key transfers require explicit confirmation and passphrase validation."
}
if section == 2 {
return "When enabled, the GPG passphrase is device-only, requires a device passcode, and is invalidated when enrolled biometrics change."
}
return "Manual lock and inactivity expiry immediately revoke the shared Rust authentication lease."
@@ -794,6 +799,13 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
var content = cell.defaultContentConfiguration()
if indexPath.section == 0 {
content.image = UIImage(systemName: "person.crop.circle")
content.text = gitIdentity?.name ?? "Git Commit Identity"
content.secondaryText = gitIdentity?.email ?? "Set the commit author name and email"
cell.accessoryType = .disclosureIndicator
cell.isUserInteractionEnabled = authentication != nil
cell.contentView.alpha = authentication == nil ? 0.45 : 1
} else if indexPath.section == 1 {
let importing = indexPath.row == 0
content.image = UIImage(systemName: importing ? "qrcode.viewfinder" : "qrcode")
content.text = importing ? "Import GPG Key" : "Export GPG Key"
@@ -803,7 +815,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
cell.accessoryType = .disclosureIndicator
cell.isUserInteractionEnabled = keyTransfer != nil
cell.contentView.alpha = keyTransfer == nil ? 0.45 : 1
} else if indexPath.section == 1 {
} else if indexPath.section == 2 {
content.image = UIImage(systemName: "faceid")
content.text = "Biometric Unlock"
content.secondaryText = state?.biometricUnlockEnabled == true
@@ -841,6 +853,10 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if indexPath.section == 0 {
editGitIdentity()
return
}
if indexPath.section == 1 {
guard let keyTransfer else { return }
if indexPath.row == 0 {
requestKeyScanner(keyTransfer)
@@ -852,7 +868,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
}
return
}
guard indexPath.section == 2, indexPath.row == 1, let authentication else { return }
guard indexPath.section == 3, indexPath.row == 1, let authentication else { return }
do {
try authentication.manualLock()
refreshState()
@@ -970,6 +986,81 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
present(alert, animated: true)
}
private func editGitIdentity() {
guard let authentication else {
presentAuthenticationFailure(.unavailable)
return
}
let alert = UIAlertController(
title: "Git Commit Identity",
message: "Enter the author used for commits created on this iPhone.",
preferredStyle: .alert
)
alert.addTextField { [gitIdentity] field in
field.placeholder = "Name"
field.text = gitIdentity?.name
field.textContentType = .name
field.autocapitalizationType = .words
field.clearButtonMode = .whileEditing
}
alert.addTextField { [gitIdentity] field in
field.placeholder = "Email"
field.text = gitIdentity?.email
field.textContentType = .emailAddress
field.keyboardType = .emailAddress
field.autocapitalizationType = .none
field.autocorrectionType = .no
field.clearButtonMode = .whileEditing
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Save", style: .default) { [weak self, weak alert] _ in
guard
let self,
let fields = alert?.textFields,
fields.count == 2
else { return }
saveGitIdentity(
authentication,
name: fields[0].text ?? "",
email: fields[1].text ?? ""
)
})
present(alert, animated: true)
}
private func saveGitIdentity(
_ authentication: MobileAuthentication,
name: String,
email: String
) {
preferenceTask?.cancel()
preferenceTask = Task { [weak self] in
let result = await Task.detached(priority: .userInitiated) {
do {
return Result<MobileGitIdentity, AuthenticationFailure>.success(
try authentication.setGitIdentity(name: name, email: email)
)
} catch let error as MobileAuthenticationFfiError {
return .failure(AuthenticationFailure(error))
} catch {
return .failure(.unexpected)
}
}.value
guard !Task.isCancelled, let self else { return }
switch result {
case let .success(identity):
gitIdentity = identity
tableView.reloadSections(IndexSet(integer: 0), with: .automatic)
UIAccessibility.post(
notification: .announcement,
argument: "Git commit identity updated"
)
case let .failure(failure):
presentAuthenticationFailure(failure)
}
}
}
@objc private func authenticationDidChange() {
refreshState()
}
@@ -1014,6 +1105,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
private func refreshState() {
state = try? authentication?.state()
gitIdentity = try? authentication?.gitIdentity()
tableView.reloadData()
}
}