Prepare Apple App Store distribution (#59)

This commit is contained in:
2026-08-16 17:55:59 +02:00
parent b50e47ab73
commit f894d43eb6
46 changed files with 20694 additions and 166 deletions

View File

@@ -20,6 +20,9 @@ extension Notification.Name {
static let ironStorageKeyMaterialDidChange = Notification.Name(
"de.rfc1437.ironstorage.key-material-did-change"
)
static let ironStorageConfigurationDidChange = Notification.Name(
"de.rfc1437.ironstorage.configuration-did-change"
)
}
@MainActor
@@ -182,6 +185,12 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
name: .ironStorageKeyMaterialDidChange,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyMaterialDidChange),
name: .ironStorageConfigurationDidChange,
object: nil
)
}
deinit {
@@ -636,7 +645,7 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
}
@objc private func setupRequested() {
navigationController?.pushViewController(OnboardingViewController(), animated: true)
navigationController?.pushViewController(SetupChoiceViewController(), animated: true)
}
@objc private func tokenUpdateRequested() {
@@ -703,8 +712,10 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
)
case .empty:
content.image = UIImage(systemName: "checkmark.circle")
content.text = "No Remote Activity"
content.secondaryText = "There are no commits to pull or push."
content.text = homePage.remoteConfigured ? "No Remote Activity" : "Local Store Ready"
content.secondaryText = homePage.remoteConfigured
? "There are no commits to pull or push."
: "Changes are committed to the local Git repository."
cell.selectionStyle = .none
case .notice:
if let notice = homePage.notice {
@@ -734,6 +745,11 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
}
private func freshnessDescription(_ page: MobileHomePage) -> String {
if !page.remoteConfigured {
return page.freshness == .current
? "Current local Git status."
: "Local Git status from this device."
}
let refreshed = page.refreshedAt.map(formattedDate)
switch page.freshness {
case .neverRefreshed:
@@ -867,7 +883,7 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
private func showHomeLoading() {
var configuration = UIContentUnavailableConfiguration.loading()
configuration.text = "Loading Activity"
configuration.secondaryText = "Reading cached remote-branch state from storage."
configuration.secondaryText = "Reading Git history and password-store status."
contentUnavailableConfiguration = configuration
}
@@ -1078,7 +1094,8 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
) -> Int {
switch section {
case 0: 2
case 1, 2, 3: 3
case 1: preferences?.syncConfigured == false ? 1 : 3
case 2, 3: 3
default: 1
}
}
@@ -1089,7 +1106,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
) -> String? {
switch section {
case 0: "Password Store"
case 1: "Application Token"
case 1: preferences?.syncConfigured == false ? "Git Sync" : "Application Token"
case 2: "GPG Key & Recovery"
case 3: "Authentication"
case 4: "Appearance"
@@ -1106,7 +1123,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
case 0:
"Non-secret repository and server identities from the shared configuration."
case 1:
"The application token is stored only in protected system storage and is never displayed."
preferences?.syncConfigured == false
? "The store has local Git history. An HTTPS remote can be configured later for synchronization."
: "The application token is stored only in protected system storage and is never displayed."
case 2:
"Import provides initial or recovery key setup. Private-key transfers require explicit confirmation and passphrase validation."
case 3:
@@ -1138,11 +1157,18 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
content.secondaryText = preferences?.serverIdentity ?? "Server identity unavailable"
cell.selectionStyle = .none
case (1, 0):
content.image = UIImage(systemName: "key.fill")
content.text = preferences?.applicationAccount ?? "No Application Token"
content.secondaryText = preferences?.applicationAccount == nil
? "Git network operations require a replacement token"
: "Token stored in protected system storage"
if preferences?.syncConfigured == false {
content.image = UIImage(systemName: "network.badge.shield.half.filled")
content.text = "Add HTTPS Remote"
content.secondaryText = "Keep local history and enable optional synchronization"
cell.accessoryType = .disclosureIndicator
} else {
content.image = UIImage(systemName: "key.fill")
content.text = preferences?.applicationAccount ?? "No Application Token"
content.secondaryText = preferences?.applicationAccount == nil
? "Git network operations require a replacement token"
: "Token stored in protected system storage"
}
cell.selectionStyle = .none
case (1, 1):
content.image = UIImage(systemName: "arrow.triangle.2.circlepath")
@@ -1228,6 +1254,11 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch (indexPath.section, indexPath.row) {
case (1, 0) where preferences?.syncConfigured == false:
navigationController?.pushViewController(
OnboardingViewController(attachLocalStore: true),
animated: true
)
case (1, 1):
tokenUpdateRequested()
case (1, 2):
@@ -1461,8 +1492,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Remove Token", style: .destructive) {
[weak self] _ in
guard let authentication = self?.authentication else { return }
self?.runPreferenceUpdate(announcement: "Application token removed") {
try self?.authentication?.removeApplicationToken()
try authentication.removeApplicationToken()
}
})
present(alert, animated: true)
@@ -1483,8 +1515,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
alert.addAction(UIAlertAction(title: "Save", style: .default) {
[weak self, weak alert] _ in
let seconds = UInt64(alert?.textFields?.first?.text ?? "") ?? 0
guard let authentication = self?.authentication else { return }
self?.runPreferenceUpdate(announcement: "Inactivity timeout updated") {
try self?.authentication?.setAuthenticationTimeout(seconds: seconds)
try authentication.setAuthenticationTimeout(seconds: seconds)
}
})
present(alert, animated: true)
@@ -1498,8 +1531,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
title: "\(selected ? "" : "")\(appearanceTitle(appearance))",
style: .default
) { [weak self] _ in
guard let authentication = self?.authentication else { return }
self?.runPreferenceUpdate(announcement: "Appearance updated") {
try self?.authentication?.setMobileAppearance(appearance: appearance)
try authentication.setMobileAppearance(appearance: appearance)
}
})
}
@@ -1656,6 +1690,14 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
watchPaired: watchSnapshot.isPaired,
watchAppInstalled: watchSnapshot.isWatchAppInstalled
)
navigationItem.rightBarButtonItem = preferences?.syncConfigured == true
? UIBarButtonItem(
title: "Update Token",
style: .plain,
target: self,
action: #selector(tokenUpdateRequested)
)
: nil
if let appearance = preferences?.appearance {
tabBarController?.overrideUserInterfaceStyle = switch appearance {
case .system: .unspecified
@@ -1673,6 +1715,7 @@ private final class KeyImportScannerViewController: UIViewController,
DataScannerViewControllerDelegate
{
private let importer: MobileKeyTransferImport
private let localSetup: Bool
private let scanner = DataScannerViewController(
recognizedDataTypes: [.barcode(symbologies: [.qr])],
qualityLevel: .balanced,
@@ -1688,11 +1731,20 @@ private final class KeyImportScannerViewController: UIViewController,
init(transfer: MobileKeyTransfer) {
importer = transfer.importer()
localSetup = false
super.init(nibName: nil, bundle: nil)
title = "Import GPG Key"
navigationItem.largeTitleDisplayMode = .never
}
init(localImporter: MobileKeyTransferImport) {
importer = localImporter
localSetup = true
super.init(nibName: nil, bundle: nil)
title = "Import Local GPG Key"
navigationItem.largeTitleDisplayMode = .never
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
@@ -1855,11 +1907,20 @@ private final class KeyImportScannerViewController: UIViewController,
private func confirmImport(_ key: MobileKeyTransferKey) {
let privateKey = key.kind == .private
if localSetup, !privateKey {
completing = false
presentFailure("A local store requires a private GPG key with an encryption subkey.") {
[weak self] in try? self?.scanner.startScanning()
}
return
}
let warning = privateKey
? "Import this private key only if you trust the device that displayed it."
: "Import this public key?"
let alert = UIAlertController(
title: privateKey ? "Import Private GPG Key?" : "Import Public GPG Key?",
title: localSetup
? "Create Local Store with This Key?"
: (privateKey ? "Import Private GPG Key?" : "Import Public GPG Key?"),
message: "\(warning)\n\n\(key.title)\n\(key.detail)",
preferredStyle: .alert
)
@@ -1874,21 +1935,37 @@ private final class KeyImportScannerViewController: UIViewController,
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { [weak self] _ in
self?.navigationController?.popViewController(animated: true)
})
alert.addAction(UIAlertAction(title: "Import", style: .default) { [weak self, weak alert] _ in
self?.finishImport(passphrase: alert?.textFields?.first?.text, makeDefault: false)
})
if privateKey {
alert.addAction(UIAlertAction(title: "Import as Default", style: .default) {
if !localSetup {
alert.addAction(UIAlertAction(title: "Import", style: .default) {
[weak self, weak alert] _ in
self?.finishImport(passphrase: alert?.textFields?.first?.text, makeDefault: true)
self?.finishImport(
passphrase: alert?.textFields?.first?.text,
makeDefault: false
)
})
}
if privateKey {
alert.addAction(UIAlertAction(
title: localSetup ? "Create Local Store" : "Import as Default",
style: .default
) {
[weak self, weak alert] _ in
self?.finishImport(
passphrase: alert?.textFields?.first?.text,
makeDefault: true
)
})
}
present(alert, animated: true)
}
private func finishImport(passphrase: String?, makeDefault: Bool) {
private func finishImport(passphrase: String?, makeDefault: Bool, overwrite: Bool = false) {
do {
let outcome = try importer.import(passphrase: passphrase, makeDefault: makeDefault)
let outcome = try importer.import(
passphrase: passphrase,
makeDefault: makeDefault,
overwrite: overwrite
)
let alert = UIAlertController(
title: outcome.title,
message: outcome.detail,
@@ -1899,6 +1976,16 @@ private final class KeyImportScannerViewController: UIViewController,
NotificationCenter.default.post(name: .ironStorageKeyMaterialDidChange, object: nil)
})
present(alert, animated: true)
} catch let error as MobileKeyTransferFfiError {
if case let .Failed(kind, _) = error, kind == .existingKey, !overwrite {
confirmOverwrite(passphrase: passphrase, makeDefault: makeDefault)
return
}
completing = false
presentFailure(error.localizedDescription) { [weak self] in
guard let self else { return }
try? scanner.startScanning()
}
} catch {
completing = false
presentFailure(error.localizedDescription) { [weak self] in
@@ -1908,6 +1995,27 @@ private final class KeyImportScannerViewController: UIViewController,
}
}
private func confirmOverwrite(passphrase: String?, makeDefault: Bool) {
let alert = UIAlertController(
title: "Replace Existing GPG Key?",
message: "A key with this fingerprint already exists. Replacing key material can change which private key is used to unlock entries.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { [weak self] _ in
self?.completing = false
try? self?.scanner.startScanning()
})
alert.addAction(UIAlertAction(title: "Replace Key", style: .destructive) {
[weak self] _ in
self?.finishImport(
passphrase: passphrase,
makeDefault: makeDefault,
overwrite: true
)
})
present(alert, animated: true)
}
private func presentFailure(_ detail: String, completion: (() -> Void)? = nil) {
guard presentedViewController == nil else { return }
let alert = UIAlertController(
@@ -4202,6 +4310,55 @@ private final class PasswordDirectoryViewController: UITableViewController, Mobi
present(alert, animated: true)
}
private func createFolderRequested() {
let alert = UIAlertController(
title: "New Folder",
message: "Enter a name for the password folder in this folder.",
preferredStyle: .alert
)
alert.addTextField { field in
field.placeholder = "Folder Name"
field.autocapitalizationType = .words
field.clearButtonMode = .whileEditing
field.returnKeyType = .go
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Create", style: .default) { [weak self, weak alert] _ in
guard let name = alert?.textFields?.first?.text, !name.isEmpty else { return }
self?.beginCreateFolder(name: name)
})
present(alert, animated: true)
}
private func beginCreateFolder(name: String) {
guard let authentication else {
presentAuthenticationFailure(.unavailable)
return
}
createTask?.cancel()
let parent = path ?? ""
createTask = Task { [weak self] in
let result = await Task.detached(priority: .userInitiated) {
do {
try authentication.createDirectory(parent: parent, name: name)
return Result<Void, AuthenticationFailure>.success(())
} catch let error as MobileAuthenticationFfiError {
return .failure(AuthenticationFailure(error))
} catch {
return .failure(.unexpected)
}
}.value
guard !Task.isCancelled, let self else { return }
switch result {
case .success:
NotificationCenter.default.post(name: .ironStorageLocalStoreDidChange, object: nil)
reload()
case let .failure(failure):
presentAuthenticationFailure(failure)
}
}
}
private func beginCreate(name: String, passphrase: String?) {
guard let authentication else {
presentAuthenticationFailure(.unavailable)
@@ -4345,12 +4502,21 @@ private final class PasswordDirectoryViewController: UITableViewController, Mobi
case let .success(page):
directoryPage = page
title = page.title
navigationItem.rightBarButtonItem = UIBarButtonItem(
let add = UIBarButtonItem(
barButtonSystemItem: .add,
target: self,
action: #selector(createRequested)
target: nil,
action: nil
)
navigationItem.rightBarButtonItem?.accessibilityLabel = "Create password entry"
add.menu = UIMenu(children: [
UIAction(title: "New Password", image: UIImage(systemName: "key")) {
[weak self] _ in self?.createRequested()
},
UIAction(title: "New Folder", image: UIImage(systemName: "folder.badge.plus")) {
[weak self] _ in self?.createFolderRequested()
},
])
add.accessibilityLabel = "Create password or folder"
navigationItem.rightBarButtonItem = add
contentUnavailableConfiguration = nil
tableView.reloadData()
if page.rows.isEmpty {
@@ -6058,8 +6224,315 @@ private final class TokenUpdateViewController: UITableViewController {
}
}
@MainActor
private final class SetupChoiceViewController: UITableViewController {
init() {
super.init(style: .insetGrouped)
title = "Set Up IronStorage"
navigationItem.largeTitleDisplayMode = .never
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
override func numberOfSections(in tableView: UITableView) -> Int { 2 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
section == 0 ? 2 : 1
}
override func tableView(
_ tableView: UITableView,
titleForHeaderInSection section: Int
) -> String? {
section == 0 ? "Local Store" : "Existing Git Store"
}
override func tableView(
_ tableView: UITableView,
titleForFooterInSection section: Int
) -> String? {
section == 0
? "Creates a local Git repository. Add an HTTPS remote later when you want to sync."
: "Clone an existing password-store repository over HTTPS."
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
var content = cell.defaultContentConfiguration()
switch (indexPath.section, indexPath.row) {
case (0, 0):
content.image = UIImage(systemName: "key.badge.plus")
content.text = "Generate Local GPG Key"
content.secondaryText = "Create a protected key and local Git history"
case (0, 1):
content.image = UIImage(systemName: "qrcode.viewfinder")
content.text = "Import Local GPG Key"
content.secondaryText = "Scan a private IronStorage key transfer"
default:
content.image = UIImage(systemName: "network")
content.text = "Connect Existing Store"
content.secondaryText = "Clone an HTTPS Git repository"
}
content.secondaryTextProperties.numberOfLines = 0
cell.contentConfiguration = content
cell.accessoryType = .disclosureIndicator
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch (indexPath.section, indexPath.row) {
case (0, 0):
navigationController?.pushViewController(LocalSetupViewController(), animated: true)
case (0, 1):
requestLocalKeyScanner()
default:
navigationController?.pushViewController(OnboardingViewController(), animated: true)
}
}
private func requestLocalKeyScanner() {
guard DataScannerViewController.isSupported else {
presentMessage("This device does not support live QR scanning.")
return
}
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
showLocalKeyScanner()
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
Task { @MainActor in
if granted {
self?.showLocalKeyScanner()
} else {
self?.presentMessage("Allow camera access in Settings to import a GPG key.")
}
}
}
default:
presentMessage("Allow camera access in Settings to import a GPG key.")
}
}
private func showLocalKeyScanner() {
guard DataScannerViewController.isAvailable else {
presentMessage("Close other camera apps and try again.")
return
}
navigationController?.pushViewController(
KeyImportScannerViewController(localImporter: mobileLocalKeyTransferImporter()),
animated: true
)
}
private func presentMessage(_ detail: String) {
let alert = UIAlertController(
title: "GPG Key Import Is Unavailable",
message: detail,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
@MainActor
private final class LocalSetupViewController: UITableViewController {
private let keyNameField = UITextField()
private let passphraseField = UITextField()
private let confirmationField = UITextField()
private var task: Task<Void, Never>?
private var operation: MobileLocalOnboardingOperation?
private var isWorking = false
init() {
super.init(style: .insetGrouped)
title = "Create Local Store"
navigationItem.largeTitleDisplayMode = .never
configureFields()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
deinit {
operation?.cancel()
task?.cancel()
}
override func numberOfSections(in tableView: UITableView) -> Int { 2 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
section == 0 ? 3 : 1
}
override func tableView(
_ tableView: UITableView,
titleForHeaderInSection section: Int
) -> String? {
section == 0 ? "Protected GPG Key" : nil
}
override func tableView(
_ tableView: UITableView,
titleForFooterInSection section: Int
) -> String? {
section == 0
? "The passphrase protects the generated private key. Keep it somewhere safe; IronStorage cannot recover it."
: "Creates a pass-compatible password store with local Git versioning and no network connection."
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
if indexPath.section == 1 {
cell.textLabel?.text = isWorking ? "Creating…" : "Create Local Store"
cell.textLabel?.textAlignment = .center
cell.textLabel?.textColor = isWorking ? .secondaryLabel : view.tintColor
cell.isUserInteractionEnabled = !isWorking
return cell
}
let field = [keyNameField, passphraseField, confirmationField][indexPath.row]
field.translatesAutoresizingMaskIntoConstraints = false
field.isEnabled = !isWorking
cell.contentView.addSubview(field)
NSLayoutConstraint.activate([
field.leadingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.leadingAnchor),
field.trailingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.trailingAnchor),
field.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 10),
field.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -10),
])
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard indexPath.section == 1, !isWorking else { return }
create(replaceExistingKey: false)
}
private func configureFields() {
for field in [keyNameField, passphraseField, confirmationField] {
field.borderStyle = .none
field.clearButtonMode = .whileEditing
field.autocorrectionType = .no
field.adjustsFontForContentSizeCategory = true
field.font = .preferredFont(forTextStyle: .body)
}
keyNameField.placeholder = "GPG key name"
keyNameField.text = "IronStorage Local Key"
keyNameField.textContentType = .name
keyNameField.autocapitalizationType = .words
passphraseField.placeholder = "GPG key passphrase"
confirmationField.placeholder = "Confirm passphrase"
for field in [passphraseField, confirmationField] {
field.isSecureTextEntry = true
field.textContentType = .newPassword
field.autocapitalizationType = .none
}
}
private func create(replaceExistingKey: Bool) {
let keyName = keyNameField.text ?? ""
let passphrase = passphraseField.text ?? ""
guard !keyName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
!passphrase.isEmpty,
passphrase == confirmationField.text
else {
presentFailure(
title: "Check GPG Key Details",
detail: "Enter a key name and matching non-empty passphrases."
)
return
}
let operation = mobileLocalOnboardingOperation()
self.operation = operation
isWorking = true
navigationItem.prompt = "Generating a protected GPG key and local Git repository."
tableView.reloadData()
task = Task { [weak self] in
let result = await Task.detached(priority: .userInitiated) {
do {
return Result<MobileOnboardingOutcome, OnboardingFailure>.success(
try operation.generate(
userId: keyName,
passphrase: passphrase,
replaceExistingKey: replaceExistingKey
)
)
} catch let error as MobileOnboardingFfiError {
return .failure(OnboardingFailure(error))
} catch {
return .failure(.unexpected)
}
}.value
self?.finish(result, keyName: keyName, passphrase: passphrase)
}
}
private func finish(
_ result: Result<MobileOnboardingOutcome, OnboardingFailure>,
keyName: String,
passphrase: String
) {
operation = nil
task = nil
isWorking = false
navigationItem.prompt = nil
tableView.reloadData()
switch result {
case let .success(outcome):
passphraseField.text = nil
confirmationField.text = nil
let alert = UIAlertController(
title: outcome.title,
message: outcome.detail,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Done", style: .default) { _ in
NotificationCenter.default.post(name: .ironStorageKeyMaterialDidChange, object: nil)
})
present(alert, animated: true)
case let .failure(failure) where failure.kind == .existingKey:
let alert = UIAlertController(
title: failure.title,
message: failure.detail,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Replace Key", style: .destructive) {
[weak self] _ in
self?.keyNameField.text = keyName
self?.passphraseField.text = passphrase
self?.confirmationField.text = passphrase
self?.create(replaceExistingKey: true)
})
present(alert, animated: true)
case let .failure(failure):
presentFailure(title: failure.title, detail: failure.detail)
}
}
private func presentFailure(title: String, detail: String) {
let alert = UIAlertController(title: title, message: detail, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
@MainActor
private final class OnboardingViewController: UITableViewController {
private let attachLocalStore: Bool
private let serverField = UITextField()
private let accountField = UITextField()
private let repositoryField = UITextField()
@@ -6072,9 +6545,10 @@ private final class OnboardingViewController: UITableViewController {
private var generation = 0
private var isWorking = false
init() {
init(attachLocalStore: Bool = false) {
self.attachLocalStore = attachLocalStore
super.init(style: .insetGrouped)
title = "Connect Store"
title = attachLocalStore ? "Add Git Remote" : "Connect Store"
navigationItem.largeTitleDisplayMode = .never
configureFields()
}
@@ -6096,7 +6570,7 @@ private final class OnboardingViewController: UITableViewController {
}
override func numberOfSections(in tableView: UITableView) -> Int {
branches.isEmpty ? 2 : 3
attachLocalStore ? 2 : (branches.isEmpty ? 2 : 3)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -6113,7 +6587,7 @@ private final class OnboardingViewController: UITableViewController {
) -> String? {
switch section {
case 0: "HTTPS Repository"
case 1: branches.isEmpty ? nil : "Remote"
case 1: attachLocalStore || branches.isEmpty ? nil : "Remote"
default: "Local Clone"
}
}
@@ -6133,7 +6607,10 @@ private final class OnboardingViewController: UITableViewController {
return fieldCell(indexPath.row)
}
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
if branches.isEmpty {
if attachLocalStore {
cell.textLabel?.text = "Connect Local Store"
cell.textLabel?.textColor = view.tintColor
} else if branches.isEmpty {
cell.textLabel?.text = "Discover Branches"
cell.textLabel?.textColor = view.tintColor
cell.accessoryType = .disclosureIndicator
@@ -6155,7 +6632,9 @@ private final class OnboardingViewController: UITableViewController {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard !isWorking else { return }
if branches.isEmpty, indexPath.section == 1 {
if attachLocalStore, indexPath.section == 1 {
connectLocalStore()
} else if branches.isEmpty, indexPath.section == 1 {
discoverBranches()
} else if indexPath.section == 1 {
chooseBranch(from: tableView.cellForRow(at: indexPath))
@@ -6221,6 +6700,17 @@ private final class OnboardingViewController: UITableViewController {
}
}
private func connectLocalStore() {
do {
let operation = try makeOperation()
begin(operation, title: "Connecting Git Remote") { operation in
.completed(try operation.connectLocalStore())
}
} catch {
present(error)
}
}
private func runSetup(useExisting: Bool) {
guard branches.indices.contains(selectedBranch) else { return }
do {
@@ -6290,7 +6780,14 @@ private final class OnboardingViewController: UITableViewController {
tokenField.text = nil
let alert = UIAlertController(title: outcome.title, message: outcome.detail, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Done", style: .default) { [weak self] _ in
self?.navigationController?.popViewController(animated: true)
if self?.attachLocalStore == true {
NotificationCenter.default.post(
name: .ironStorageConfigurationDidChange,
object: nil
)
} else {
self?.navigationController?.popViewController(animated: true)
}
})
present(alert, animated: true)
case let .failure(failure):