Skip to content

Commit

Permalink
Redesign thread credentials sharing
Browse files Browse the repository at this point in the history
  • Loading branch information
bgoncal committed Dec 12, 2023
1 parent a5d07b9 commit ceebbf8
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 136 deletions.
4 changes: 4 additions & 0 deletions HomeAssistant.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@
424A7F462B188946008C8DF3 /* WidgetBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F452B188946008C8DF3 /* WidgetBackground.swift */; };
424A7F482B188BF3008C8DF3 /* WidgetContentMargin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F472B188BF3008C8DF3 /* WidgetContentMargin.swift */; };
426740A92B17391000C1DD73 /* Data+Hexadecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426740A72B17390A00C1DD73 /* Data+Hexadecimal.swift */; };
429C72202B28D0EC00BCD558 /* Haptics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429C721F2B28D0EC00BCD558 /* Haptics.swift */; };
42CA28BB2B1028330093B31A /* SimulatorThreadClientService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42CA28BA2B1028330093B31A /* SimulatorThreadClientService.swift */; };
42CFCD6A2B1F958A00CCEF4A /* DropSupportMessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42CFCD692B1F958A00CCEF4A /* DropSupportMessageViewController.swift */; };
42DD84132B14ACAB00936F16 /* Color+ColorAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42DD84122B14ACAB00936F16 /* Color+ColorAsset.swift */; };
Expand Down Expand Up @@ -1594,6 +1595,7 @@
424A7F472B188BF3008C8DF3 /* WidgetContentMargin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetContentMargin.swift; sourceTree = "<group>"; };
426740A72B17390A00C1DD73 /* Data+Hexadecimal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Hexadecimal.swift"; sourceTree = "<group>"; };
42805A132B0226050095414C /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Base; path = Base.lproj/AppIntentVocabulary.plist; sourceTree = "<group>"; };
429C721F2B28D0EC00BCD558 /* Haptics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Haptics.swift; sourceTree = "<group>"; };
42CA28A62B1012DE0093B31A /* ThreadCredentialsSharingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadCredentialsSharingView.swift; sourceTree = "<group>"; };
42CA28AD2B101D4D0093B31A /* HACornerRadius.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HACornerRadius.swift; sourceTree = "<group>"; };
42CA28AF2B101D6B0093B31A /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3035,6 +3037,7 @@
children = (
42CA28AD2B101D4D0093B31A /* HACornerRadius.swift */,
42DD84122B14ACAB00936F16 /* Color+ColorAsset.swift */,
429C721F2B28D0EC00BCD558 /* Haptics.swift */,
);
path = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -5988,6 +5991,7 @@
D014EEA92128E192008EA6F5 /* ConnectionInfo.swift in Sources */,
11169BC8262BE460005EF90A /* UNNotificationContent+Additions.swift in Sources */,
11AF4D14249C7E09006C74C0 /* ActivitySensor.swift in Sources */,
429C72202B28D0EC00BCD558 /* Haptics.swift in Sources */,
11B38EE9275C54A200205C7B /* GetCameraImageIntentHandler.swift in Sources */,
426740A92B17391000C1DD73 /* Data+Hexadecimal.swift in Sources */,
D0EEF306214DD3CF00D1D360 /* ObjectMapperTransformers.swift in Sources */,
Expand Down
8 changes: 2 additions & 6 deletions Sources/App/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,6 @@ Home Assistant is free and open source home automation software with a focus on
"settings.developer.header" = "Developer";
"settings.developer.map_notification.notification.body" = "Expand this to show the map content extension";
"settings.developer.map_notification.title" = "Show map notification content extension";
"settings.developer.mock_thread_credentials_sharing.title" = "Simulator Thread Credentials Sharing";
"settings.developer.show_log_files.title" = "Show log files in Finder";
"settings.developer.sync_watch_context.title" = "Sync Watch Context";
"settings.event_log.title" = "Event Log";
Expand Down Expand Up @@ -543,12 +542,9 @@ Home Assistant is free and open source home automation software with a focus on
"share_extension.entered_placeholder" = "'entered' in event";
"share_extension.error.title" = "Couldn't Send";
"success_label" = "Success";
"thread.credentials.border_agent_id_title" = "Border Agent ID";
"thread.credentials.network_key_title" = "Network Key";
"thread.credentials.network_name_title" = "Network Name";
"thread.credentials.no_credential_available" = "You don't have credentials available on your iCloud Keychain.";
"thread.credentials.screen_title" = "Thread Credentials";
"thread.credentials.share_credentials_button_title" = "Share credential with Home Assistant";
"thread.credentials.share_credentials.no_credentials_title" = "You don't have credentials to share";
"thread.credentials.share_credentials.no_credentials_message" = "Make sure your are logged in with your iCloud account which is owner of a Home in Apple Home.";
"token_error.connection_failed" = "Connection failed.";
"token_error.expired" = "Token is expired.";
"token_error.token_unavailable" = "Token is unavailable.";
Expand Down
17 changes: 0 additions & 17 deletions Sources/App/Settings/DebugSettingsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -382,23 +382,6 @@ class DebugSettingsViewController: HAFormViewController {
alert.popoverPresentationController?.sourceView = cell.formViewController()?.view
}

