Skip to content

Commit

Permalink
Redesign thread credentials sharing (#2478)
Browse files Browse the repository at this point in the history
  • Loading branch information
bgoncal authored Dec 13, 2023
1 parent e075dc5 commit 42d68af
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 138 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 @@ -335,7 +335,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 @@ -544,12 +543,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()
}
}
}
})
.alert(alertTitle, isPresented: $viewModel.showAlert) {
errorAlertActions
} message: {
if case let .error(_, message) = viewModel.alertType {
Text(message)
}
} else {
progressView
}
}
.navigationViewStyle(.stack)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black.opacity(0.5))
.alert(alertTitle, isPresented: $viewModel.showAlert) {
alertActions
} message: {
Text(alertMessage)
}
.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)
}

private var alertTitle: String {
if case let .error(title, _) = viewModel.alertType {
switch viewModel.alertType {
case let .empty(title, _):
return title
} else if case let .success(title) = viewModel.alertType {
case let .error(title, _):
return title
} else {
case .none:
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 ""
}
}

private var doneButton: some View {
Button {
/* no-op */
dismiss()
} 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()
}
.listStyle(.plain)
.background(Color(uiColor: .secondarySystemBackground))
} label: {
Text(L10n.retryLabel)
}
}

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()
}
}

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)
}
}
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
))
} 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))
}
}

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

shareCredentialWithHomeAssistant(credential: first) { [weak self] success in
if success {
self?.credentialsToImport.removeFirst()
self?.processImport()
}
}
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)
case let .rejected(error):
self
.showAlert(type: .error(
title: L10n.errorLabel,
message: "Error message: \(error.localizedDescription)"
message: error.localizedDescription
))
completion(false)
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions Sources/App/WebView/WebViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1111,11 +1111,14 @@ extension WebViewController: WKScriptMessageHandler {

private func threadCredentialsRequested() {
if #available(iOS 16.4, *) {
let threadDebugView = UIHostingController(rootView: ThreadCredentialsSharingView(viewModel: .init(
let threadManagementView = UIHostingController(rootView: ThreadCredentialsSharingView(viewModel: .init(
server: server,
threadClient: ThreadClientService()
)))
present(threadDebugView, animated: true)
threadManagementView.view.backgroundColor = .clear
threadManagementView.modalPresentationStyle = .overFullScreen
threadManagementView.modalTransitionStyle = .crossDissolve
present(threadManagementView, animated: true)
}
}
}
Expand Down
Loading

0 comments on commit 42d68af

Please sign in to comment.