mirror of
https://github.com/home-assistant/iOS.git
synced 2026-06-25 07:32:12 -05:00
<!-- Thank you for submitting a Pull Request and helping to improve Home Assistant. Please complete the following sections to help the processing and review of your changes. Please do not delete anything from this template. --> ## Summary <!-- Provide a brief summary of the changes you have made and most importantly what they aim to achieve --> - Fix http/https section not showing on manual URL entry during onboarding; - Fix Safari view controller presentation during onboarding; - Fix ha button style missing highlighted and pressed state missing; - Fix registered list of devices not up to date when user names the device; ## Screenshots <!-- If this is a user-facing change not in the frontend, please include screenshots in light and dark mode. --> ## Link to pull request in Documentation repository <!-- Pull requests that add, change or remove functionality must have a corresponding pull request in the Companion App Documentation repository (https://github.com/home-assistant/companion.home-assistant). Please add the number of this pull request after the "#" --> Documentation: home-assistant/companion.home-assistant# ## Any other notes <!-- If there is any other information of note, like if this Pull Request is part of a bigger change, please include it here. -->
156 lines
6.0 KiB
Swift
156 lines
6.0 KiB
Swift
import HAKit
|
||
import PromiseKit
|
||
import Shared
|
||
import SwiftUI
|
||
|
||
private struct RegisteredDevice {
|
||
var name: String
|
||
var id: String
|
||
|
||
init?(data: HAData) throws {
|
||
self.name = try data.decode("name")
|
||
self.id = try {
|
||
let identifiers: [[String]] = try data.decode("identifiers")
|
||
for identifier in identifiers where identifier.count == 2 && identifier.starts(with: ["mobile_app"]) {
|
||
return identifier[1]
|
||
}
|
||
|
||
throw HADataError.couldntTransform(key: "identifiers")
|
||
}()
|
||
}
|
||
|
||
func matches(name other: String) -> Bool {
|
||
name.lowercased() == other.lowercased()
|
||
}
|
||
}
|
||
|
||
struct OnboardingAuthStepDeviceNaming: OnboardingAuthPostStep {
|
||
init(
|
||
api: HomeAssistantAPI,
|
||
sender: UIViewController
|
||
) {
|
||
self.api = api
|
||
self.sender = sender
|
||
}
|
||
|
||
var api: HomeAssistantAPI
|
||
var sender: UIViewController
|
||
|
||
static var supportedPoints: Set<OnboardingAuthStepPoint> {
|
||
Set([.beforeRegister])
|
||
}
|
||
|
||
var timeout: TimeInterval = 30.0
|
||
|
||
/// Whether the user has already been prompted for a device name.
|
||
static var firstUserDeviceNameInput = true
|
||
|
||
func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
|
||
let devices = fetchDeviceList()
|
||
|
||
let timeout: Promise<[RegisteredDevice]> = after(seconds: timeout).then { () -> Promise<[RegisteredDevice]> in
|
||
switch api.connection.state {
|
||
case let .disconnected(reason: .waitingToReconnect(lastError: .some(error), atLatest: _, retryCount: _)):
|
||
throw error
|
||
default:
|
||
throw OnboardingAuthError(kind: .invalidURL, data: nil)
|
||
}
|
||
}
|
||
|
||
// racing the request, not the whole flow, importantly.
|
||
// otherwise we'd fail out before the user finished typing.
|
||
|
||
return race(timeout, devices).then { [self] registeredDevices -> Promise<Void> in
|
||
guard !registeredDevices.contains(where: { $0.id == Current.settingsStore.integrationDeviceID }) else {
|
||
// if the integration is registered already, we will take over that one, so we don't need to look
|
||
return .value(())
|
||
}
|
||
|
||
// this can be removed once the mobile_app notify service stops being device name specific
|
||
return promptForDeviceName(
|
||
deviceName: Current.device.deviceName(),
|
||
registeredDevices: registeredDevices,
|
||
sender: sender
|
||
)
|
||
}
|
||
}
|
||
|
||
private func promptForDeviceName(
|
||
deviceName: String,
|
||
errorMessage: String? = nil,
|
||
registeredDevices: [RegisteredDevice],
|
||
sender: UIViewController
|
||
) -> Promise<Void> {
|
||
guard registeredDevices.contains(where: { $0.matches(name: deviceName) }) ||
|
||
OnboardingAuthStepDeviceNaming.firstUserDeviceNameInput else {
|
||
// if the device name is not already taken, we can safely use it and don't need to prompt
|
||
return .value(())
|
||
}
|
||
OnboardingAuthStepDeviceNaming.firstUserDeviceNameInput = false
|
||
|
||
return Promise<Void> { seal in
|
||
let view = UIHostingController(rootView: DeviceNameView(errorMessage: errorMessage, saveAction: { name in
|
||
guard name.isEmpty == false else {
|
||
promptForDeviceName(
|
||
deviceName: deviceName,
|
||
errorMessage: L10n.Onboarding.DeviceNameCheck.Error.title(deviceName),
|
||
registeredDevices: registeredDevices,
|
||
sender: sender
|
||
).pipe(to: seal.resolve)
|
||
return
|
||
}
|
||
|
||
// Fetch updated device list to ensure we have current data
|
||
fetchDeviceList().done { updatedDevices in
|
||
if updatedDevices.contains(where: { $0.matches(name: name) }) {
|
||
// Name conflicts with a registered device
|
||
promptForDeviceName(
|
||
deviceName: deviceName,
|
||
errorMessage: L10n.Onboarding.DeviceNameCheck.Error.title(deviceName),
|
||
registeredDevices: updatedDevices,
|
||
sender: sender
|
||
).pipe(to: seal.resolve)
|
||
} else {
|
||
// No conflict, proceed with the name
|
||
api.server.info.setSetting(value: name, for: .overrideDeviceName)
|
||
resetFirstUserDeviceNameInput()
|
||
seal.fulfill(())
|
||
}
|
||
}.catch { _ in
|
||
// If we can't fetch updated list, fall back to showing error with original list
|
||
promptForDeviceName(
|
||
deviceName: deviceName,
|
||
errorMessage: L10n.Onboarding.DeviceNameCheck.Error.title(deviceName),
|
||
registeredDevices: registeredDevices,
|
||
sender: sender
|
||
).pipe(to: seal.resolve)
|
||
}
|
||
}, cancelAction: {
|
||
resetFirstUserDeviceNameInput()
|
||
seal.reject(PMKError.cancelled)
|
||
}))
|
||
|
||
sender.present(view, animated: true, completion: nil)
|
||
}
|
||
}
|
||
|
||
// In case the flow is completed or cancelled, we reset the first user device name input flag.
|
||
private func resetFirstUserDeviceNameInput() {
|
||
OnboardingAuthStepDeviceNaming.firstUserDeviceNameInput = true
|
||
}
|
||
|
||
private func fetchDeviceList() -> Promise<[RegisteredDevice]> {
|
||
firstly { () -> Promise<[HAData]> in
|
||
api.connection.send(.init(type: "config/device_registry/list")).promise.compactMap {
|
||
if case let .array(value) = $0 {
|
||
return value
|
||
} else {
|
||
throw HomeAssistantAPI.APIError.invalidResponse
|
||
}
|
||
}
|
||
}.compactMapValues { value -> RegisteredDevice? in
|
||
try? RegisteredDevice(data: value)
|
||
}
|
||
}
|
||
}
|