if #available(iOS 16.4, *) {
section <<< ButtonRow {
$0.title = L10n.Settings.Developer.MockThreadCredentialsSharing.title
}.onCellSelection { [weak self] _, _ in
guard let server = Current.servers.all.first else { return }
let viewController = UIHostingController(
rootView: ThreadCredentialsSharingView(
viewModel: .init(
server: server,
threadClient: SimulatorThreadClientService()
)
)
)
self?.present(viewController, animated: true, completion: nil)
}
}

section <<< SwitchRow {
$0.title = L10n.Settings.Developer.AnnoyingBackgroundNotifications.title
$0.value = prefs.bool(forKey: XCGLogger.shouldNotifyUserDefaultsKey)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,125 +11,96 @@ struct ThreadCredentialsSharingView: View {
}

var body: some View {
NavigationView {
Group {
if viewModel.showLoader {
progressView
} else {
credentialsList
}
}
.navigationTitle(L10n.Thread.Credentials.screenTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar(content: {
ToolbarItem(placement: .topBarTrailing) {
Button {
dismiss()
} label: {
Text(L10n.doneLabel)
VStack {
if viewModel.showImportSuccess {
successView
.onAppear {
Haptics.shared.play(.medium)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
dismiss()
}

Check warning on line 21 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift#L14-L21

Added lines #L14 - L21 were not covered by tests
}
}
})
.alert(alertTitle, isPresented: $viewModel.showAlert) {
errorAlertActions
} message: {
if case let .error(_, message) = viewModel.alertType {
Text(message)
}
} else {
progressView

Check warning on line 24 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift#L23-L24

Added lines #L23 - L24 were not covered by tests
}
}
.navigationViewStyle(.stack)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black.opacity(0.5))
.alert(alertTitle, isPresented: $viewModel.showAlert) {
alertActions
} message: {
Text(alertMessage)
}

Check warning on line 33 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift#L27-L33

Added lines #L27 - L33 were not covered by tests
.onAppear {
Task {
await viewModel.retrieveAllCredentials()
}
}
}

private var successView: some View {
Image(systemName: "checkmark.circle.fill")
.resizable()
.frame(width: 65, height: 65)
.aspectRatio(contentMode: .fit)
.foregroundColor(.white)
}

Check warning on line 47 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift#L41-L47

Added lines #L41 - L47 were not covered by tests

private var alertTitle: String {
if case let .error(title, _) = viewModel.alertType {
switch viewModel.alertType {
case let .empty(title, _):

Check warning on line 51 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift#L50-L51

Added lines #L50 - L51 were not covered by tests
return title
} else if case let .success(title) = viewModel.alertType {
case let .error(title, _):

Check warning on line 53 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift#L53

Added line #L53 was not covered by tests
return title
} else {
case .none:

Check warning on line 55 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift#L55

Added line #L55 was not covered by tests
return ""
}
}

@ViewBuilder
private var errorAlertActions: some View {
private var alertMessage: String {
switch viewModel.alertType {
case let .empty(_, message):
return message
case let .error(_, message):
return message
default:
return ""
}
}

Check warning on line 69 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift#L60-L69

Added lines #L60 - L69 were not covered by tests

private var doneButton: some View {

Check warning on line 71 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift#L71

Added line #L71 was not covered by tests
Button {
/* no-op */
dismiss()

Check warning on line 73 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift#L73

Added line #L73 was not covered by tests
} label: {
Text(L10n.doneLabel)
}

if case .error = viewModel.alertType {
Button {
Task {
await viewModel.retrieveAllCredentials()
}
} label: {
Text(L10n.retryLabel)
}
}
}

private var progressView: some View {
ProgressView()
.progressViewStyle(.circular)
.scaleEffect(CGSize(width: 2, height: 2))
}

@ViewBuilder
private var credentialsList: some View {
if viewModel.credentials.isEmpty {
Text(L10n.Thread.Credentials.noCredentialAvailable)
.multilineTextAlignment(.center)
} else {
List(viewModel.credentials, id: \.borderAgentID) { credential in
makeCredentialCard(credential: credential)
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
private var retryButton: some View {
Button {
Task {
await viewModel.retrieveAllCredentials()

Check warning on line 82 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift#L79-L82

Added lines #L79 - L82 were not covered by tests
}
.listStyle(.plain)
.background(Color(uiColor: .secondarySystemBackground))
} label: {
Text(L10n.retryLabel)

Check warning on line 85 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift#L84-L85

Added lines #L84 - L85 were not covered by tests
}
}

private func makeCredentialCard(credential: ThreadCredential) -> some View {
CardView(backgroundColor: Color(uiColor: .systemBackground)) {
makeCardPropertyView(
title: L10n.Thread.Credentials.networkNameTitle,
value: credential.networkName
)
makeCardPropertyView(
title: L10n.Thread.Credentials.borderAgentIdTitle,
value: credential.borderAgentID
)
makeCardPropertyView(
title: L10n.Thread.Credentials.networkKeyTitle,
value: credential.networkKey
)
Button {
viewModel.shareCredentialWithHomeAssistant(credential: credential)
} label: {
Text(L10n.Thread.Credentials.shareCredentialsButtonTitle)
}
.buttonStyle(.textButton)
@ViewBuilder
private var alertActions: some View {
switch viewModel.alertType {
case .error, .empty:
doneButton
retryButton
default:
EmptyView()

Check warning on line 96 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift#L90-L96

Added lines #L90 - L96 were not covered by tests
}
}

private func makeCardPropertyView(title: String, value: String) -> some View {
VStack(alignment: .leading, spacing: .zero) {
Group {
Text(title)
.font(.footnote)
Text(value)
.textSelection(.enabled)
.font(.body.bold())
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var progressView: some View {
ProgressView()
.progressViewStyle(.circular)
.scaleEffect(CGSize(width: 2, height: 2))
.tint(.white)

Check warning on line 104 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingView.swift#L100-L104

Added lines #L100 - L104 were not covered by tests
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ import Shared
@available(iOS 13, *)
final class ThreadCredentialsSharingViewModel: ObservableObject {
enum AlertType {
case success(title: String)
case empty(title: String, message: String)
case error(title: String, message: String)
}

@Published var credentials: [ThreadCredential] = []
@Published var showAlert = false
@Published var showLoader = false
@Published var alertType: AlertType?
@Published var showImportSuccess = false

private let threadClient: THClientProtocol
private let connection: HAConnection
private var credentialsToImport: [String] = []

init(server: Server, threadClient: THClientProtocol) {
self.threadClient = threadClient
Expand All @@ -24,32 +25,56 @@ final class ThreadCredentialsSharingViewModel: ObservableObject {

@MainActor
func retrieveAllCredentials() async {
showLoader = true
do {
credentials = try await threadClient.retrieveAllCredentials()

if credentials.isEmpty {
showAlert(type: .empty(
title: L10n.Thread.Credentials.ShareCredentials.noCredentialsTitle,
message: L10n.Thread.Credentials.ShareCredentials.noCredentialsMessage
))

Check warning on line 35 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingViewModel.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingViewModel.swift#L32-L35

Added lines #L32 - L35 were not covered by tests
} else {
credentialsToImport = credentials.map(\.activeOperationalDataSet)
processImport()
}
} catch {
showAlert(type: .error(title: L10n.errorLabel, message: "Error message: \(error.localizedDescription)"))
showAlert(type: .error(title: L10n.errorLabel, message: error.localizedDescription))

Check warning on line 41 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingViewModel.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingViewModel.swift#L41

Added line #L41 was not covered by tests
}
}

@MainActor
private func processImport() {
guard let first = credentialsToImport.first else {
showImportSuccess = true
return

Check warning on line 49 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingViewModel.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingViewModel.swift#L48-L49

Added lines #L48 - L49 were not covered by tests
}

shareCredentialWithHomeAssistant(credential: first) { [weak self] success in
if success {
self?.credentialsToImport.removeFirst()
self?.processImport()
}

Check warning on line 56 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingViewModel.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingViewModel.swift#L53-L56

Added lines #L53 - L56 were not covered by tests
}
showLoader = false
}

@MainActor
func shareCredentialWithHomeAssistant(credential: ThreadCredential) {
private func shareCredentialWithHomeAssistant(credential: String, completion: @escaping (Bool) -> Void) {
let request = HARequest(type: .webSocket("thread/add_dataset_tlv"), data: [
"tlv": credential.activeOperationalDataSet,
"tlv": credential,
"source": "iOS-app",
])
connection.send(request).promise.pipe { [weak self] result in
guard let self else { return }
switch result {
case .fulfilled:
self.showAlert(type: .success(title: L10n.successLabel))
completion(true)

Check warning on line 70 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingViewModel.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingViewModel.swift#L70

Added line #L70 was not covered by tests
case let .rejected(error):
self
.showAlert(type: .error(
title: L10n.errorLabel,
message: "Error message: \(error.localizedDescription)"
message: error.localizedDescription

Check warning on line 75 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingViewModel.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingViewModel.swift#L75

Added line #L75 was not covered by tests
))
completion(false)

Check warning on line 77 in Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingViewModel.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/ThreadCredentialsSharing/ThreadCredentialsSharingViewModel.swift#L77

Added line #L77 was not covered by tests
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/App/WebView/WebViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,8 @@ extension WebViewController: WKScriptMessageHandler {
server: server,
threadClient: ThreadClientService()
)))
threadDebugView.view.backgroundColor = .clear
threadDebugView.modalPresentationStyle = .overFullScreen

Check warning on line 1119 in Sources/App/WebView/WebViewController.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/WebView/WebViewController.swift#L1118-L1119

Added lines #L1118 - L1119 were not covered by tests
present(threadDebugView, animated: true)
}
}
Expand Down
20 changes: 6 additions & 14 deletions Sources/Shared/Resources/Swiftgen/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1138,10 +1138,6 @@ public enum L10n {
public static var body: String { return L10n.tr("Localizable", "settings.developer.map_notification.notification.body") }
}
}
public enum MockThreadCredentialsSharing {
/// Simulator Thread Credentials Sharing
public static var title: String { return L10n.tr("Localizable", "settings.developer.mock_thread_credentials_sharing.title") }
}
public enum ShowLogFiles {
/// Show log files in Finder
public static var title: String { return L10n.tr("Localizable", "settings.developer.show_log_files.title") }
Expand Down Expand Up @@ -1780,18 +1776,14 @@ public enum L10n {

public enum Thread {
public enum Credentials {
/// Border Agent ID
public static var borderAgentIdTitle: String { return L10n.tr("Localizable", "thread.credentials.border_agent_id_title") }
/// Network Key
public static var networkKeyTitle: String { return L10n.tr("Localizable", "thread.credentials.network_key_title") }
/// Network Name
public static var networkNameTitle: String { return L10n.tr("Localizable", "thread.credentials.network_name_title") }
/// You don't have credentials available on your iCloud Keychain.
public static var noCredentialAvailable: String { return L10n.tr("Localizable", "thread.credentials.no_credential_available") }
/// Thread Credentials
public static var screenTitle: String { return L10n.tr("Localizable", "thread.credentials.screen_title") }
/// Share credential with Home Assistant
public static var shareCredentialsButtonTitle: String { return L10n.tr("Localizable", "thread.credentials.share_credentials_button_title") }
public enum ShareCredentials {
/// Make sure your are logged in with your iCloud account which is owner of a Home in Apple Home.
public static var noCredentialsMessage: String { return L10n.tr("Localizable", "thread.credentials.share_credentials.no_credentials_message") }
/// You don't have credentials to share
public static var noCredentialsTitle: String { return L10n.tr("Localizable", "thread.credentials.share_credentials.no_credentials_title") }
}
}
}

Expand Down
Loading

0 comments on commit ceebbf8

Please sign in to comment.