From feccf6241517b148680c6c709df98a1daf026025 Mon Sep 17 00:00:00 2001 From: Michal Polanski Date: Wed, 3 Jul 2024 12:58:00 +0200 Subject: [PATCH] Issue #394: Add option to lock app by PIN code --- SUPLA.xcodeproj/project.pbxproj | 624 ++++++++++++++---- SUPLA/AddWizardVC.m | 3 +- SUPLA/AppDelegate.m | 159 ----- SUPLA/AppDelegate.swift | 100 ++- SUPLA/Core/Config/GlobalSettings.swift | 7 + SUPLA/Core/DI/DiContainer.swift | 15 +- SUPLA/Core/Events/ListsEventsManager.swift | 6 + .../Extensions/Completable+Supla.swift} | 14 +- .../Core/Extensions/ObservableType+Ext.swift | 7 + SUPLA/Core/Extensions/String+Ext.swift | 9 +- SUPLA/Core/Extensions/Synchronization.swift | 25 + .../UINavigationController+Ext.swift | 26 + .../UIViewController+SuplaNavBar.swift | 41 ++ .../Infrastructure/DatabaseProxy.swift} | 26 +- SUPLA/Core/Infrastructure/ThreadHandler.swift | 6 +- SUPLA/Core/Navigation/Coordinator.swift | 46 ++ ...AAddWizardVC+NavigationSubcontroller.swift | 23 + .../Core/Navigation/SuplaAppCoordinator.swift | 316 +++++++++ SUPLA/Core/Networking/SuplaAppProvider.swift | 39 ++ SUPLA/Core/Networking/SuplaResultCode.swift | 55 ++ SUPLA/Core/State/SuplaAppEvent.swift | 34 + SUPLA/Core/State/SuplaAppState.swift | 141 ++++ SUPLA/Core/State/SuplaAppStateHolder.swift | 109 +++ SUPLA/Core/SuplaCore.swift | 23 + .../SuplaCore+BaseViewController.swift | 115 ++++ .../SuplaCore+BaseViewModel.swift | 52 ++ .../Button/BackgroundStack.swift | 46 ++ .../Button/BorderedButton.swift | 46 ++ .../SwiftUiComponents/Button/TextButton.swift | 56 ++ .../Core/SwiftUiComponents/FilledButton.swift | 72 ++ .../Core/SwiftUiComponents/PinTextField.swift | 191 ++++++ SUPLA/Core/SwiftUiComponents/Text.swift | 124 ++++ SUPLA/Core/UI/BaseViewController.swift | 29 +- SUPLA/Core/UI/BaseViewModel.swift | 72 +- .../UI/Components/SuplaTabBarController.swift | 32 +- SUPLA/Core/UI/Details/StandardDetailVC.swift | 47 +- .../UI/Dialogs/Base/SACustomDialogVC.swift | 1 - SUPLA/Core/UI/Dialogs/SAAlertDialogVC.swift | 49 +- .../UI/Dialogs/SAAuthorizationDialogVM.swift | 87 --- .../NavigationBarVisibilityController.swift | 30 + .../TableView/BaseTableViewController.swift | 6 +- SUPLA/CoreDataManager.swift | 68 +- SUPLA/CreateAccountVC.m | 2 +- ...AccountCreationNavigationCoordinator.swift | 98 --- .../AccountCreation/AccountCreationVC.swift | 45 +- .../Base.lproj/AccountCreationVC.xib | 20 +- .../AccountCreation}/CheckBox.swift | 0 .../AccountCreation}/RoundedButton.swift | 0 .../AccountRemovalNavigationCoordinator.swift | 64 -- .../AccountRemoval/AccountRemovalVC.swift | 13 +- .../Features/AppSettings/AppSettingsVC.swift | 18 +- .../Features/AppSettings/AppSettingsVM.swift | 33 +- .../AppSettings/Cells/LockScreenCell.swift | 47 ++ .../SAAuthorizationDialogVC.swift | 23 + .../SAAuthorizationDialogVM.swift | 43 ++ .../SACredentialsDialogBaseVC.swift} | 14 +- .../SACredentialsDialogBaseVM.swift | 71 ++ .../Authorization/SALoginDialogVC.swift} | 24 +- .../Authorization/SALoginDialogVM.swift | 46 ++ .../Features/ChannelList/ChannelListVC.swift | 18 +- .../History/BaseHistoryDetailVC.swift | 2 - .../Details/GpmDetail/GpmDetailVC.swift | 8 +- .../History/GpmHistoryDetailVC.swift | 1 - .../LegacyDetail/DetailViewController.swift | 5 +- .../SwitchDetailNavigationCoordinator.swift | 38 -- .../Details/SwitchDetail/SwitchDetailVC.swift | 10 +- .../SwitchGeneral/SwitchGeneralVC.swift | 3 - .../TimerDetail/SwitchTimerDetailVC.swift | 3 - .../History/ThermometerHistoryDetailVC.swift | 4 - .../ThermometerDetailVC.swift | 8 +- .../ThermostatHistoryDetailVC.swift | 4 - .../ScheduleDetail/ScheduleDetailVC.swift | 6 - .../ThermostatDetail/ThermostatDetailVC.swift | 10 +- .../ThermostatGeneralVC.swift | 2 - .../TimerDetail/ThermostatTimerDetailVC.swift | 2 - .../WindowDetail/Base/BaseWindowVC.swift | 3 - .../Details/WindowDetail/WindowDetailVC.swift | 21 +- .../DeviceCatalog/DeviceCatalogVC.swift | 15 +- SUPLA/Features/GroupList/GroupListVC.swift | 10 +- .../LocationOrderingVC.swift | 7 + .../LocationOrderingVM.swift | 0 .../LockScreen/LockScreenFeature.swift | 20 + SUPLA/Features/LockScreen/LockScreenVC.swift | 56 ++ SUPLA/Features/LockScreen/LockScreenVM.swift | 123 ++++ .../Features/LockScreen/LockScreenView.swift | 138 ++++ .../LockScreen/LockScreenViewState.swift} | 22 +- SUPLA/Features/LockScreen/UnlockAction.swift | 46 ++ SUPLA/Features/Main/MainVC.swift | 20 +- .../Menu}/SuplaMenuController.swift | 72 +- .../NotificationsLog/NotificationsLogVC.swift | 7 +- SUPLA/Features/PinSetup/PinSetupFeature.swift | 19 + SUPLA/Features/PinSetup/PinSetupVC.swift | 45 ++ SUPLA/Features/PinSetup/PinSetupVM.swift | 65 ++ SUPLA/Features/PinSetup/PinSetupView.swift | 95 +++ .../Features/PinSetup/PinSetupViewState.swift | 34 + .../Profiles}/AddNewProfileCell.swift | 0 .../Profiles}/EditableProfileItemCell.swift | 4 +- .../Profiles}/ProfileItemCell.swift | 0 .../Profiles}/ProfilesVC.swift | 103 +-- .../Profiles}/ProfilesVM.swift | 71 +- SUPLA/Features/SceneList/SceneListVC.swift | 6 +- SUPLA/Features/Status/StatusFeature.swift | 21 + .../StatusVC.swift} | 45 +- SUPLA/Features/Status/StatusVM.swift | 92 +++ SUPLA/Features/Status/StatusView.swift | 121 ++++ SUPLA/Features/Status/StatusViewState.swift | 53 ++ SUPLA/Features/WebContent/WebContentVC.swift | 3 +- SUPLA/Info.plist | 8 +- SUPLA/Launch Screen.storyboard | 56 +- SUPLA/Model/AppSettings/LockScreenScope.swift | 34 + .../AppSettings/LockScreenSettings.swift | 98 +++ .../OptionalValue.swift} | 39 +- SUPLA/Model/ValuesFormatter.swift | 9 + .../MainNavigationCoordinator.swift | 313 --------- SUPLA/Navigation/NavigationCoordinator.swift | 145 ---- .../PresentationNavigationCoordinator.swift | 84 --- .../ProfilesNavigationCoordinator.swift | 82 --- .../SuplaNavigationController.swift | 72 -- .../ChannelConfigRepository.swift | 9 + ...eralPurposeMeasurementItemRepository.swift | 2 +- SUPLA/Resources/Base.lproj/AddWizardVC.xib | 19 +- .../SAZWaveConfigurationWizardVC.xib | 24 +- SUPLA/Resources/Default.strings | 44 ++ .../Extensions/Color+Supla.swift} | 34 +- SUPLA/Resources/Extensions/Font+Supla.swift | 44 ++ SUPLA/Resources/Extensions/String+Icons.swift | 5 + .../Resources/Extensions/UIColor+Supla.swift | 2 + .../Resources/Extensions/UIImage+Supla.swift | 4 + .../Colors/primary.colorset/Contents.json | 18 - .../Colors/toolbar.colorset/Contents.json | 38 ++ .../icon_fingerprint.imageset/Contents.json | 12 + .../ic_fingerprint.svg | 1 + .../icon_status_error.imageset/Contents.json | 12 + .../icon_status_error.imageset/error.svg | 11 + .../Images/logo_light.imageset/Contents.json | 15 + .../Images/logo_light.imageset/Supla logo.svg | 3 + .../logo_with_name.imageset/Contents.json | 12 + .../logo_with_icon.svg | 15 + SUPLA/Resources/Strings.swift | 50 +- SUPLA/Resources/cs.lproj/Localizable.strings | 26 +- SUPLA/Resources/de.lproj/Localizable.strings | 56 +- SUPLA/Resources/el.lproj/Localizable.strings | 26 +- SUPLA/Resources/es.lproj/Localizable.strings | 26 +- SUPLA/Resources/fr.lproj/Localizable.strings | 26 +- SUPLA/Resources/it.lproj/Localizable.strings | 24 +- SUPLA/Resources/lt.lproj/Localizable.strings | 26 +- SUPLA/Resources/nb.lproj/Localizable.strings | 26 +- SUPLA/Resources/nl.lproj/Localizable.strings | 26 +- SUPLA/Resources/pl.lproj/Localizable.strings | 56 +- .../Resources/pt-PT.lproj/Localizable.strings | 26 +- SUPLA/Resources/ru.lproj/Localizable.strings | 26 +- .../Resources/sk-SK.lproj/Localizable.strings | 26 +- SUPLA/Resources/sl.lproj/Localizable.strings | 26 +- SUPLA/Resources/uk.lproj/Localizable.strings | 26 +- SUPLA/SADialog.m | 4 +- SUPLA/SAMenuItems.m | 2 +- SUPLA/SAZWaveConfigurationWizardVC.m | 3 +- SUPLA/SUPLA-Bridging-Header.h | 2 - SUPLA/StatusVC.h | 39 -- SUPLA/StatusVC.m | 115 ---- SUPLA/StatusVC.xib | 121 ---- SUPLA/SuplaClient.h | 2 + SUPLA/SuplaClient.m | 23 + SUPLA/Transitions/SwipeTransition.swift | 68 -- SUPLA/UI/BaseViewController.h | 5 +- SUPLA/UI/BaseViewController.m | 2 +- SUPLA/UseCase/App/InitializationUseCase.swift | 59 ++ SUPLA/UseCase/Client/AuthorizeUseCase.swift | 8 +- SUPLA/UseCase/Client/DisconnectUseCase.swift | 62 ++ SUPLA/UseCase/Client/LoginUseCase.swift | 126 ++++ .../Client/ReconnectUseCase.swift} | 40 +- SUPLA/UseCase/LegacyWrapper.swift | 11 - SUPLA/UseCase/Lock/CheckPinUseCase.swift | 132 ++++ .../Profile/ActivateProfileUseCase.swift | 29 +- .../Profile/DeleteAllProfileDataUseCase.swift | 4 +- .../Profile/DeleteProfileUseCase.swift | 72 +- .../Profile/SaveOrCreateProfileUseCase.swift | 4 +- SUPLA/main.m | 2 +- SUPLATests/Mocks/GlobalSettingsMock.swift | 8 + .../Infrastructure/DatabaseProxyMock.swift | 27 + .../Infrastructure/DateProviderMock.swift | 47 +- .../Infrastructure/ThreadHandlerMock.swift | 5 + SUPLATests/Mocks/ListsEventsManagerMock.swift | 5 + .../Navigation/SuplaAppCoordinatorMock.swift | 169 +++++ .../ChannelConfigRepositoryMock.swift | 7 + SUPLATests/Mocks/SuplaAppProviderMock.swift | 74 +++ .../Mocks/SuplaAppStateHolderMock.swift | 36 + .../Mocks/SuplaClientProviderMock.swift | 15 + .../Mocks/UseCase/ClientUseCasesMocks.swift | 23 + .../Mocks/UseCase/LockUseCasesMocks.swift | 30 + .../Mocks/UseCase/ProfileUseCasesMocks.swift | 4 +- SUPLATests/Mocks/ValuesFormatterMock.swift | 2 + .../SAAuthorizationDialogVMTests.swift | 44 +- .../AppSettings/AppSettingsVMTests.swift | 220 ++++-- .../SwitchGeneral/SwitchGeneralVMTests.swift | 3 +- .../TimerDetail/TimerDetailVMTests.swift | 2 + .../ScheduleDetailVMTests.swift | 8 +- .../ThermostatGeneralVMTests.swift | 19 +- .../ThermostatTimerDetailVMTests.swift | 4 + .../LockScreen/LockScreenVMTests.swift | 143 ++++ .../Features/PinSetup/PinSetupVMTests.swift | 79 +++ .../Tests/Features/Status/StatusVMTests.swift | 164 +++++ .../UpdateToken/UpdateTokenTaskTests.swift | 32 +- .../AppSettings/LockScreenSettingsTests.swift | 54 ++ .../App/InitializationUseCaseTests.swift | 100 +++ .../Client/AuthorizeUseCaseTests.swift | 6 +- .../UseCase/Lock/CheckPinUseCaseTests.swift | 245 +++++++ .../Profile/ActivateProfileUseCaseTests.swift | 44 +- .../DeleteAllProfileDataUseCaseTests.swift | 99 ++- .../Profile/DeleteProfileUseCaseTests.swift | 46 +- .../SaveOrCreateProfileUseCaseTests.swift | 16 +- .../Tools/Core/SingleTestCase.swift | 37 +- .../Tools/Extensions/Observable+Test.swift | 16 +- SuplaApp.h | 21 +- SuplaApp.m | 174 +---- 215 files changed, 6784 insertions(+), 2940 deletions(-) delete mode 100644 SUPLA/AppDelegate.m rename SUPLA/{Features/NotificationsLog/NotificationsLogNavigationCoordinator.swift => Core/Extensions/Completable+Supla.swift} (76%) create mode 100644 SUPLA/Core/Extensions/Synchronization.swift create mode 100644 SUPLA/Core/Extensions/UINavigationController+Ext.swift create mode 100644 SUPLA/Core/Extensions/UIViewController+SuplaNavBar.swift rename SUPLA/{AppDelegate.h => Core/Infrastructure/DatabaseProxy.swift} (70%) create mode 100644 SUPLA/Core/Navigation/Coordinator.swift create mode 100644 SUPLA/Core/Navigation/SAAddWizardVC+NavigationSubcontroller.swift create mode 100644 SUPLA/Core/Navigation/SuplaAppCoordinator.swift create mode 100644 SUPLA/Core/Networking/SuplaAppProvider.swift create mode 100644 SUPLA/Core/Networking/SuplaResultCode.swift create mode 100644 SUPLA/Core/State/SuplaAppEvent.swift create mode 100644 SUPLA/Core/State/SuplaAppState.swift create mode 100644 SUPLA/Core/State/SuplaAppStateHolder.swift create mode 100644 SUPLA/Core/SuplaCore.swift create mode 100644 SUPLA/Core/SwiftUiComponents/Architecture/SuplaCore+BaseViewController.swift create mode 100644 SUPLA/Core/SwiftUiComponents/Architecture/SuplaCore+BaseViewModel.swift create mode 100644 SUPLA/Core/SwiftUiComponents/Button/BackgroundStack.swift create mode 100644 SUPLA/Core/SwiftUiComponents/Button/BorderedButton.swift create mode 100644 SUPLA/Core/SwiftUiComponents/Button/TextButton.swift create mode 100644 SUPLA/Core/SwiftUiComponents/FilledButton.swift create mode 100644 SUPLA/Core/SwiftUiComponents/PinTextField.swift create mode 100644 SUPLA/Core/SwiftUiComponents/Text.swift delete mode 100644 SUPLA/Core/UI/Dialogs/SAAuthorizationDialogVM.swift create mode 100644 SUPLA/Core/UI/NavigationBarVisibilityController.swift delete mode 100644 SUPLA/Features/AccountCreation/AccountCreationNavigationCoordinator.swift rename SUPLA/{Controls => Features/AccountCreation}/CheckBox.swift (100%) rename SUPLA/{Controls => Features/AccountCreation}/RoundedButton.swift (100%) delete mode 100644 SUPLA/Features/AccountRemoval/AccountRemovalNavigationCoordinator.swift create mode 100644 SUPLA/Features/AppSettings/Cells/LockScreenCell.swift create mode 100644 SUPLA/Features/Authorization/SAAuthorizationDialogVC.swift create mode 100644 SUPLA/Features/Authorization/SAAuthorizationDialogVM.swift rename SUPLA/{Core/UI/Dialogs/SAAuthorizationDialogVC.swift => Features/Authorization/SACredentialsDialogBaseVC.swift} (94%) create mode 100644 SUPLA/Features/Authorization/SACredentialsDialogBaseVM.swift rename SUPLA/{Core/SuplaAppWrapper.swift => Features/Authorization/SALoginDialogVC.swift} (69%) create mode 100644 SUPLA/Features/Authorization/SALoginDialogVM.swift delete mode 100644 SUPLA/Features/Details/SwitchDetail/SwitchDetailNavigationCoordinator.swift rename SUPLA/{Cfg => Features/LocationOrdering}/LocationOrderingVC.swift (96%) rename SUPLA/{Cfg => Features/LocationOrdering}/LocationOrderingVM.swift (100%) create mode 100644 SUPLA/Features/LockScreen/LockScreenFeature.swift create mode 100644 SUPLA/Features/LockScreen/LockScreenVC.swift create mode 100644 SUPLA/Features/LockScreen/LockScreenVM.swift create mode 100644 SUPLA/Features/LockScreen/LockScreenView.swift rename SUPLA/{Core/BaseViewControllerExtensions.swift => Features/LockScreen/LockScreenViewState.swift} (67%) create mode 100644 SUPLA/Features/LockScreen/UnlockAction.swift rename SUPLA/{Navigation => Features/Menu}/SuplaMenuController.swift (69%) create mode 100644 SUPLA/Features/PinSetup/PinSetupFeature.swift create mode 100644 SUPLA/Features/PinSetup/PinSetupVC.swift create mode 100644 SUPLA/Features/PinSetup/PinSetupVM.swift create mode 100644 SUPLA/Features/PinSetup/PinSetupView.swift create mode 100644 SUPLA/Features/PinSetup/PinSetupViewState.swift rename SUPLA/{Cfg => Features/Profiles}/AddNewProfileCell.swift (100%) rename SUPLA/{Cfg => Features/Profiles}/EditableProfileItemCell.swift (96%) rename SUPLA/{Cfg => Features/Profiles}/ProfileItemCell.swift (100%) rename SUPLA/{Cfg => Features/Profiles}/ProfilesVC.swift (79%) rename SUPLA/{Cfg => Features/Profiles}/ProfilesVM.swift (77%) create mode 100644 SUPLA/Features/Status/StatusFeature.swift rename SUPLA/Features/{AppSettings/AppSettingsNavigationCoordinator.swift => Status/StatusVC.swift} (50%) create mode 100644 SUPLA/Features/Status/StatusVM.swift create mode 100644 SUPLA/Features/Status/StatusView.swift create mode 100644 SUPLA/Features/Status/StatusViewState.swift create mode 100644 SUPLA/Model/AppSettings/LockScreenScope.swift create mode 100644 SUPLA/Model/AppSettings/LockScreenSettings.swift rename SUPLA/{Features/Details/ThermostatDetail/ThermostatDetailNavigationCoordinator.swift => Model/OptionalValue.swift} (57%) delete mode 100644 SUPLA/Navigation/MainNavigationCoordinator.swift delete mode 100644 SUPLA/Navigation/NavigationCoordinator.swift delete mode 100644 SUPLA/Navigation/PresentationNavigationCoordinator.swift delete mode 100644 SUPLA/Navigation/ProfilesNavigationCoordinator.swift delete mode 100644 SUPLA/Navigation/SuplaNavigationController.swift rename SUPLA/{Features/Details/LegacyDetail/LegacyDetailNavigationCoordinator.swift => Resources/Extensions/Color+Supla.swift} (56%) create mode 100644 SUPLA/Resources/Extensions/Font+Supla.swift create mode 100644 SUPLA/Resources/Resources.xcassets/Colors/toolbar.colorset/Contents.json create mode 100644 SUPLA/Resources/Resources.xcassets/Images/Icons/icon_fingerprint.imageset/Contents.json create mode 100644 SUPLA/Resources/Resources.xcassets/Images/Icons/icon_fingerprint.imageset/ic_fingerprint.svg create mode 100644 SUPLA/Resources/Resources.xcassets/Images/Icons/icon_status_error.imageset/Contents.json create mode 100644 SUPLA/Resources/Resources.xcassets/Images/Icons/icon_status_error.imageset/error.svg create mode 100644 SUPLA/Resources/Resources.xcassets/Images/logo_light.imageset/Contents.json create mode 100644 SUPLA/Resources/Resources.xcassets/Images/logo_light.imageset/Supla logo.svg create mode 100644 SUPLA/Resources/Resources.xcassets/Images/logo_with_name.imageset/Contents.json create mode 100644 SUPLA/Resources/Resources.xcassets/Images/logo_with_name.imageset/logo_with_icon.svg delete mode 100644 SUPLA/StatusVC.h delete mode 100644 SUPLA/StatusVC.m delete mode 100644 SUPLA/StatusVC.xib delete mode 100644 SUPLA/Transitions/SwipeTransition.swift create mode 100644 SUPLA/UseCase/App/InitializationUseCase.swift create mode 100644 SUPLA/UseCase/Client/DisconnectUseCase.swift create mode 100644 SUPLA/UseCase/Client/LoginUseCase.swift rename SUPLA/{Features/Details/ThermometerDetail/ThermometerDetailNavigatorCoordinator.swift => UseCase/Client/ReconnectUseCase.swift} (55%) create mode 100644 SUPLA/UseCase/Lock/CheckPinUseCase.swift create mode 100644 SUPLATests/Mocks/Infrastructure/DatabaseProxyMock.swift create mode 100644 SUPLATests/Mocks/Navigation/SuplaAppCoordinatorMock.swift create mode 100644 SUPLATests/Mocks/SuplaAppProviderMock.swift create mode 100644 SUPLATests/Mocks/SuplaAppStateHolderMock.swift create mode 100644 SUPLATests/Mocks/UseCase/LockUseCasesMocks.swift create mode 100644 SUPLATests/Tests/Features/LockScreen/LockScreenVMTests.swift create mode 100644 SUPLATests/Tests/Features/PinSetup/PinSetupVMTests.swift create mode 100644 SUPLATests/Tests/Features/Status/StatusVMTests.swift create mode 100644 SUPLATests/Tests/Model/AppSettings/LockScreenSettingsTests.swift create mode 100644 SUPLATests/Tests/UseCase/App/InitializationUseCaseTests.swift create mode 100644 SUPLATests/Tests/UseCase/Lock/CheckPinUseCaseTests.swift rename SUPLA/Features/Details/GpmDetail/GpmDetailNavigatorCoordinator.swift => SUPLATests/Tools/Core/SingleTestCase.swift (57%) rename SUPLA/Features/DeviceCatalog/DeviceCatalogNavigationCoordinator.swift => SUPLATests/Tools/Extensions/Observable+Test.swift (68%) diff --git a/SUPLA.xcodeproj/project.pbxproj b/SUPLA.xcodeproj/project.pbxproj index 8025a5d58..ab50260bd 100644 --- a/SUPLA.xcodeproj/project.pbxproj +++ b/SUPLA.xcodeproj/project.pbxproj @@ -176,7 +176,6 @@ 4006E90A1DC62D0A00C4456D /* DetailView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4006E9091DC62D0A00C4456D /* DetailView.m */; }; 4017E12C1BB9CE2900570AC8 /* ChannelCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4017E12A1BB9CE2900570AC8 /* ChannelCell.m */; }; 401CA1BF1BA067DB00117AF4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 401CA1BE1BA067DB00117AF4 /* main.m */; }; - 401CA1C21BA067DB00117AF4 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 401CA1C11BA067DB00117AF4 /* AppDelegate.m */; }; 401CA1CD1BA067DB00117AF4 /* Resources.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 401CA1CC1BA067DB00117AF4 /* Resources.xcassets */; }; 401CA1DC1BA067DB00117AF4 /* SUPLATests.m in Sources */ = {isa = PBXBuildFile; fileRef = 401CA1DB1BA067DB00117AF4 /* SUPLATests.m */; }; 401CA1F21BA0A28A00117AF4 /* SuplaClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 401CA1F11BA0A28A00117AF4 /* SuplaClient.m */; }; @@ -217,8 +216,6 @@ 407D4AF81BC6C7DE009A5505 /* app_icon57x57.png in Resources */ = {isa = PBXBuildFile; fileRef = 407D4AF51BC6C7DE009A5505 /* app_icon57x57.png */; }; 407D4AF91BC6C7DE009A5505 /* app_icon114x114@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 407D4AF61BC6C7DE009A5505 /* app_icon114x114@2x.png */; }; 407D4AFB1BC6D6AA009A5505 /* btnOK@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 407D4AFA1BC6D6AA009A5505 /* btnOK@3x.png */; }; - 407D4AFF1BC6E491009A5505 /* StatusVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 407D4AFD1BC6E491009A5505 /* StatusVC.m */; }; - 407D4B001BC6E491009A5505 /* StatusVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 407D4AFE1BC6E491009A5505 /* StatusVC.xib */; }; 407ECDC21DAFBB9600F01C7E /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 407ECDC11DAFBB9600F01C7E /* AudioToolbox.framework */; }; 408034C41BC83D1A007666E7 /* MGSwipeButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 408034C11BC83D1A007666E7 /* MGSwipeButton.m */; }; 408034C51BC83D1A007666E7 /* MGSwipeTableCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 408034C31BC83D1A007666E7 /* MGSwipeTableCell.m */; }; @@ -299,7 +296,6 @@ A5074BAC2BC9489D0081B6B1 /* WebContentVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BAB2BC9489D0081B6B1 /* WebContentVC.swift */; }; A5074BAF2BC954CB0081B6B1 /* DeviceCatalogVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BAE2BC954CB0081B6B1 /* DeviceCatalogVM.swift */; }; A5074BB12BC957370081B6B1 /* DeviceCatalogVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BB02BC957370081B6B1 /* DeviceCatalogVC.swift */; }; - A5074BB32BC957E70081B6B1 /* DeviceCatalogNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BB22BC957E70081B6B1 /* DeviceCatalogNavigationCoordinator.swift */; }; A5074BB52BCCFD2B0081B6B1 /* IconCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BB42BCCFD2B0081B6B1 /* IconCell.swift */; }; A5074BB82BCE58CA0081B6B1 /* BaseWindowVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BB72BCE58CA0081B6B1 /* BaseWindowVM.swift */; }; A5074BBA2BCE5C520081B6B1 /* BaseWindowVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BB92BCE5C520081B6B1 /* BaseWindowVC.swift */; }; @@ -379,7 +375,6 @@ A51BE8E82AA705AD00718F2F /* StandardDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BE8E62AA705AD00718F2F /* StandardDetailVC.swift */; }; A51BE8EB2AA7136000718F2F /* ThermostatDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BE8EA2AA7136000718F2F /* ThermostatDetailVC.swift */; }; A51BE8ED2AA713A500718F2F /* ThermostatDetailVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BE8EC2AA713A500718F2F /* ThermostatDetailVM.swift */; }; - A51BE8EF2AA715C300718F2F /* ThermostatDetailNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BE8EE2AA715C300718F2F /* ThermostatDetailNavigationCoordinator.swift */; }; A51BE8F32AA7188500718F2F /* ThermostatGeneralVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BE8F22AA7188500718F2F /* ThermostatGeneralVM.swift */; }; A51BE8F52AA7190500718F2F /* ThermostatGeneralVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BE8F42AA7190500718F2F /* ThermostatGeneralVC.swift */; }; A51BE8F72AA71E3E00718F2F /* ScheduleDetailVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BE8F62AA71E3E00718F2F /* ScheduleDetailVM.swift */; }; @@ -441,7 +436,6 @@ A530EDF72A5404A200F8DAEE /* UIFont+Supla.swift in Sources */ = {isa = PBXBuildFile; fileRef = A530EDF62A5404A200F8DAEE /* UIFont+Supla.swift */; }; A530EDFA2A54063F00F8DAEE /* OpenSans-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A530EDF82A54063F00F8DAEE /* OpenSans-Medium.ttf */; }; A530EDFB2A54063F00F8DAEE /* OpenSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A530EDF92A54063F00F8DAEE /* OpenSans-Light.ttf */; }; - A530EDFD2A54153000F8DAEE /* SwitchDetailNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A530EDFC2A54153000F8DAEE /* SwitchDetailNavigationCoordinator.swift */; }; A530EE002A54417400F8DAEE /* SwitchTimerDetailVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A530EDFF2A54417400F8DAEE /* SwitchTimerDetailVM.swift */; }; A530EE022A5441F300F8DAEE /* SwitchTimerDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A530EE012A5441F300F8DAEE /* SwitchTimerDetailVC.swift */; }; A530EE042A555AFA00F8DAEE /* ReadChannelByRemoteIdUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A530EE032A555AFA00F8DAEE /* ReadChannelByRemoteIdUseCase.swift */; }; @@ -480,7 +474,6 @@ A530EE532A5D437C00F8DAEE /* TimerProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A530EE522A5D437C00F8DAEE /* TimerProgressView.swift */; }; A54149272B63031800B44BD6 /* GpmDetailVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54149262B63031800B44BD6 /* GpmDetailVM.swift */; }; A54149292B63034500B44BD6 /* GpmDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54149282B63034500B44BD6 /* GpmDetailVC.swift */; }; - A541492B2B63038600B44BD6 /* GpmDetailNavigatorCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A541492A2B63038600B44BD6 /* GpmDetailNavigatorCoordinator.swift */; }; A541492E2B63055700B44BD6 /* GpmHistoryDetailVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A541492D2B63055700B44BD6 /* GpmHistoryDetailVM.swift */; }; A54149302B63059200B44BD6 /* GpmHistoryDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A541492F2B63059200B44BD6 /* GpmHistoryDetailVC.swift */; }; A54149332B63B73700B44BD6 /* GeneralPurposeMeasurementItemRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54149322B63B73700B44BD6 /* GeneralPurposeMeasurementItemRepository.swift */; }; @@ -552,7 +545,6 @@ A55501EA2B7FC38500FD3296 /* GeneralPurposeMeterIconNameProducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55501E92B7FC38500FD3296 /* GeneralPurposeMeterIconNameProducer.swift */; }; A55501F32B8382C500FD3296 /* NotificationsLogVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55501F22B8382C500FD3296 /* NotificationsLogVM.swift */; }; A55501F52B83842000FD3296 /* NotificationsLogVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55501F42B83842000FD3296 /* NotificationsLogVC.swift */; }; - A55501F72B8384DF00FD3296 /* NotificationsLogNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55501F62B8384DF00FD3296 /* NotificationsLogNavigationCoordinator.swift */; }; A55501F92B838E7300FD3296 /* NotificationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55501F82B838E7300FD3296 /* NotificationRepository.swift */; }; A55501FC2B83F28800FD3296 /* InsertNotificationUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55501FB2B83F28800FD3296 /* InsertNotificationUseCase.swift */; }; A55A8D702BA831D900C540D4 /* WindowDetailVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55A8D6F2BA831D900C540D4 /* WindowDetailVM.swift */; }; @@ -639,10 +631,8 @@ A573B0B12A602F5F001E19D0 /* ExecuteSimpleActionUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A573B0B02A602F5F001E19D0 /* ExecuteSimpleActionUseCaseTests.swift */; }; A573B0B32A603003001E19D0 /* VibrationServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A573B0B22A603003001E19D0 /* VibrationServiceMock.swift */; }; A573B0B52A6037EA001E19D0 /* StartTimerUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A573B0B42A6037EA001E19D0 /* StartTimerUseCaseTests.swift */; }; - A573B38C29DE9F5600EBAFC4 /* BaseViewControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A573B38B29DE9F5600EBAFC4 /* BaseViewControllerExtensions.swift */; }; A573B39229DEB25900EBAFC4 /* ViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A573B39129DEB25900EBAFC4 /* ViewState.swift */; }; A5756F8229DC102800C32A1B /* AccountRemovalVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5756F8129DC102800C32A1B /* AccountRemovalVC.swift */; }; - A5756F8429DC23A800C32A1B /* AccountRemovalNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5756F8329DC23A800C32A1B /* AccountRemovalNavigationCoordinator.swift */; }; A57638C629E5D4C9003E15A3 /* XCTestCaseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57638C529E5D4C9003E15A3 /* XCTestCaseExtensions.swift */; }; A57638CC29E5EF95003E15A3 /* ProfileManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57638CB29E5EF95003E15A3 /* ProfileManagerMock.swift */; }; A57638CE29E5EFDA003E15A3 /* GlobalSettingsMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57638CD29E5EFDA003E15A3 /* GlobalSettingsMock.swift */; }; @@ -679,9 +669,7 @@ A57777CF29E69688004513E6 /* RuntimeConfigMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57777CE29E69688004513E6 /* RuntimeConfigMock.swift */; }; A57777D129E6A319004513E6 /* CreateAccountVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = A57777D029E6A319004513E6 /* CreateAccountVC.xib */; }; A57785BD29E7FAD0001C631E /* SuplaClientProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57785BC29E7FAD0001C631E /* SuplaClientProvider.swift */; }; - A57785BF29E80406001C631E /* SuplaAppWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57785BE29E80406001C631E /* SuplaAppWrapper.swift */; }; A57785C129E80C16001C631E /* SuplaClientProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57785C029E80C16001C631E /* SuplaClientProviderMock.swift */; }; - A57785C329E80CF7001C631E /* SuplaAppWrapperMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57785C229E80CF7001C631E /* SuplaAppWrapperMock.swift */; }; A57B03C62B29D68B00C22966 /* HomePlusDetailRefreshHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57B03C52B29D68B00C22966 /* HomePlusDetailRefreshHelper.swift */; }; A57C4AAC2AAB20B100D9C695 /* RequestResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57C4AAB2AAB20B100D9C695 /* RequestResult.swift */; }; A57C4AAE2AAB21F700D9C695 /* ChannelConfigEventsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57C4AAD2AAB21F700D9C695 /* ChannelConfigEventsManager.swift */; }; @@ -705,6 +693,10 @@ A58316FE2B6452AB006113F8 /* ThermometerAndHumidityValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58316FD2B6452AB006113F8 /* ThermometerAndHumidityValueProvider.swift */; }; A58317002B645814006113F8 /* ThermometerAndHumidityValueStringProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58316FF2B645814006113F8 /* ThermometerAndHumidityValueStringProvider.swift */; }; A5843DE42C1088C800DA0784 /* ShadingSystemPositionPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5843DE32C1088C800DA0784 /* ShadingSystemPositionPresentation.swift */; }; + A58472012C2D7F3200713D36 /* UIViewController+SuplaNavBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58472002C2D7F3200713D36 /* UIViewController+SuplaNavBar.swift */; }; + A58472042C2EACA600713D36 /* SuplaResultCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58472032C2EACA600713D36 /* SuplaResultCode.swift */; }; + A58472062C2EC7E500713D36 /* DisconnectUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58472052C2EC7E500713D36 /* DisconnectUseCase.swift */; }; + A58472082C2ED60E00713D36 /* SuplaAppProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58472072C2ED60E00713D36 /* SuplaAppProvider.swift */; }; A58A63052C0715E500A9D02D /* VerticalBlindsVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A63042C0715E500A9D02D /* VerticalBlindsVM.swift */; }; A58A63072C07163E00A9D02D /* VerticalBlindWindowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A63062C07163E00A9D02D /* VerticalBlindWindowState.swift */; }; A58A630B2C071ACB00A9D02D /* VerticalBlindsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A630A2C071ACB00A9D02D /* VerticalBlindsVC.swift */; }; @@ -774,6 +766,30 @@ A5A14A422B614242004B1598 /* SuplaChannelGeneralPurposeMeasurementConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A14A412B614242004B1598 /* SuplaChannelGeneralPurposeMeasurementConfig.swift */; }; A5A14A442B6143AC004B1598 /* SuplaChannelGeneralPurposeMeterConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A14A432B6143AC004B1598 /* SuplaChannelGeneralPurposeMeterConfig.swift */; }; A5A15FE32C22FEAE0049AA73 /* AlarmArmamentIconNameProducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A15FE22C22FEAE0049AA73 /* AlarmArmamentIconNameProducer.swift */; }; + A5A15FE62C256F850049AA73 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A15FE52C256F850049AA73 /* AppDelegate.swift */; }; + A5A15FE82C2985780049AA73 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A15FE72C2985780049AA73 /* Coordinator.swift */; }; + A5A15FEA2C2987450049AA73 /* UINavigationController+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A15FE92C2987450049AA73 /* UINavigationController+Ext.swift */; }; + A5A15FED2C298A6A0049AA73 /* StatusVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A15FEC2C298A6A0049AA73 /* StatusVC.swift */; }; + A5A15FEF2C298AAD0049AA73 /* StatusVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A15FEE2C298AAD0049AA73 /* StatusVM.swift */; }; + A5A15FF12C298B700049AA73 /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A15FF02C298B700049AA73 /* StatusView.swift */; }; + A5A15FF42C2994BF0049AA73 /* BorderedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A15FF32C2994BF0049AA73 /* BorderedButton.swift */; }; + A5A15FF72C299C8A0049AA73 /* BackgroundStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A15FF62C299C8A0049AA73 /* BackgroundStack.swift */; }; + A5A15FF92C299D230049AA73 /* Color+Supla.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A15FF82C299D230049AA73 /* Color+Supla.swift */; }; + A5A15FFD2C2A161E0049AA73 /* SuplaCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A15FFC2C2A161E0049AA73 /* SuplaCore.swift */; }; + A5A15FFF2C2AA44B0049AA73 /* StatusFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A15FFE2C2AA44B0049AA73 /* StatusFeature.swift */; }; + A5A160012C2AA4950049AA73 /* StatusViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A160002C2AA4950049AA73 /* StatusViewState.swift */; }; + A5A160032C2AAEDE0049AA73 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A160022C2AAEDE0049AA73 /* Text.swift */; }; + A5A160052C2AAFBD0049AA73 /* Font+Supla.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A160042C2AAFBD0049AA73 /* Font+Supla.swift */; }; + A5A160072C2AB3ED0049AA73 /* SuplaCore+BaseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A160062C2AB3ED0049AA73 /* SuplaCore+BaseViewModel.swift */; }; + A5A160092C2AB42F0049AA73 /* SuplaCore+BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A160082C2AB42F0049AA73 /* SuplaCore+BaseViewController.swift */; }; + A5A1600D2C2AC17F0049AA73 /* SuplaAppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A1600C2C2AC17F0049AA73 /* SuplaAppState.swift */; }; + A5A1600F2C2AC18F0049AA73 /* SuplaAppEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A1600E2C2AC18F0049AA73 /* SuplaAppEvent.swift */; }; + A5A160112C2AD0F70049AA73 /* SuplaAppStateHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A160102C2AD0F70049AA73 /* SuplaAppStateHolder.swift */; }; + A5A160132C2AD4470049AA73 /* Synchronization.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A160122C2AD4470049AA73 /* Synchronization.swift */; }; + A5A160152C2ADE270049AA73 /* SuplaAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A160142C2ADE270049AA73 /* SuplaAppCoordinator.swift */; }; + A5A160182C2AE9F30049AA73 /* InitializationUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A160172C2AE9F30049AA73 /* InitializationUseCase.swift */; }; + A5A1601A2C2BEC6B0049AA73 /* FilledButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A160192C2BEC6B0049AA73 /* FilledButton.swift */; }; + A5A1601D2C2BEE8D0049AA73 /* TextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A1601C2C2BEE8D0049AA73 /* TextButton.swift */; }; A5A1C0A829F2AAFA0083818D /* VibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A1C0A729F2AAFA0083818D /* VibrationService.swift */; }; A5A23C2D2ABD96DB00233542 /* SuplaChannelConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A23C2C2ABD96DB00233542 /* SuplaChannelConfigTests.swift */; }; A5A23C2F2ABDAF8E00233542 /* ValuesFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A23C2E2ABDAF8E00233542 /* ValuesFormatterTests.swift */; }; @@ -844,10 +860,28 @@ A5CE73292B4607AE003F882C /* EspConfigResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CE73282B4607AE003F882C /* EspConfigResult.swift */; }; A5CE732C2B469C02003F882C /* arduino.html in Resources */ = {isa = PBXBuildFile; fileRef = A5CE732B2B469C02003F882C /* arduino.html */; }; A5CE732F2B469C36003F882C /* EspHtmlParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CE732E2B469C36003F882C /* EspHtmlParserTests.swift */; }; + A5D7125F2C37EA8300A8EF52 /* LockScreenFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D7125E2C37EA8300A8EF52 /* LockScreenFeature.swift */; }; + A5D712612C37EAA400A8EF52 /* LockScreenViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D712602C37EAA400A8EF52 /* LockScreenViewState.swift */; }; + A5D712632C37EB4900A8EF52 /* LockScreenVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D712622C37EB4900A8EF52 /* LockScreenVM.swift */; }; + A5D712652C37EB8F00A8EF52 /* LockScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D712642C37EB8F00A8EF52 /* LockScreenView.swift */; }; + A5D712672C37EBE700A8EF52 /* LockScreenVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D712662C37EBE700A8EF52 /* LockScreenVC.swift */; }; + A5D712692C37ECF400A8EF52 /* UnlockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D712682C37ECF400A8EF52 /* UnlockAction.swift */; }; + A5D7126C2C380D7700A8EF52 /* CheckPinUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D7126B2C380D7700A8EF52 /* CheckPinUseCase.swift */; }; + A5D7126E2C3D2F2000A8EF52 /* NavigationBarVisibilityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D7126D2C3D2F2000A8EF52 /* NavigationBarVisibilityController.swift */; }; + A5D712702C3E68D400A8EF52 /* OptionalValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D7126F2C3E68D400A8EF52 /* OptionalValue.swift */; }; + A5D712722C3E6FD700A8EF52 /* LockUseCasesMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D712712C3E6FD700A8EF52 /* LockUseCasesMocks.swift */; }; + A5D712752C3E708900A8EF52 /* CheckPinUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D712742C3E708900A8EF52 /* CheckPinUseCaseTests.swift */; }; + A5D712772C3E769300A8EF52 /* SingleTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D712762C3E769300A8EF52 /* SingleTestCase.swift */; }; + A5D7127A2C3E8F8500A8EF52 /* InitializationUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D712792C3E8F8500A8EF52 /* InitializationUseCaseTests.swift */; }; + A5D7127C2C3E916C00A8EF52 /* DatabaseProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D7127B2C3E916C00A8EF52 /* DatabaseProxy.swift */; }; + A5D7127E2C3E9D6D00A8EF52 /* DatabaseProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D7127D2C3E9D6D00A8EF52 /* DatabaseProxyMock.swift */; }; + A5D712812C3EA2E300A8EF52 /* LockScreenVMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D712802C3EA2E300A8EF52 /* LockScreenVMTests.swift */; }; + A5D712852C3EA3A700A8EF52 /* SuplaAppCoordinatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D712842C3EA3A700A8EF52 /* SuplaAppCoordinatorMock.swift */; }; + A5D712882C3EB24900A8EF52 /* PinSetupVMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D712872C3EB24900A8EF52 /* PinSetupVMTests.swift */; }; + A5D7128B2C3EB58D00A8EF52 /* StatusVMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D7128A2C3EB58D00A8EF52 /* StatusVMTests.swift */; }; A5D837C82AF0F154002A420D /* PullToRefreshView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D837C72AF0F154002A420D /* PullToRefreshView.swift */; }; A5D837CB2AF1034E002A420D /* ThermometerDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D837CA2AF1034E002A420D /* ThermometerDetailVC.swift */; }; A5D837CD2AF1035E002A420D /* ThermometerDetailVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D837CC2AF1035E002A420D /* ThermometerDetailVM.swift */; }; - A5D837CF2AF10374002A420D /* ThermometerDetailNavigatorCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D837CE2AF10374002A420D /* ThermometerDetailNavigatorCoordinator.swift */; }; A5D837D22AF1061C002A420D /* ThermometerHistoryDetailVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D837D12AF1061C002A420D /* ThermometerHistoryDetailVM.swift */; }; A5D837D42AF1062B002A420D /* ThermometerHistoryDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D837D32AF1062B002A420D /* ThermometerHistoryDetailVC.swift */; }; A5D837D92AF1069A002A420D /* BaseHistoryDetailVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D837D82AF1069A002A420D /* BaseHistoryDetailVM.swift */; }; @@ -887,6 +921,15 @@ A5E490612A4012E9006801FE /* UpdateChannelIconRelationsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E490602A4012E9006801FE /* UpdateChannelIconRelationsUseCase.swift */; }; A5E490632A4019E6006801FE /* UpdateGroupIconRelationsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E490622A4019E6006801FE /* UpdateGroupIconRelationsUseCase.swift */; }; A5E490652A401A7D006801FE /* UpdateSceneIconRelationsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E490642A401A7D006801FE /* UpdateSceneIconRelationsUseCase.swift */; }; + A5E9CE242C32BB8700509702 /* LoginUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E9CE232C32BB8700509702 /* LoginUseCase.swift */; }; + A5E9CE272C32CE3500509702 /* SALoginDialogVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E9CE262C32CE3500509702 /* SALoginDialogVC.swift */; }; + A5E9CE292C32CE4700509702 /* SALoginDialogVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E9CE282C32CE4700509702 /* SALoginDialogVM.swift */; }; + A5E9CE2B2C32CE5D00509702 /* SACredentialsDialogBaseVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E9CE2A2C32CE5D00509702 /* SACredentialsDialogBaseVC.swift */; }; + A5E9CE2D2C32CE6D00509702 /* SACredentialsDialogBaseVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E9CE2C2C32CE6D00509702 /* SACredentialsDialogBaseVM.swift */; }; + A5E9CE2F2C33D92900509702 /* SAAddWizardVC+NavigationSubcontroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E9CE2E2C33D92900509702 /* SAAddWizardVC+NavigationSubcontroller.swift */; }; + A5E9CE332C342ADC00509702 /* ReconnectUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E9CE322C342ADC00509702 /* ReconnectUseCase.swift */; }; + A5E9CE372C3430DB00509702 /* Completable+Supla.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E9CE362C3430DB00509702 /* Completable+Supla.swift */; }; + A5E9CE392C35385700509702 /* SuplaAppStateHolderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E9CE382C35385700509702 /* SuplaAppStateHolderMock.swift */; }; A5EC52402C0F22F40022F055 /* RequestChannelConfigUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5EC523F2C0F22F40022F055 /* RequestChannelConfigUseCase.swift */; }; A5EC52422C0F2E280022F055 /* SuplaChannelFacadeBlindConfig+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5EC52412C0F2E280022F055 /* SuplaChannelFacadeBlindConfig+Mock.swift */; }; A5EC52442C0F33990022F055 /* RequestChannelConfigUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5EC52432C0F33990022F055 /* RequestChannelConfigUseCaseTests.swift */; }; @@ -960,7 +1003,6 @@ A5F29BE02A275F7600ED700A /* SwapGroupPositionsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F29BDF2A275F7600ED700A /* SwapGroupPositionsUseCase.swift */; }; A5F29BE22A27628300ED700A /* SwapScenePositionsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F29BE12A27628300ED700A /* SwapScenePositionsUseCase.swift */; }; A5F29BE62A27739000ED700A /* MoveableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F29BE52A27739000ED700A /* MoveableCell.swift */; }; - A5F29BE82A2774BF00ED700A /* LegacyDetailNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F29BE72A2774BF00ED700A /* LegacyDetailNavigationCoordinator.swift */; }; A5F29BEB2A27787600ED700A /* ProvideDetailTypeUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F29BEA2A27787600ED700A /* ProvideDetailTypeUseCase.swift */; }; A5F29BEF2A287C6800ED700A /* SAChannelBase+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F29BEE2A287C6800ED700A /* SAChannelBase+Ext.swift */; }; A5F29BF12A28AE0B00ED700A /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F29BF02A28AE0B00ED700A /* NotificationView.swift */; }; @@ -972,6 +1014,18 @@ A5F29C002A2DC50000ED700A /* CoreDataMigrationStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F29BFF2A2DC50000ED700A /* CoreDataMigrationStep.swift */; }; A5F29C022A2DC56600ED700A /* NSPersistentStoreCoordinator+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F29C012A2DC56600ED700A /* NSPersistentStoreCoordinator+Ext.swift */; }; A5F29C042A2DC5D500ED700A /* NSManagedObjectModel+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F29C032A2DC5D500ED700A /* NSManagedObjectModel+Ext.swift */; }; + A5F5C3E42C3545F90058E255 /* Observable+Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F5C3E32C3545F90058E255 /* Observable+Test.swift */; }; + A5F5C3E62C35501D0058E255 /* SuplaAppProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F5C3E52C35501D0058E255 /* SuplaAppProviderMock.swift */; }; + A5F5C3E82C357DF00058E255 /* LockScreenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F5C3E72C357DF00058E255 /* LockScreenCell.swift */; }; + A5F5C3EA2C3580000058E255 /* LockScreenScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F5C3E92C3580000058E255 /* LockScreenScope.swift */; }; + A5F5C3EC2C3581BD0058E255 /* LockScreenSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F5C3EB2C3581BD0058E255 /* LockScreenSettings.swift */; }; + A5F5C3EF2C358AF90058E255 /* LockScreenSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F5C3EE2C358AF90058E255 /* LockScreenSettingsTests.swift */; }; + A5F5C3F22C3682390058E255 /* PinSetupFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F5C3F12C3682390058E255 /* PinSetupFeature.swift */; }; + A5F5C3F42C3682500058E255 /* PinSetupVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F5C3F32C3682500058E255 /* PinSetupVM.swift */; }; + A5F5C3F62C36827B0058E255 /* PinSetupViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F5C3F52C36827A0058E255 /* PinSetupViewState.swift */; }; + A5F5C3F82C3683270058E255 /* PinSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F5C3F72C3683270058E255 /* PinSetupView.swift */; }; + A5F5C3FA2C36963B0058E255 /* PinTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F5C3F92C36963B0058E255 /* PinTextField.swift */; }; + A5F5C3FC2C3698040058E255 /* PinSetupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F5C3FB2C3698040058E255 /* PinSetupVC.swift */; }; A5F8361D2A2E004C00E5CA71 /* Migration10to11ModelMapping.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = A5F8361C2A2E004C00E5CA71 /* Migration10to11ModelMapping.xcmappingmodel */; }; A5F8361F2A2E008C00E5CA71 /* AuthProfileItemInitialMigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F8361E2A2E008C00E5CA71 /* AuthProfileItemInitialMigrationPolicy.swift */; }; A5FE675C2A65CA1700147D1F /* SuplaCloudClientRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FE675B2A65CA1700147D1F /* SuplaCloudClientRepository.swift */; }; @@ -983,22 +1037,14 @@ A5FE67692A666FF800147D1F /* RequestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FE67682A666FF800147D1F /* RequestHelper.swift */; }; AE1874C2290C581D00437146 /* SceneCaptionEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1874C1290C581D00437146 /* SceneCaptionEditor.swift */; }; AE348E66277F348700F363A3 /* SAMoveTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = AE348E65277F348700F363A3 /* SAMoveTableView.m */; }; - AE3DECE22761DB32005923E4 /* AccountCreationNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE3DECE02761DB32005923E4 /* AccountCreationNavigationCoordinator.swift */; }; - AE3DECE32761DB32005923E4 /* PresentationNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE3DECE12761DB32005923E4 /* PresentationNavigationCoordinator.swift */; }; - AE5332E227648E770050E690 /* SuplaNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5332E127648E770050E690 /* SuplaNavigationController.swift */; }; AE5332E62764A7E30050E690 /* SuplaMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5332E52764A7E30050E690 /* SuplaMenuController.swift */; }; - AE53535427691EFC0077BFFB /* AppSettingsNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE53535327691EFC0077BFFB /* AppSettingsNavigationCoordinator.swift */; }; AE535356276927690077BFFB /* LocationOrderingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE535355276927690077BFFB /* LocationOrderingVC.swift */; }; AE5A60E6270E2A7A00F2B780 /* AccountCreationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5A60E5270E2A7A00F2B780 /* AccountCreationVC.swift */; }; - AE68E73227739D8500E55DB7 /* SwipeTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE68E73127739D8500E55DB7 /* SwipeTransition.swift */; }; AE7452892827E37600A3AFAD /* ProfilesVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE7452882827E37600A3AFAD /* ProfilesVM.swift */; }; AE74528B2827E38900A3AFAD /* ProfilesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE74528A2827E38900A3AFAD /* ProfilesVC.swift */; }; AE74528D2827E5D400A3AFAD /* ProfileChooser.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE74528C2827E5D400A3AFAD /* ProfileChooser.swift */; }; AE74528F2827F09400A3AFAD /* Dimens.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE74528E2827F09400A3AFAD /* Dimens.swift */; }; - AE7452912827F49100A3AFAD /* ProfilesNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE7452902827F49100A3AFAD /* ProfilesNavigationCoordinator.swift */; }; AE7CAAFD275BF87D0024095F /* BaseViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AE7CAAFC275BF87D0024095F /* BaseViewController.m */; }; - AE929A87275FD5D500B75715 /* MainNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE929A86275FD5D500B75715 /* MainNavigationCoordinator.swift */; }; - AE929A8B2761487700B75715 /* NavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE929A8A2761487700B75715 /* NavigationCoordinator.swift */; }; AEAAF2F82752937D0030CBA8 /* SUPLA.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 407004251BB5706500F6187F /* SUPLA.xcdatamodeld */; }; AEB195DE276E5A040091D314 /* FadeTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB195DD276E5A040091D314 /* FadeTransition.swift */; }; AEBCD8FC26E4D247001904F3 /* TemperaturePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEBCD8FB26E4D247001904F3 /* TemperaturePresenter.swift */; }; @@ -1457,8 +1503,6 @@ 401CA1B91BA067DB00117AF4 /* SUPLA.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SUPLA.app; sourceTree = BUILT_PRODUCTS_DIR; }; 401CA1BD1BA067DB00117AF4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 401CA1BE1BA067DB00117AF4 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 401CA1C01BA067DB00117AF4 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 401CA1C11BA067DB00117AF4 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 401CA1CC1BA067DB00117AF4 /* Resources.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Resources.xcassets; sourceTree = ""; }; 401CA1D51BA067DB00117AF4 /* SUPLATests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SUPLATests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 401CA1DA1BA067DB00117AF4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1511,9 +1555,6 @@ 407D4AF51BC6C7DE009A5505 /* app_icon57x57.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = app_icon57x57.png; sourceTree = ""; }; 407D4AF61BC6C7DE009A5505 /* app_icon114x114@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "app_icon114x114@2x.png"; sourceTree = ""; }; 407D4AFA1BC6D6AA009A5505 /* btnOK@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "btnOK@3x.png"; sourceTree = ""; }; - 407D4AFC1BC6E491009A5505 /* StatusVC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StatusVC.h; sourceTree = ""; }; - 407D4AFD1BC6E491009A5505 /* StatusVC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StatusVC.m; sourceTree = ""; }; - 407D4AFE1BC6E491009A5505 /* StatusVC.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = StatusVC.xib; sourceTree = ""; }; 407ECDC11DAFBB9600F01C7E /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; 408034C01BC83D1A007666E7 /* MGSwipeButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MGSwipeButton.h; path = ThirdPartyComponents/MGSwipe/MGSwipeButton.h; sourceTree = ""; }; 408034C11BC83D1A007666E7 /* MGSwipeButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MGSwipeButton.m; path = ThirdPartyComponents/MGSwipe/MGSwipeButton.m; sourceTree = ""; }; @@ -1644,7 +1685,6 @@ A5074BAB2BC9489D0081B6B1 /* WebContentVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebContentVC.swift; sourceTree = ""; }; A5074BAE2BC954CB0081B6B1 /* DeviceCatalogVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCatalogVM.swift; sourceTree = ""; }; A5074BB02BC957370081B6B1 /* DeviceCatalogVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCatalogVC.swift; sourceTree = ""; }; - A5074BB22BC957E70081B6B1 /* DeviceCatalogNavigationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCatalogNavigationCoordinator.swift; sourceTree = ""; }; A5074BB42BCCFD2B0081B6B1 /* IconCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconCell.swift; sourceTree = ""; }; A5074BB72BCE58CA0081B6B1 /* BaseWindowVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseWindowVM.swift; sourceTree = ""; }; A5074BB92BCE5C520081B6B1 /* BaseWindowVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseWindowVC.swift; sourceTree = ""; }; @@ -1724,7 +1764,6 @@ A51BE8E62AA705AD00718F2F /* StandardDetailVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandardDetailVC.swift; sourceTree = ""; }; A51BE8EA2AA7136000718F2F /* ThermostatDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermostatDetailVC.swift; sourceTree = ""; }; A51BE8EC2AA713A500718F2F /* ThermostatDetailVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermostatDetailVM.swift; sourceTree = ""; }; - A51BE8EE2AA715C300718F2F /* ThermostatDetailNavigationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermostatDetailNavigationCoordinator.swift; sourceTree = ""; }; A51BE8F22AA7188500718F2F /* ThermostatGeneralVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermostatGeneralVM.swift; sourceTree = ""; }; A51BE8F42AA7190500718F2F /* ThermostatGeneralVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermostatGeneralVC.swift; sourceTree = ""; }; A51BE8F62AA71E3E00718F2F /* ScheduleDetailVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleDetailVM.swift; sourceTree = ""; }; @@ -1787,7 +1826,6 @@ A530EDF62A5404A200F8DAEE /* UIFont+Supla.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Supla.swift"; sourceTree = ""; }; A530EDF82A54063F00F8DAEE /* OpenSans-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "OpenSans-Medium.ttf"; sourceTree = ""; }; A530EDF92A54063F00F8DAEE /* OpenSans-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "OpenSans-Light.ttf"; sourceTree = ""; }; - A530EDFC2A54153000F8DAEE /* SwitchDetailNavigationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchDetailNavigationCoordinator.swift; sourceTree = ""; }; A530EDFF2A54417400F8DAEE /* SwitchTimerDetailVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchTimerDetailVM.swift; sourceTree = ""; }; A530EE012A5441F300F8DAEE /* SwitchTimerDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchTimerDetailVC.swift; sourceTree = ""; }; A530EE032A555AFA00F8DAEE /* ReadChannelByRemoteIdUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadChannelByRemoteIdUseCase.swift; sourceTree = ""; }; @@ -1826,7 +1864,6 @@ A530EE522A5D437C00F8DAEE /* TimerProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerProgressView.swift; sourceTree = ""; }; A54149262B63031800B44BD6 /* GpmDetailVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GpmDetailVM.swift; sourceTree = ""; }; A54149282B63034500B44BD6 /* GpmDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GpmDetailVC.swift; sourceTree = ""; }; - A541492A2B63038600B44BD6 /* GpmDetailNavigatorCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GpmDetailNavigatorCoordinator.swift; sourceTree = ""; }; A541492D2B63055700B44BD6 /* GpmHistoryDetailVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GpmHistoryDetailVM.swift; sourceTree = ""; }; A541492F2B63059200B44BD6 /* GpmHistoryDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GpmHistoryDetailVC.swift; sourceTree = ""; }; A54149322B63B73700B44BD6 /* GeneralPurposeMeasurementItemRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPurposeMeasurementItemRepository.swift; sourceTree = ""; }; @@ -1899,7 +1936,6 @@ A55501E92B7FC38500FD3296 /* GeneralPurposeMeterIconNameProducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPurposeMeterIconNameProducer.swift; sourceTree = ""; }; A55501F22B8382C500FD3296 /* NotificationsLogVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsLogVM.swift; sourceTree = ""; }; A55501F42B83842000FD3296 /* NotificationsLogVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsLogVC.swift; sourceTree = ""; }; - A55501F62B8384DF00FD3296 /* NotificationsLogNavigationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsLogNavigationCoordinator.swift; sourceTree = ""; }; A55501F82B838E7300FD3296 /* NotificationRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRepository.swift; sourceTree = ""; }; A55501FB2B83F28800FD3296 /* InsertNotificationUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsertNotificationUseCase.swift; sourceTree = ""; }; A55A8D6F2BA831D900C540D4 /* WindowDetailVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDetailVM.swift; sourceTree = ""; }; @@ -1988,10 +2024,8 @@ A573B0B02A602F5F001E19D0 /* ExecuteSimpleActionUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExecuteSimpleActionUseCaseTests.swift; sourceTree = ""; }; A573B0B22A603003001E19D0 /* VibrationServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrationServiceMock.swift; sourceTree = ""; }; A573B0B42A6037EA001E19D0 /* StartTimerUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTimerUseCaseTests.swift; sourceTree = ""; }; - A573B38B29DE9F5600EBAFC4 /* BaseViewControllerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewControllerExtensions.swift; sourceTree = ""; }; A573B39129DEB25900EBAFC4 /* ViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewState.swift; sourceTree = ""; }; A5756F8129DC102800C32A1B /* AccountRemovalVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRemovalVC.swift; sourceTree = ""; }; - A5756F8329DC23A800C32A1B /* AccountRemovalNavigationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRemovalNavigationCoordinator.swift; sourceTree = ""; }; A57638C529E5D4C9003E15A3 /* XCTestCaseExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCaseExtensions.swift; sourceTree = ""; }; A57638CB29E5EF95003E15A3 /* ProfileManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManagerMock.swift; sourceTree = ""; }; A57638CD29E5EFDA003E15A3 /* GlobalSettingsMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSettingsMock.swift; sourceTree = ""; }; @@ -2028,9 +2062,7 @@ A57777CE29E69688004513E6 /* RuntimeConfigMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeConfigMock.swift; sourceTree = ""; }; A57777D029E6A319004513E6 /* CreateAccountVC.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CreateAccountVC.xib; sourceTree = ""; }; A57785BC29E7FAD0001C631E /* SuplaClientProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaClientProvider.swift; sourceTree = ""; }; - A57785BE29E80406001C631E /* SuplaAppWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaAppWrapper.swift; sourceTree = ""; }; A57785C029E80C16001C631E /* SuplaClientProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaClientProviderMock.swift; sourceTree = ""; }; - A57785C229E80CF7001C631E /* SuplaAppWrapperMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaAppWrapperMock.swift; sourceTree = ""; }; A57B03C52B29D68B00C22966 /* HomePlusDetailRefreshHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePlusDetailRefreshHelper.swift; sourceTree = ""; }; A57C4AAB2AAB20B100D9C695 /* RequestResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestResult.swift; sourceTree = ""; }; A57C4AAD2AAB21F700D9C695 /* ChannelConfigEventsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelConfigEventsManager.swift; sourceTree = ""; }; @@ -2055,6 +2087,10 @@ A58316FD2B6452AB006113F8 /* ThermometerAndHumidityValueProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermometerAndHumidityValueProvider.swift; sourceTree = ""; }; A58316FF2B645814006113F8 /* ThermometerAndHumidityValueStringProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermometerAndHumidityValueStringProvider.swift; sourceTree = ""; }; A5843DE32C1088C800DA0784 /* ShadingSystemPositionPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadingSystemPositionPresentation.swift; sourceTree = ""; }; + A58472002C2D7F3200713D36 /* UIViewController+SuplaNavBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+SuplaNavBar.swift"; sourceTree = ""; }; + A58472032C2EACA600713D36 /* SuplaResultCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaResultCode.swift; sourceTree = ""; }; + A58472052C2EC7E500713D36 /* DisconnectUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectUseCase.swift; sourceTree = ""; }; + A58472072C2ED60E00713D36 /* SuplaAppProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaAppProvider.swift; sourceTree = ""; }; A58A63042C0715E500A9D02D /* VerticalBlindsVM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = VerticalBlindsVM.swift; path = SUPLA/Features/Details/WindowDetail/VerticalBlind/VerticalBlindsVM.swift; sourceTree = SOURCE_ROOT; }; A58A63062C07163E00A9D02D /* VerticalBlindWindowState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalBlindWindowState.swift; sourceTree = ""; }; A58A630A2C071ACB00A9D02D /* VerticalBlindsVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalBlindsVC.swift; sourceTree = ""; }; @@ -2126,6 +2162,30 @@ A5A14A412B614242004B1598 /* SuplaChannelGeneralPurposeMeasurementConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaChannelGeneralPurposeMeasurementConfig.swift; sourceTree = ""; }; A5A14A432B6143AC004B1598 /* SuplaChannelGeneralPurposeMeterConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaChannelGeneralPurposeMeterConfig.swift; sourceTree = ""; }; A5A15FE22C22FEAE0049AA73 /* AlarmArmamentIconNameProducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlarmArmamentIconNameProducer.swift; sourceTree = ""; }; + A5A15FE52C256F850049AA73 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + A5A15FE72C2985780049AA73 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; + A5A15FE92C2987450049AA73 /* UINavigationController+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Ext.swift"; sourceTree = ""; }; + A5A15FEC2C298A6A0049AA73 /* StatusVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusVC.swift; sourceTree = ""; }; + A5A15FEE2C298AAD0049AA73 /* StatusVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusVM.swift; sourceTree = ""; }; + A5A15FF02C298B700049AA73 /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; + A5A15FF32C2994BF0049AA73 /* BorderedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedButton.swift; sourceTree = ""; }; + A5A15FF62C299C8A0049AA73 /* BackgroundStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundStack.swift; sourceTree = ""; }; + A5A15FF82C299D230049AA73 /* Color+Supla.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Supla.swift"; sourceTree = ""; }; + A5A15FFC2C2A161E0049AA73 /* SuplaCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaCore.swift; sourceTree = ""; }; + A5A15FFE2C2AA44B0049AA73 /* StatusFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFeature.swift; sourceTree = ""; }; + A5A160002C2AA4950049AA73 /* StatusViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusViewState.swift; sourceTree = ""; }; + A5A160022C2AAEDE0049AA73 /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = ""; }; + A5A160042C2AAFBD0049AA73 /* Font+Supla.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+Supla.swift"; sourceTree = ""; }; + A5A160062C2AB3ED0049AA73 /* SuplaCore+BaseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuplaCore+BaseViewModel.swift"; sourceTree = ""; }; + A5A160082C2AB42F0049AA73 /* SuplaCore+BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuplaCore+BaseViewController.swift"; sourceTree = ""; }; + A5A1600C2C2AC17F0049AA73 /* SuplaAppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaAppState.swift; sourceTree = ""; }; + A5A1600E2C2AC18F0049AA73 /* SuplaAppEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaAppEvent.swift; sourceTree = ""; }; + A5A160102C2AD0F70049AA73 /* SuplaAppStateHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaAppStateHolder.swift; sourceTree = ""; }; + A5A160122C2AD4470049AA73 /* Synchronization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Synchronization.swift; sourceTree = ""; }; + A5A160142C2ADE270049AA73 /* SuplaAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaAppCoordinator.swift; sourceTree = ""; }; + A5A160172C2AE9F30049AA73 /* InitializationUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitializationUseCase.swift; sourceTree = ""; }; + A5A160192C2BEC6B0049AA73 /* FilledButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilledButton.swift; sourceTree = ""; }; + A5A1601C2C2BEE8D0049AA73 /* TextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextButton.swift; sourceTree = ""; }; A5A1C0A729F2AAFA0083818D /* VibrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrationService.swift; sourceTree = ""; }; A5A23C2C2ABD96DB00233542 /* SuplaChannelConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaChannelConfigTests.swift; sourceTree = ""; }; A5A23C2E2ABDAF8E00233542 /* ValuesFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValuesFormatterTests.swift; sourceTree = ""; }; @@ -2196,10 +2256,28 @@ A5CE73282B4607AE003F882C /* EspConfigResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EspConfigResult.swift; sourceTree = ""; }; A5CE732B2B469C02003F882C /* arduino.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = arduino.html; sourceTree = ""; }; A5CE732E2B469C36003F882C /* EspHtmlParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EspHtmlParserTests.swift; sourceTree = ""; }; + A5D7125E2C37EA8300A8EF52 /* LockScreenFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenFeature.swift; sourceTree = ""; }; + A5D712602C37EAA400A8EF52 /* LockScreenViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenViewState.swift; sourceTree = ""; }; + A5D712622C37EB4900A8EF52 /* LockScreenVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenVM.swift; sourceTree = ""; }; + A5D712642C37EB8F00A8EF52 /* LockScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenView.swift; sourceTree = ""; }; + A5D712662C37EBE700A8EF52 /* LockScreenVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenVC.swift; sourceTree = ""; }; + A5D712682C37ECF400A8EF52 /* UnlockAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockAction.swift; sourceTree = ""; }; + A5D7126B2C380D7700A8EF52 /* CheckPinUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckPinUseCase.swift; sourceTree = ""; }; + A5D7126D2C3D2F2000A8EF52 /* NavigationBarVisibilityController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarVisibilityController.swift; sourceTree = ""; }; + A5D7126F2C3E68D400A8EF52 /* OptionalValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalValue.swift; sourceTree = ""; }; + A5D712712C3E6FD700A8EF52 /* LockUseCasesMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockUseCasesMocks.swift; sourceTree = ""; }; + A5D712742C3E708900A8EF52 /* CheckPinUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckPinUseCaseTests.swift; sourceTree = ""; }; + A5D712762C3E769300A8EF52 /* SingleTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleTestCase.swift; sourceTree = ""; }; + A5D712792C3E8F8500A8EF52 /* InitializationUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitializationUseCaseTests.swift; sourceTree = ""; }; + A5D7127B2C3E916C00A8EF52 /* DatabaseProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseProxy.swift; sourceTree = ""; }; + A5D7127D2C3E9D6D00A8EF52 /* DatabaseProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseProxyMock.swift; sourceTree = ""; }; + A5D712802C3EA2E300A8EF52 /* LockScreenVMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenVMTests.swift; sourceTree = ""; }; + A5D712842C3EA3A700A8EF52 /* SuplaAppCoordinatorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaAppCoordinatorMock.swift; sourceTree = ""; }; + A5D712872C3EB24900A8EF52 /* PinSetupVMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinSetupVMTests.swift; sourceTree = ""; }; + A5D7128A2C3EB58D00A8EF52 /* StatusVMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusVMTests.swift; sourceTree = ""; }; A5D837C72AF0F154002A420D /* PullToRefreshView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullToRefreshView.swift; sourceTree = ""; }; A5D837CA2AF1034E002A420D /* ThermometerDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermometerDetailVC.swift; sourceTree = ""; }; A5D837CC2AF1035E002A420D /* ThermometerDetailVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermometerDetailVM.swift; sourceTree = ""; }; - A5D837CE2AF10374002A420D /* ThermometerDetailNavigatorCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermometerDetailNavigatorCoordinator.swift; sourceTree = ""; }; A5D837D12AF1061C002A420D /* ThermometerHistoryDetailVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermometerHistoryDetailVM.swift; sourceTree = ""; }; A5D837D32AF1062B002A420D /* ThermometerHistoryDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermometerHistoryDetailVC.swift; sourceTree = ""; }; A5D837D82AF1069A002A420D /* BaseHistoryDetailVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseHistoryDetailVM.swift; sourceTree = ""; }; @@ -2239,6 +2317,15 @@ A5E490602A4012E9006801FE /* UpdateChannelIconRelationsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateChannelIconRelationsUseCase.swift; sourceTree = ""; }; A5E490622A4019E6006801FE /* UpdateGroupIconRelationsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateGroupIconRelationsUseCase.swift; sourceTree = ""; }; A5E490642A401A7D006801FE /* UpdateSceneIconRelationsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSceneIconRelationsUseCase.swift; sourceTree = ""; }; + A5E9CE232C32BB8700509702 /* LoginUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginUseCase.swift; sourceTree = ""; }; + A5E9CE262C32CE3500509702 /* SALoginDialogVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SALoginDialogVC.swift; sourceTree = ""; }; + A5E9CE282C32CE4700509702 /* SALoginDialogVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SALoginDialogVM.swift; sourceTree = ""; }; + A5E9CE2A2C32CE5D00509702 /* SACredentialsDialogBaseVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SACredentialsDialogBaseVC.swift; sourceTree = ""; }; + A5E9CE2C2C32CE6D00509702 /* SACredentialsDialogBaseVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SACredentialsDialogBaseVM.swift; sourceTree = ""; }; + A5E9CE2E2C33D92900509702 /* SAAddWizardVC+NavigationSubcontroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SAAddWizardVC+NavigationSubcontroller.swift"; sourceTree = ""; }; + A5E9CE322C342ADC00509702 /* ReconnectUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReconnectUseCase.swift; sourceTree = ""; }; + A5E9CE362C3430DB00509702 /* Completable+Supla.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Completable+Supla.swift"; sourceTree = ""; }; + A5E9CE382C35385700509702 /* SuplaAppStateHolderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaAppStateHolderMock.swift; sourceTree = ""; }; A5EC523F2C0F22F40022F055 /* RequestChannelConfigUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChannelConfigUseCase.swift; sourceTree = ""; }; A5EC52412C0F2E280022F055 /* SuplaChannelFacadeBlindConfig+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuplaChannelFacadeBlindConfig+Mock.swift"; sourceTree = ""; }; A5EC52432C0F33990022F055 /* RequestChannelConfigUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChannelConfigUseCaseTests.swift; sourceTree = ""; }; @@ -2312,7 +2399,6 @@ A5F29BDF2A275F7600ED700A /* SwapGroupPositionsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapGroupPositionsUseCase.swift; sourceTree = ""; }; A5F29BE12A27628300ED700A /* SwapScenePositionsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapScenePositionsUseCase.swift; sourceTree = ""; }; A5F29BE52A27739000ED700A /* MoveableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveableCell.swift; sourceTree = ""; }; - A5F29BE72A2774BF00ED700A /* LegacyDetailNavigationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyDetailNavigationCoordinator.swift; sourceTree = ""; }; A5F29BEA2A27787600ED700A /* ProvideDetailTypeUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProvideDetailTypeUseCase.swift; sourceTree = ""; }; A5F29BEE2A287C6800ED700A /* SAChannelBase+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SAChannelBase+Ext.swift"; sourceTree = ""; }; A5F29BF02A28AE0B00ED700A /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = ""; }; @@ -2324,6 +2410,18 @@ A5F29BFF2A2DC50000ED700A /* CoreDataMigrationStep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataMigrationStep.swift; sourceTree = ""; }; A5F29C012A2DC56600ED700A /* NSPersistentStoreCoordinator+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPersistentStoreCoordinator+Ext.swift"; sourceTree = ""; }; A5F29C032A2DC5D500ED700A /* NSManagedObjectModel+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectModel+Ext.swift"; sourceTree = ""; }; + A5F5C3E32C3545F90058E255 /* Observable+Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Observable+Test.swift"; sourceTree = ""; }; + A5F5C3E52C35501D0058E255 /* SuplaAppProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaAppProviderMock.swift; sourceTree = ""; }; + A5F5C3E72C357DF00058E255 /* LockScreenCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenCell.swift; sourceTree = ""; }; + A5F5C3E92C3580000058E255 /* LockScreenScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenScope.swift; sourceTree = ""; }; + A5F5C3EB2C3581BD0058E255 /* LockScreenSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenSettings.swift; sourceTree = ""; }; + A5F5C3EE2C358AF90058E255 /* LockScreenSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenSettingsTests.swift; sourceTree = ""; }; + A5F5C3F12C3682390058E255 /* PinSetupFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinSetupFeature.swift; sourceTree = ""; }; + A5F5C3F32C3682500058E255 /* PinSetupVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinSetupVM.swift; sourceTree = ""; }; + A5F5C3F52C36827A0058E255 /* PinSetupViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinSetupViewState.swift; sourceTree = ""; }; + A5F5C3F72C3683270058E255 /* PinSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinSetupView.swift; sourceTree = ""; }; + A5F5C3F92C36963B0058E255 /* PinTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinTextField.swift; sourceTree = ""; }; + A5F5C3FB2C3698040058E255 /* PinSetupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinSetupVC.swift; sourceTree = ""; }; A5F8361C2A2E004C00E5CA71 /* Migration10to11ModelMapping.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = Migration10to11ModelMapping.xcmappingmodel; sourceTree = ""; }; A5F8361E2A2E008C00E5CA71 /* AuthProfileItemInitialMigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProfileItemInitialMigrationPolicy.swift; sourceTree = ""; }; A5FE675B2A65CA1700147D1F /* SuplaCloudClientRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaCloudClientRepository.swift; sourceTree = ""; }; @@ -2336,23 +2434,15 @@ AE1874C1290C581D00437146 /* SceneCaptionEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneCaptionEditor.swift; sourceTree = ""; }; AE348E64277F348700F363A3 /* SAMoveTableView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SAMoveTableView.h; sourceTree = ""; }; AE348E65277F348700F363A3 /* SAMoveTableView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SAMoveTableView.m; sourceTree = ""; }; - AE3DECE02761DB32005923E4 /* AccountCreationNavigationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountCreationNavigationCoordinator.swift; sourceTree = ""; }; - AE3DECE12761DB32005923E4 /* PresentationNavigationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationNavigationCoordinator.swift; sourceTree = ""; }; - AE5332E127648E770050E690 /* SuplaNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaNavigationController.swift; sourceTree = ""; }; AE5332E52764A7E30050E690 /* SuplaMenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuplaMenuController.swift; sourceTree = ""; }; - AE53535327691EFC0077BFFB /* AppSettingsNavigationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsNavigationCoordinator.swift; sourceTree = ""; }; AE535355276927690077BFFB /* LocationOrderingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationOrderingVC.swift; sourceTree = ""; }; AE5A60E5270E2A7A00F2B780 /* AccountCreationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCreationVC.swift; sourceTree = ""; }; - AE68E73127739D8500E55DB7 /* SwipeTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeTransition.swift; sourceTree = ""; }; AE7452882827E37600A3AFAD /* ProfilesVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilesVM.swift; sourceTree = ""; }; AE74528A2827E38900A3AFAD /* ProfilesVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilesVC.swift; sourceTree = ""; }; AE74528C2827E5D400A3AFAD /* ProfileChooser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileChooser.swift; sourceTree = ""; }; AE74528E2827F09400A3AFAD /* Dimens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dimens.swift; sourceTree = ""; }; - AE7452902827F49100A3AFAD /* ProfilesNavigationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfilesNavigationCoordinator.swift; sourceTree = ""; }; AE7CAAFB275BF87D0024095F /* BaseViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BaseViewController.h; path = UI/BaseViewController.h; sourceTree = ""; }; AE7CAAFC275BF87D0024095F /* BaseViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BaseViewController.m; path = UI/BaseViewController.m; sourceTree = ""; }; - AE929A86275FD5D500B75715 /* MainNavigationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationCoordinator.swift; sourceTree = ""; }; - AE929A8A2761487700B75715 /* NavigationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinator.swift; sourceTree = ""; }; AEA1E96127422AA9005C34CB /* SAThermostatMeasurementItem+CoreDataProperties.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SAThermostatMeasurementItem+CoreDataProperties.h"; sourceTree = ""; }; AEB195DD276E5A040091D314 /* FadeTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FadeTransition.swift; sourceTree = ""; }; AEBCD8FB26E4D247001904F3 /* TemperaturePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperaturePresenter.swift; sourceTree = ""; }; @@ -2488,7 +2578,6 @@ 0180440B24C254CF005E53EB /* SAChannelStatePopup.xib */, 01943EC0249D1C8800DA2B8F /* SASuperuserAuthorizationDialog.xib */, 010714822656EC43009C119F /* SAZWaveWakeupSettingsDialog.xib */, - 407D4AFE1BC6E491009A5505 /* StatusVC.xib */, 406778541BCBF9F4008DD37F /* SectionCell.xib */, 40DE1A3A1BCD5E9B004CF43B /* AboutVC.xib */, 40DB208F1DC9049400FAFAB0 /* RGBWDetail.xib */, @@ -2637,8 +2726,6 @@ 40A6757E1BA1A5B6004A51C4 /* SuplaApp.m */, 01EFD948236CA94400893489 /* NSData+AES.h */, 01EFD949236CA94400893489 /* NSData+AES.m */, - 401CA1C01BA067DB00117AF4 /* AppDelegate.h */, - 401CA1C11BA067DB00117AF4 /* AppDelegate.m */, 401CA1BC1BA067DB00117AF4 /* Supporting Files */, 40AB8BDC1DCB32EB0030F3DE /* SARateApp.h */, 0111E922263F374800302356 /* UIColor+SUPLA.h */, @@ -2658,6 +2745,7 @@ 016ABE15236D8363001BF5FB /* SAKeychain.m */, 01433DF6257F8FE20093C5DB /* SAWifi.h */, 01433DF7257F8FE20093C5DB /* SAWifi.m */, + A5A15FE52C256F850049AA73 /* AppDelegate.swift */, ); path = SUPLA; sourceTree = ""; @@ -2966,8 +3054,6 @@ isa = PBXGroup; children = ( AEB195DC276E59B60091D314 /* Transitions */, - AE929A85275FD53A00B75715 /* Navigation */, - AE5A60E4270E2A4100F2B780 /* Cfg */, AED6395C270CA37A00E5105B /* Controls */, 40DE1A331BCD1B55004CF43B /* AboutVC.h */, 40DE1A341BCD1B55004CF43B /* AboutVC.m */, @@ -3045,8 +3131,6 @@ 010714722656A6FB009C119F /* SAZWaveWakeupSettingsDialog.m */, 406778521BCBF9F4008DD37F /* SectionCell.h */, 406778531BCBF9F4008DD37F /* SectionCell.m */, - 407D4AFC1BC6E491009A5505 /* StatusVC.h */, - 407D4AFD1BC6E491009A5505 /* StatusVC.m */, 012119122368B597004C1993 /* xib */, AE7CAAFB275BF87D0024095F /* BaseViewController.h */, AE7CAAFC275BF87D0024095F /* BaseViewController.m */, @@ -3111,7 +3195,6 @@ children = ( A5074BAE2BC954CB0081B6B1 /* DeviceCatalogVM.swift */, A5074BB02BC957370081B6B1 /* DeviceCatalogVC.swift */, - A5074BB22BC957E70081B6B1 /* DeviceCatalogNavigationCoordinator.swift */, ); path = DeviceCatalog; sourceTree = ""; @@ -3411,7 +3494,6 @@ A51BE8F02AA7183F00718F2F /* ThermostatGeneral */, A51BE8EA2AA7136000718F2F /* ThermostatDetailVC.swift */, A51BE8EC2AA713A500718F2F /* ThermostatDetailVM.swift */, - A51BE8EE2AA715C300718F2F /* ThermostatDetailNavigationCoordinator.swift */, ); path = ThermostatDetail; sourceTree = ""; @@ -3498,6 +3580,8 @@ A56D5ACE2A4C37AD004F45DA /* SingleCallWrapper.m */, A56D5AD02A4C37DA004F45DA /* SingleCallWrapper.h */, A56233F42AB856B3001CB948 /* DelayedCommandSubject.swift */, + A58472032C2EACA600713D36 /* SuplaResultCode.swift */, + A58472072C2ED60E00713D36 /* SuplaAppProvider.swift */, ); name = SuplaClient; sourceTree = ""; @@ -3570,7 +3654,6 @@ A530EDF12A54001F00F8DAEE /* SwitchGeneral */, A530EDEA2A53F60400F8DAEE /* SwitchDetailVM.swift */, A530EDEC2A53F6A800F8DAEE /* SwitchDetailVC.swift */, - A530EDFC2A54153000F8DAEE /* SwitchDetailNavigationCoordinator.swift */, A530EE052A5575D700F8DAEE /* DeviceStateView.swift */, ); path = SwitchDetail; @@ -3610,6 +3693,8 @@ AED63960270CA46C00E5105B /* UIColor+Supla.swift */, A530EDF62A5404A200F8DAEE /* UIFont+Supla.swift */, A530EE0C2A56C65D00F8DAEE /* UIImage+Supla.swift */, + A5A15FF82C299D230049AA73 /* Color+Supla.swift */, + A5A160042C2AAFBD0049AA73 /* Font+Supla.swift */, ); path = Extensions; sourceTree = ""; @@ -3686,6 +3771,9 @@ A55A8D9E2BAC263000C540D4 /* ExecuteRollerShutterActionUseCase.swift */, A55A8DA52BAC4F5500C540D4 /* AuthorizeUseCase.swift */, A5074BE32BD251590081B6B1 /* ExecuteFacadeBlindActionUseCase.swift */, + A58472052C2EC7E500713D36 /* DisconnectUseCase.swift */, + A5E9CE232C32BB8700509702 /* LoginUseCase.swift */, + A5E9CE322C342ADC00509702 /* ReconnectUseCase.swift */, ); path = Client; sourceTree = ""; @@ -3749,7 +3837,6 @@ A541492C2B6303DD00B44BD6 /* History */, A54149262B63031800B44BD6 /* GpmDetailVM.swift */, A54149282B63034500B44BD6 /* GpmDetailVC.swift */, - A541492A2B63038600B44BD6 /* GpmDetailNavigatorCoordinator.swift */, ); path = GpmDetail; sourceTree = ""; @@ -3962,8 +4049,9 @@ A553863029E02C4300B5CF3F /* AccountCreation */ = { isa = PBXGroup; children = ( + AED6395D270CA3A800E5105B /* RoundedButton.swift */, + AECB1BE627109EA3001A9714 /* CheckBox.swift */, 40844D811FB0DDB000432AA0 /* AccountCreationVC.xib */, - AE3DECE02761DB32005923E4 /* AccountCreationNavigationCoordinator.swift */, AE5A60E5270E2A7A00F2B780 /* AccountCreationVC.swift */, AECB1BE4270EB56F001A9714 /* AccountCreationVM.swift */, ); @@ -3975,7 +4063,6 @@ children = ( A55501F22B8382C500FD3296 /* NotificationsLogVM.swift */, A55501F42B83842000FD3296 /* NotificationsLogVC.swift */, - A55501F62B8384DF00FD3296 /* NotificationsLogNavigationCoordinator.swift */, A5E34CE32B848C6100DE511F /* NotificationViewCell.swift */, ); path = NotificationsLog; @@ -4043,6 +4130,7 @@ A55A8D8F2BAB13A200C540D4 /* RollerShutter */, A51BE8FD2AA73A3E00718F2F /* Thermostat */, A5812CC12BBC8E4200FC5998 /* UserIcon.swift */, + A5D7126F2C3E68D400A8EF52 /* OptionalValue.swift */, ); name = SuplaApp; sourceTree = ""; @@ -4082,8 +4170,6 @@ A55A8DAD2BAC643B00C540D4 /* Base */, A55A8DA12BAC2A9800C540D4 /* SAAlertDialogVC.swift */, A55A8DA32BAC2B0E00C540D4 /* SAAlertDialogVM.swift */, - A55A8DA92BAC608C00C540D4 /* SAAuthorizationDialogVC.swift */, - A55A8DAB2BAC60A200C540D4 /* SAAuthorizationDialogVM.swift */, ); path = Dialogs; sourceTree = ""; @@ -4195,6 +4281,7 @@ A54A06622AF50E8A00C03DBC /* RequestHelperMock.swift */, A55A8DC02BB2F46F00C540D4 /* ThreadHandlerMock.swift */, A55A8DC22BB2F4E000C540D4 /* NotificationCenterWrapperMock.swift */, + A5D7127D2C3E9D6D00A8EF52 /* DatabaseProxyMock.swift */, ); path = Infrastructure; sourceTree = ""; @@ -4226,6 +4313,8 @@ A573B39029DEB24400EBAFC4 /* Core */ = { isa = PBXGroup; children = ( + A5A1600B2C2AC16A0049AA73 /* State */, + A5A15FE42C256E320049AA73 /* Navigation */, A5074BA62BC9239C0081B6B1 /* Branding */, A52ACD4229CB92BB0092729F /* Networking */, A5E4904E2A3C7987006801FE /* Infrastructure */, @@ -4236,10 +4325,10 @@ A5F29B702A1E32D100ED700A /* Repository */, A55E353929E68DEE00F88252 /* Config */, A553862929E0201A00B5CF3F /* DI */, + A5A15FF52C299C390049AA73 /* SwiftUiComponents */, A5F14B7729DED97400682FA6 /* UI */, - A573B38B29DE9F5600EBAFC4 /* BaseViewControllerExtensions.swift */, - A57785BE29E80406001C631E /* SuplaAppWrapper.swift */, A5AF25A32B91FBB90026DA27 /* SALog.swift */, + A5A15FFC2C2A161E0049AA73 /* SuplaCore.swift */, ); path = Core; sourceTree = ""; @@ -4247,6 +4336,13 @@ A5756F7F29DC0FE900C32A1B /* Features */ = { isa = PBXGroup; children = ( + A5D7125D2C37EA7200A8EF52 /* LockScreen */, + A5F5C3F02C36822A0058E255 /* PinSetup */, + A5E9CE312C33DC2100509702 /* Menu */, + A5E9CE302C33DB8800509702 /* LocationOrdering */, + A5E9CE252C32CE0B00509702 /* Authorization */, + A58472022C2D9CF400713D36 /* Profiles */, + A5A15FEB2C298A580049AA73 /* Status */, A5074BAD2BC954A10081B6B1 /* DeviceCatalog */, A5074BA52BC922E70081B6B1 /* WebContent */, A55501F12B83828100FD3296 /* NotificationsLog */, @@ -4267,7 +4363,6 @@ isa = PBXGroup; children = ( A5756F8129DC102800C32A1B /* AccountRemovalVC.swift */, - A5756F8329DC23A800C32A1B /* AccountRemovalNavigationCoordinator.swift */, A5F14B7129DED06400682FA6 /* AccountRemovalVM.swift */, ); path = AccountRemoval; @@ -4286,6 +4381,7 @@ children = ( A57638C529E5D4C9003E15A3 /* XCTestCaseExtensions.swift */, A59AB8A72A306B4E00D91F1F /* NSManagedObject+Test.swift */, + A5F5C3E32C3545F90058E255 /* Observable+Test.swift */, ); path = Extensions; sourceTree = ""; @@ -4293,6 +4389,7 @@ A57638CA29E5EF80003E15A3 /* Mocks */ = { isa = PBXGroup; children = ( + A5D712832C3EA39B00A8EF52 /* Navigation */, A54A066A2AF528F200C03DBC /* Events */, A54A06672AF527CD00C03DBC /* Config */, A54A06592AF4E3A300C03DBC /* Native */, @@ -4306,7 +4403,6 @@ A57638CD29E5EFDA003E15A3 /* GlobalSettingsMock.swift */, A57777CE29E69688004513E6 /* RuntimeConfigMock.swift */, A57785C029E80C16001C631E /* SuplaClientProviderMock.swift */, - A57785C229E80CF7001C631E /* SuplaAppWrapperMock.swift */, A59AB8A32A3058A600D91F1F /* ListsEventsManagerMock.swift */, A5E490542A3C7F2A006801FE /* UserNotificationCenterMock.swift */, A50CD3D72A4D9A040012DD9B /* SingleCallMock.swift */, @@ -4315,6 +4411,8 @@ A5ABE5CA2ABCD80300FFA50B /* DelayedThermostatActionSubjectMock.swift */, A5ABE5CC2ABCD89400FFA50B /* LoadingTimoutManagerMock.swift */, A5ABE5CE2ABD696900FFA50B /* DataMocks.swift */, + A5E9CE382C35385700509702 /* SuplaAppStateHolderMock.swift */, + A5F5C3E52C35501D0058E255 /* SuplaAppProviderMock.swift */, ); path = Mocks; sourceTree = ""; @@ -4382,6 +4480,18 @@ path = Model; sourceTree = ""; }; + A58472022C2D9CF400713D36 /* Profiles */ = { + isa = PBXGroup; + children = ( + 6E8756622830E86B00D5A283 /* ProfileItemCell.swift */, + 6E9F28C828436EE800142502 /* AddNewProfileCell.swift */, + 6E05868028539E490044786B /* EditableProfileItemCell.swift */, + AE7452882827E37600A3AFAD /* ProfilesVM.swift */, + AE74528A2827E38900A3AFAD /* ProfilesVC.swift */, + ); + path = Profiles; + sourceTree = ""; + }; A58A63032C07154700A9D02D /* VerticalBlind */ = { isa = PBXGroup; children = ( @@ -4550,6 +4660,7 @@ A54A06892AF925FB00C03DBC /* ProfileUseCasesMocks.swift */, A52BFEE92B17277C00A2F64C /* ClientUseCasesMocks.swift */, A5E40B5C2B86075500DB6ABE /* ValueProviderMocks.swift */, + A5D712712C3E6FD700A8EF52 /* LockUseCasesMocks.swift */, ); path = UseCase; sourceTree = ""; @@ -4561,6 +4672,7 @@ A59AB8E02A309C1500D91F1F /* UseCaseTest.swift */, A54A06572AF3FDCA00C03DBC /* ObservableTestCase.swift */, A55A8DBE2BB2EED300C540D4 /* CompletableTestCase.swift */, + A5D712762C3E769300A8EF52 /* SingleTestCase.swift */, ); path = Core; sourceTree = ""; @@ -4618,18 +4730,20 @@ A59AB8C02A308ADA00D91F1F /* UseCase */ = { isa = PBXGroup; children = ( + A5D712782C3E8F5100A8EF52 /* App */, + A59AB8E42A30A72000D91F1F /* Channel */, + A530EE372A57FCA500F8DAEE /* ChannelBase */, A5E40B692B8634A900DB6ABE /* ChannelConfig */, - A58B2CB22AC195EB00764388 /* Thermostat */, A596838B2ABE26430005E73C /* ChannelRelation */, A573B0AF2A602F4A001E19D0 /* Client */, - A59AB8E42A30A72000D91F1F /* Channel */, - A530EE372A57FCA500F8DAEE /* ChannelBase */, A59AB8C12A308AE500D91F1F /* Detail */, A59AB8DD2A309BA900D91F1F /* Group */, A530EE312A57E36600F8DAEE /* Icon */, A59AB8EE2A30ACA700D91F1F /* Location */, + A5D712732C3E707800A8EF52 /* Lock */, A59AB8DA2A30934300D91F1F /* Profile */, A59AB8E92A30AA1800D91F1F /* Scene */, + A58B2CB22AC195EB00764388 /* Thermostat */, ); path = UseCase; sourceTree = ""; @@ -4735,6 +4849,77 @@ path = ChannelConfig; sourceTree = ""; }; + A5A15FE42C256E320049AA73 /* Navigation */ = { + isa = PBXGroup; + children = ( + A5A15FE72C2985780049AA73 /* Coordinator.swift */, + A5A160142C2ADE270049AA73 /* SuplaAppCoordinator.swift */, + A5E9CE2E2C33D92900509702 /* SAAddWizardVC+NavigationSubcontroller.swift */, + ); + path = Navigation; + sourceTree = ""; + }; + A5A15FEB2C298A580049AA73 /* Status */ = { + isa = PBXGroup; + children = ( + A5A15FEC2C298A6A0049AA73 /* StatusVC.swift */, + A5A15FEE2C298AAD0049AA73 /* StatusVM.swift */, + A5A15FF02C298B700049AA73 /* StatusView.swift */, + A5A15FFE2C2AA44B0049AA73 /* StatusFeature.swift */, + A5A160002C2AA4950049AA73 /* StatusViewState.swift */, + ); + path = Status; + sourceTree = ""; + }; + A5A15FF52C299C390049AA73 /* SwiftUiComponents */ = { + isa = PBXGroup; + children = ( + A5A1601B2C2BEE7F0049AA73 /* Button */, + A5A1600A2C2AB4520049AA73 /* Architecture */, + A5A160022C2AAEDE0049AA73 /* Text.swift */, + A5A160192C2BEC6B0049AA73 /* FilledButton.swift */, + A5F5C3F92C36963B0058E255 /* PinTextField.swift */, + ); + path = SwiftUiComponents; + sourceTree = ""; + }; + A5A1600A2C2AB4520049AA73 /* Architecture */ = { + isa = PBXGroup; + children = ( + A5A160062C2AB3ED0049AA73 /* SuplaCore+BaseViewModel.swift */, + A5A160082C2AB42F0049AA73 /* SuplaCore+BaseViewController.swift */, + ); + path = Architecture; + sourceTree = ""; + }; + A5A1600B2C2AC16A0049AA73 /* State */ = { + isa = PBXGroup; + children = ( + A5A1600C2C2AC17F0049AA73 /* SuplaAppState.swift */, + A5A1600E2C2AC18F0049AA73 /* SuplaAppEvent.swift */, + A5A160102C2AD0F70049AA73 /* SuplaAppStateHolder.swift */, + ); + path = State; + sourceTree = ""; + }; + A5A160162C2AE9DF0049AA73 /* App */ = { + isa = PBXGroup; + children = ( + A5A160172C2AE9F30049AA73 /* InitializationUseCase.swift */, + ); + path = App; + sourceTree = ""; + }; + A5A1601B2C2BEE7F0049AA73 /* Button */ = { + isa = PBXGroup; + children = ( + A5A15FF32C2994BF0049AA73 /* BorderedButton.swift */, + A5A15FF62C299C8A0049AA73 /* BackgroundStack.swift */, + A5A1601C2C2BEE8D0049AA73 /* TextButton.swift */, + ); + path = Button; + sourceTree = ""; + }; A5A184B22A2DD0A20063FD2B /* Mappings */ = { isa = PBXGroup; children = ( @@ -4758,6 +4943,7 @@ A5A23C2A2ABD96BE00233542 /* Model */ = { isa = PBXGroup; children = ( + A5F5C3ED2C358ADC0058E255 /* AppSettings */, A55A8DD22BB44ED700C540D4 /* SuplaApp */, A54A066D2AF5409D00C03DBC /* Chart */, A5A23C2B2ABD96C600233542 /* SuplaClient */, @@ -4862,7 +5048,6 @@ A5AE7A832A3AC6E80097FA8B /* Cells */, AEF79D8A2712FE1800D7554B /* AppSettingsVM.swift */, AEF79D8C2712FF8D00D7554B /* AppSettingsVC.swift */, - AE53535327691EFC0077BFFB /* AppSettingsNavigationCoordinator.swift */, ); path = AppSettings; sourceTree = ""; @@ -4878,6 +5063,7 @@ A5AE7A8E2A3AE0290097FA8B /* TitleArrowButtonCell.swift */, A5AE7A902A3B17AF0097FA8B /* PermissionCell.swift */, A5812CBB2BBC305200FC5998 /* NightModeCell.swift */, + A5F5C3E72C357DF00058E255 /* LockScreenCell.swift */, ); path = Cells; sourceTree = ""; @@ -4987,13 +5173,81 @@ path = Esp; sourceTree = ""; }; + A5D7125D2C37EA7200A8EF52 /* LockScreen */ = { + isa = PBXGroup; + children = ( + A5D7125E2C37EA8300A8EF52 /* LockScreenFeature.swift */, + A5D712602C37EAA400A8EF52 /* LockScreenViewState.swift */, + A5D712622C37EB4900A8EF52 /* LockScreenVM.swift */, + A5D712642C37EB8F00A8EF52 /* LockScreenView.swift */, + A5D712662C37EBE700A8EF52 /* LockScreenVC.swift */, + A5D712682C37ECF400A8EF52 /* UnlockAction.swift */, + ); + path = LockScreen; + sourceTree = ""; + }; + A5D7126A2C380D6A00A8EF52 /* Lock */ = { + isa = PBXGroup; + children = ( + A5D7126B2C380D7700A8EF52 /* CheckPinUseCase.swift */, + ); + path = Lock; + sourceTree = ""; + }; + A5D712732C3E707800A8EF52 /* Lock */ = { + isa = PBXGroup; + children = ( + A5D712742C3E708900A8EF52 /* CheckPinUseCaseTests.swift */, + ); + path = Lock; + sourceTree = ""; + }; + A5D712782C3E8F5100A8EF52 /* App */ = { + isa = PBXGroup; + children = ( + A5D712792C3E8F8500A8EF52 /* InitializationUseCaseTests.swift */, + ); + path = App; + sourceTree = ""; + }; + A5D7127F2C3EA2C400A8EF52 /* LockScreen */ = { + isa = PBXGroup; + children = ( + A5D712802C3EA2E300A8EF52 /* LockScreenVMTests.swift */, + ); + path = LockScreen; + sourceTree = ""; + }; + A5D712832C3EA39B00A8EF52 /* Navigation */ = { + isa = PBXGroup; + children = ( + A5D712842C3EA3A700A8EF52 /* SuplaAppCoordinatorMock.swift */, + ); + path = Navigation; + sourceTree = ""; + }; + A5D712862C3EB23500A8EF52 /* PinSetup */ = { + isa = PBXGroup; + children = ( + A5D712872C3EB24900A8EF52 /* PinSetupVMTests.swift */, + ); + path = PinSetup; + sourceTree = ""; + }; + A5D712892C3EB58100A8EF52 /* Status */ = { + isa = PBXGroup; + children = ( + A5D7128A2C3EB58D00A8EF52 /* StatusVMTests.swift */, + ); + path = Status; + sourceTree = ""; + }; A5D837C92AF10335002A420D /* ThermometerDetail */ = { isa = PBXGroup; children = ( A5D837D02AF10607002A420D /* History */, A5D837CA2AF1034E002A420D /* ThermometerDetailVC.swift */, A5D837CC2AF1035E002A420D /* ThermometerDetailVM.swift */, - A5D837CE2AF10374002A420D /* ThermometerDetailNavigatorCoordinator.swift */, ); path = ThermometerDetail; sourceTree = ""; @@ -5075,6 +5329,8 @@ A5E490492A3C6215006801FE /* ChannelHeight.swift */, A5E4904C2A3C6250006801FE /* TemperatureUnit.swift */, A5812CB92BBC2DB500FC5998 /* DarkModeSetting.swift */, + A5F5C3E92C3580000058E255 /* LockScreenScope.swift */, + A5F5C3EB2C3581BD0058E255 /* LockScreenSettings.swift */, ); path = AppSettings; sourceTree = ""; @@ -5090,6 +5346,7 @@ A54A064B2AF3E55700C03DBC /* SuplaSchedulers.swift */, A55A8DA72BAC50B100C540D4 /* NotificationCenterWrapper.swift */, A55A8DBA2BB2E30900C540D4 /* ThreadHandler.swift */, + A5D7127B2C3E916C00A8EF52 /* DatabaseProxy.swift */, ); path = Infrastructure; sourceTree = ""; @@ -5113,24 +5370,55 @@ path = Icon; sourceTree = ""; }; + A5E9CE252C32CE0B00509702 /* Authorization */ = { + isa = PBXGroup; + children = ( + A55A8DA92BAC608C00C540D4 /* SAAuthorizationDialogVC.swift */, + A55A8DAB2BAC60A200C540D4 /* SAAuthorizationDialogVM.swift */, + A5E9CE262C32CE3500509702 /* SALoginDialogVC.swift */, + A5E9CE282C32CE4700509702 /* SALoginDialogVM.swift */, + A5E9CE2A2C32CE5D00509702 /* SACredentialsDialogBaseVC.swift */, + A5E9CE2C2C32CE6D00509702 /* SACredentialsDialogBaseVM.swift */, + ); + path = Authorization; + sourceTree = ""; + }; + A5E9CE302C33DB8800509702 /* LocationOrdering */ = { + isa = PBXGroup; + children = ( + AE535355276927690077BFFB /* LocationOrderingVC.swift */, + AEED87DE276F1FCE005D76CF /* LocationOrderingVM.swift */, + ); + path = LocationOrdering; + sourceTree = ""; + }; + A5E9CE312C33DC2100509702 /* Menu */ = { + isa = PBXGroup; + children = ( + AE5332E52764A7E30050E690 /* SuplaMenuController.swift */, + ); + path = Menu; + sourceTree = ""; + }; A5F14B7729DED97400682FA6 /* UI */ = { isa = PBXGroup; children = ( - A55A8DA02BAC29ED00C540D4 /* Dialogs */, - A52BFEAB2B050D0000A2F64C /* TextFields */, - A57668C62AE7CE480025509D /* Charts */, - A51BE8E42AA7054F00718F2F /* Details */, - A530EE512A5D432600F8DAEE /* Views */, + A5F14B7829DED9F000682FA6 /* BaseViewController.swift */, + A5F14B7329DED41000682FA6 /* BaseViewModel.swift */, A530EE482A5C13D700F8DAEE /* Buttons */, + A57668C62AE7CE480025509D /* Charts */, A530EDEE2A53F79900F8DAEE /* Components */, + A51BE8E42AA7054F00718F2F /* Details */, + A55A8DA02BAC29ED00C540D4 /* Dialogs */, + A57C4AB62AAF4F7600D9C695 /* FilteredTapGestureDelegate.swift */, + A5F29B8C2A20AD8100ED700A /* Nibs.swift */, + A52BFEB02B06163B00A2F64C /* SADateTimePicker.swift */, + A5D7126D2C3D2F2000A8EF52 /* NavigationBarVisibilityController.swift */, A5F29B8E2A20B21A00ED700A /* TableView */, - A5F14B7329DED41000682FA6 /* BaseViewModel.swift */, + A52BFEAB2B050D0000A2F64C /* TextFields */, A5F14B7529DED44600682FA6 /* ViewEvent.swift */, + A530EE512A5D432600F8DAEE /* Views */, A573B39129DEB25900EBAFC4 /* ViewState.swift */, - A5F14B7829DED9F000682FA6 /* BaseViewController.swift */, - A5F29B8C2A20AD8100ED700A /* Nibs.swift */, - A57C4AB62AAF4F7600D9C695 /* FilteredTapGestureDelegate.swift */, - A52BFEB02B06163B00A2F64C /* SADateTimePicker.swift */, ); path = UI; sourceTree = ""; @@ -5138,6 +5426,9 @@ A5F14B7A29DEE4B600682FA6 /* Features */ = { isa = PBXGroup; children = ( + A5D712892C3EB58100A8EF52 /* Status */, + A5D712862C3EB23500A8EF52 /* PinSetup */, + A5D7127F2C3EA2C400A8EF52 /* LockScreen */, A5E40B622B86240A00DB6ABE /* Details */, A57638C329E585CA003E15A3 /* AccountCreation */, A5F14B7B29DEE4C200682FA6 /* AccountRemoval */, @@ -5243,25 +5534,29 @@ A5F29B812A1F8D7A00ED700A /* Extensions */ = { isa = PBXGroup; children = ( - A5F29B732A1E33AC00ED700A /* NSManagedObjectContext+Rx.swift */, - A5F29B8A2A209EC900ED700A /* NSFetchRequest+Ext.swift */, - A5F29BA22A234BFA00ED700A /* ObservableType+Ext.swift */, - A5F29C012A2DC56600ED700A /* NSPersistentStoreCoordinator+Ext.swift */, - A5F29C032A2DC5D500ED700A /* NSManagedObjectModel+Ext.swift */, - A573B0952A5FD152001E19D0 /* FatalError.swift */, - A5FE67662A666F2200147D1F /* String+Ext.swift */, - A58A9BFD2AA0A4BD00D28848 /* UIView+Ext.swift */, - A5477DC22AA5BE7A00220B4A /* Int+Ext.swift */, - A56233E22AB1903C001CB948 /* Float+Ext.swift */, - A56233E82AB31A26001CB948 /* CGPoint+Ext.swift */, + A57668EC2AEA85150025509D /* Array+Ext.swift */, A56233EA2AB31A45001CB948 /* CALayer+Ext.swift */, + A56233E82AB31A26001CB948 /* CGPoint+Ext.swift */, + A5AD70172C00766400A36318 /* CGRect+Ext.swift */, + A5E9CE362C3430DB00509702 /* Completable+Supla.swift */, A57668E22AEA4E450025509D /* Date+Ext.swift */, - A57668EC2AEA85150025509D /* Array+Ext.swift */, A57668FE2AEFB96A0025509D /* Double+Ext.swift */, - A55A8D7C2BA9A15200C540D4 /* UIImageView+Ext.swift */, + A573B0952A5FD152001E19D0 /* FatalError.swift */, + A56233E22AB1903C001CB948 /* Float+Ext.swift */, + A5477DC22AA5BE7A00220B4A /* Int+Ext.swift */, + A5F29B8A2A209EC900ED700A /* NSFetchRequest+Ext.swift */, + A5F29B732A1E33AC00ED700A /* NSManagedObjectContext+Rx.swift */, + A5F29C032A2DC5D500ED700A /* NSManagedObjectModel+Ext.swift */, + A5F29C012A2DC56600ED700A /* NSPersistentStoreCoordinator+Ext.swift */, + A5F29BA22A234BFA00ED700A /* ObservableType+Ext.swift */, A55A8D972BAB28CC00C540D4 /* ScopeFunctions.swift */, + A5FE67662A666F2200147D1F /* String+Ext.swift */, + A5A160122C2AD4470049AA73 /* Synchronization.swift */, + A55A8D7C2BA9A15200C540D4 /* UIImageView+Ext.swift */, + A5A15FE92C2987450049AA73 /* UINavigationController+Ext.swift */, + A58A9BFD2AA0A4BD00D28848 /* UIView+Ext.swift */, + A58472002C2D7F3200713D36 /* UIViewController+SuplaNavBar.swift */, A50B5D232BEE4CE600918D18 /* UIViewController+Toast.swift */, - A5AD70172C00766400A36318 /* CGRect+Ext.swift */, ); path = Extensions; sourceTree = ""; @@ -5269,6 +5564,8 @@ A5F29B842A1F95B100ED700A /* UseCase */ = { isa = PBXGroup; children = ( + A5D7126A2C380D6A00A8EF52 /* Lock */, + A5A160162C2AE9DF0049AA73 /* App */, A55501FA2B83F26C00FD3296 /* Notification */, A5A14A3B2B6130E9004B1598 /* ChannelConfig */, A56233F12AB3A375001CB948 /* Thermostat */, @@ -5415,7 +5712,6 @@ children = ( A57B03C42B29D67700C22966 /* Helper */, AEF1F9B927726D56008E441A /* DetailViewController.swift */, - A5F29BE72A2774BF00ED700A /* LegacyDetailNavigationCoordinator.swift */, ); path = LegacyDetail; sourceTree = ""; @@ -5448,52 +5744,44 @@ path = Migration; sourceTree = ""; }; - A5FE675D2A65CC2A00147D1F /* ClientApi */ = { + A5F5C3ED2C358ADC0058E255 /* AppSettings */ = { isa = PBXGroup; children = ( - A5FE675E2A65CC4300147D1F /* Autodiscover.swift */, - A5FE67602A65CC9900147D1F /* SuplaCloudClient.swift */, - A57668CE2AE914140025509D /* TemperatureMeasurement.swift */, - A57668D62AE98FCA0025509D /* TemperatureAndHumidityMeasurement.swift */, - A57668DA2AE992310025509D /* Measurement.swift */, - A54149342B63B91B00B44BD6 /* GeneralPurposeMeasurement.swift */, - A503ABB02B6A9B43008CDA1F /* GeneralPurposeMeter.swift */, + A5F5C3EE2C358AF90058E255 /* LockScreenSettingsTests.swift */, ); - path = ClientApi; + path = AppSettings; sourceTree = ""; }; - AE5A60E4270E2A4100F2B780 /* Cfg */ = { + A5F5C3F02C36822A0058E255 /* PinSetup */ = { isa = PBXGroup; children = ( - 6E05868028539E490044786B /* EditableProfileItemCell.swift */, - 6E9F28C828436EE800142502 /* AddNewProfileCell.swift */, - 6E8756622830E86B00D5A283 /* ProfileItemCell.swift */, - AE535355276927690077BFFB /* LocationOrderingVC.swift */, - AEED87DE276F1FCE005D76CF /* LocationOrderingVM.swift */, - AE7452882827E37600A3AFAD /* ProfilesVM.swift */, - AE74528A2827E38900A3AFAD /* ProfilesVC.swift */, + A5F5C3F12C3682390058E255 /* PinSetupFeature.swift */, + A5F5C3F32C3682500058E255 /* PinSetupVM.swift */, + A5F5C3F52C36827A0058E255 /* PinSetupViewState.swift */, + A5F5C3F72C3683270058E255 /* PinSetupView.swift */, + A5F5C3FB2C3698040058E255 /* PinSetupVC.swift */, ); - path = Cfg; + path = PinSetup; sourceTree = ""; }; - AE929A85275FD53A00B75715 /* Navigation */ = { + A5FE675D2A65CC2A00147D1F /* ClientApi */ = { isa = PBXGroup; children = ( - AE7452902827F49100A3AFAD /* ProfilesNavigationCoordinator.swift */, - AE3DECE12761DB32005923E4 /* PresentationNavigationCoordinator.swift */, - AE929A86275FD5D500B75715 /* MainNavigationCoordinator.swift */, - AE929A8A2761487700B75715 /* NavigationCoordinator.swift */, - AE5332E127648E770050E690 /* SuplaNavigationController.swift */, - AE5332E52764A7E30050E690 /* SuplaMenuController.swift */, + A5FE675E2A65CC4300147D1F /* Autodiscover.swift */, + A5FE67602A65CC9900147D1F /* SuplaCloudClient.swift */, + A57668CE2AE914140025509D /* TemperatureMeasurement.swift */, + A57668D62AE98FCA0025509D /* TemperatureAndHumidityMeasurement.swift */, + A57668DA2AE992310025509D /* Measurement.swift */, + A54149342B63B91B00B44BD6 /* GeneralPurposeMeasurement.swift */, + A503ABB02B6A9B43008CDA1F /* GeneralPurposeMeter.swift */, ); - path = Navigation; + path = ClientApi; sourceTree = ""; }; AEB195DC276E59B60091D314 /* Transitions */ = { isa = PBXGroup; children = ( AEB195DD276E5A040091D314 /* FadeTransition.swift */, - AE68E73127739D8500E55DB7 /* SwipeTransition.swift */, ); path = Transitions; sourceTree = ""; @@ -5509,8 +5797,6 @@ AED6395C270CA37A00E5105B /* Controls */ = { isa = PBXGroup; children = ( - AED6395D270CA3A800E5105B /* RoundedButton.swift */, - AECB1BE627109EA3001A9714 /* CheckBox.swift */, AE348E64277F348700F363A3 /* SAMoveTableView.h */, AE348E65277F348700F363A3 /* SAMoveTableView.m */, ); @@ -5655,7 +5941,6 @@ 01D5E0CA22DB8F0A00FBE1DC /* SAChartMarkerView.xib in Resources */, 405AC41A1BC15898004F7311 /* app_icon@2x.png in Resources */, 0148934125AA19DA00B9974E /* infinitywhite@3x.png in Resources */, - 407D4B001BC6E491009A5505 /* StatusVC.xib in Resources */, 406778561BCBF9F4008DD37F /* SectionCell.xib in Resources */, 013F7EC423EB4B500061A497 /* battery@3x.png in Resources */, 407D4AE91BC6C5C7009A5505 /* app_icon87x87@3x.png in Resources */, @@ -5876,7 +6161,6 @@ buildActionMask = 2147483647; files = ( AEF6A5D028BBD1EC0019684A /* SceneCell.swift in Sources */, - A573B38C29DE9F5600EBAFC4 /* BaseViewControllerExtensions.swift in Sources */, A55A8DBB2BB2E30900C540D4 /* ThreadHandler.swift in Sources */, 01F8857222E5E79100D18373 /* SAImpulseCounterMeasurementItem+CoreDataClass.m in Sources */, 010C8786249A6A92002FE526 /* SADialog.m in Sources */, @@ -5889,9 +6173,7 @@ A5074BF12BDA73250081B6B1 /* GroupTotalValue.swift in Sources */, A51BE8FF2AA73A5900718F2F /* MeasurementValue.swift in Sources */, A56234012ABACEAF001CB948 /* SAChannel+Ext.swift in Sources */, - AE3DECE22761DB32005923E4 /* AccountCreationNavigationCoordinator.swift in Sources */, A541493E2B63E42900B44BD6 /* EmptyChartData.swift in Sources */, - AE53535427691EFC0077BFFB /* AppSettingsNavigationCoordinator.swift in Sources */, A5F29BC02A24DDB100ED700A /* UpdateChannelGroupRelationUseCase.swift in Sources */, A5F29B802A1E489300ED700A /* ProfileRepository.swift in Sources */, 010714782656C605009C119F /* SAZWaveWakeupSettingsReport.m in Sources */, @@ -5930,10 +6212,10 @@ A54149292B63034500B44BD6 /* GpmDetailVC.swift in Sources */, 40E8BE891FC06A5600FB2FE6 /* TFHppleElement.m in Sources */, A530EE362A57F65F00F8DAEE /* GetChannelBaseStateUseCase.swift in Sources */, - A5F29BE82A2774BF00ED700A /* LegacyDetailNavigationCoordinator.swift in Sources */, AED63961270CA46C00E5105B /* UIColor+Supla.swift in Sources */, 01A195E2264B264A006D2A20 /* SAChannelCaptionSetResult.m in Sources */, A5A14A3A2B612F1C004B1598 /* ChannelConfigRepository.swift in Sources */, + A5A160012C2AA4950049AA73 /* StatusViewState.swift in Sources */, 6E8756632830E86D00D5A283 /* ProfileItemCell.swift in Sources */, A541493A2B63D81000B44BD6 /* ChartData.swift in Sources */, A57668E12AE99DB90025509D /* ChartDataAggregation.swift in Sources */, @@ -5943,6 +6225,7 @@ A55501FC2B83F28800FD3296 /* InsertNotificationUseCase.swift in Sources */, 01C1719922C7F55B005983E1 /* SAMeasurementItem+CoreDataClass.m in Sources */, A55A8D722BA8406900C540D4 /* WindowDetailVC.swift in Sources */, + A5E9CE272C32CE3500509702 /* SALoginDialogVC.swift in Sources */, A58A630E2C071B0600A9D02D /* VerticalBlindsView.swift in Sources */, A5F29BE62A27739000ED700A /* MoveableCell.swift in Sources */, A5AE7A8F2A3AE0290097FA8B /* TitleArrowButtonCell.swift in Sources */, @@ -5956,7 +6239,6 @@ A5F14B7229DED06400682FA6 /* AccountRemovalVM.swift in Sources */, A5A14A362B611863004B1598 /* GetChannelBaseDefaultCaptionUseCase.swift in Sources */, A5756F8229DC102800C32A1B /* AccountRemovalVC.swift in Sources */, - A5074BB32BC957E70081B6B1 /* DeviceCatalogNavigationCoordinator.swift in Sources */, A5E490652A401A7D006801FE /* UpdateSceneIconRelationsUseCase.swift in Sources */, A5B3CC082B62817B00F95AC3 /* WeigthValueStringProvider.swift in Sources */, A57668DB2AE992310025509D /* Measurement.swift in Sources */, @@ -5975,7 +6257,6 @@ A5F8361D2A2E004C00E5CA71 /* Migration10to11ModelMapping.xcmappingmodel in Sources */, A56091752AE685A800AFE14F /* ThermostatHistoryDetailVM.swift in Sources */, A5B3CBEB2B625D2F00F95AC3 /* DistanceValueProvider.swift in Sources */, - AE929A87275FD5D500B75715 /* MainNavigationCoordinator.swift in Sources */, 019170A1233D4D5A00820BDB /* SADownloadThermostatMeasurements.m in Sources */, A5F29BBE2A24DB1300ED700A /* UpdateGroupUseCase.swift in Sources */, 019D22F0233CC2D000F17135 /* SAPreloader.m in Sources */, @@ -5986,25 +6267,30 @@ A5F8361F2A2E008C00E5CA71 /* AuthProfileItemInitialMigrationPolicy.swift in Sources */, 40DE1A361BCD1B55004CF43B /* AboutVC.m in Sources */, A5E4905A2A3FA151006801FE /* GetAllIconsToDownloadUseCase.swift in Sources */, - A51BE8EF2AA715C300718F2F /* ThermostatDetailNavigationCoordinator.swift in Sources */, + A5A15FE82C2985780049AA73 /* Coordinator.swift in Sources */, 4017E12C1BB9CE2900570AC8 /* ChannelCell.m in Sources */, - A541492B2B63038600B44BD6 /* GpmDetailNavigatorCoordinator.swift in Sources */, 011021C425CC39AC00621D41 /* SARegistrationEnabled.m in Sources */, + A58472062C2EC7E500713D36 /* DisconnectUseCase.swift in Sources */, A5F29B6B2A1E26AA00ED700A /* SceneListVC.swift in Sources */, A530EE222A56FF2200F8DAEE /* HumidityAndThermometerIconNameProducer.swift in Sources */, 019F4AAF2614B75800065420 /* SAAbstractPickerField.m in Sources */, 6EC4B005286F420500731079 /* SAElectricityMeterChartMarkerView.swift in Sources */, + A5A160182C2AE9F30049AA73 /* InitializationUseCase.swift in Sources */, + A5D712672C37EBE700A8EF52 /* LockScreenVC.swift in Sources */, 01EFD94A236CA94400893489 /* NSData+AES.m in Sources */, A5477DCD2AA5F7A900220B4A /* IssueIconType.swift in Sources */, A5D837C82AF0F154002A420D /* PullToRefreshView.swift in Sources */, A5D837D92AF1069A002A420D /* BaseHistoryDetailVM.swift in Sources */, + A5F5C3E82C357DF00058E255 /* LockScreenCell.swift in Sources */, A57668F12AEA9F5B0025509D /* BaseDownloadLogUseCase.swift in Sources */, A52BFEC22B078BB200A2F64C /* SuplaLedStatusField.swift in Sources */, + A5F5C3EC2C3581BD0058E255 /* LockScreenSettings.swift in Sources */, A50B5D0A2BEA4EC700918D18 /* ShadingBlindMarker.swift in Sources */, A56233FB2AB88722001CB948 /* DelayedWeeklyScheduleConfigSubject.swift in Sources */, A530EE432A58AA6E00F8DAEE /* DeviceStateHelperVCI.swift in Sources */, A55501F52B83842000FD3296 /* NotificationsLogVC.swift in Sources */, A55A8DAC2BAC60A200C540D4 /* SAAuthorizationDialogVM.swift in Sources */, + A5E9CE372C3430DB00509702 /* Completable+Supla.swift in Sources */, 0148933425AA12DB00B9974E /* SADiwCalibrationTool.m in Sources */, A58A630B2C071ACB00A9D02D /* VerticalBlindsVC.swift in Sources */, 01C1719222C7F3A2005983E1 /* SAElectricityMeasurementItem+CoreDataProperties.m in Sources */, @@ -6018,6 +6304,7 @@ A503ABBA2B6BB0BF008CDA1F /* GpmValueFormatter.swift in Sources */, 40C7BA7720C047CF00ACEE42 /* SAChannel+CoreDataProperties.m in Sources */, A530EE302A57E2B900F8DAEE /* DigiglassVerticalIconNameProducer.swift in Sources */, + A5A160072C2AB3ED0049AA73 /* SuplaCore+BaseViewModel.swift in Sources */, 01F8856E22E5E79100D18373 /* SAImpulseCounterMeasurementItem+CoreDataProperties.m in Sources */, A5074BD32BCE98760081B6B1 /* SlatTiltSlider.swift in Sources */, A52BFEC62B07904E00A2F64C /* SuplaButtonVolumeField.swift in Sources */, @@ -6033,20 +6320,24 @@ A56233DA2AB061EB001CB948 /* SACustomDialogVC.swift in Sources */, 01199C8324C34AC50062454B /* SAChannelStatePopup.m in Sources */, 011021B525CC386D00621D41 /* SAVersionError.m in Sources */, + A5A160112C2AD0F70049AA73 /* SuplaAppStateHolder.swift in Sources */, A5F14B7929DED9F000682FA6 /* BaseViewController.swift in Sources */, A5F29BC22A24E30100ED700A /* ChangeChannelsVisibilityUseCase.swift in Sources */, A57C4AB72AAF4F7600D9C695 /* FilteredTapGestureDelegate.swift in Sources */, - AE7452912827F49100A3AFAD /* ProfilesNavigationCoordinator.swift in Sources */, A58A631F2C09C0F200A9D02D /* GarageDoorView.swift in Sources */, A55A8DAF2BAC64A700C540D4 /* SADialogTitleLabel.swift in Sources */, 0172677D234FAF78000F1CFB /* SADownloadImpulseCounterMeasurements.m in Sources */, A56233FF2ABAC488001CB948 /* ThermostatIconNameProducer.swift in Sources */, + A58472082C2ED60E00713D36 /* SuplaAppProvider.swift in Sources */, A5F29BF52A2A1E1200ED700A /* ChannelCaptionEditor.swift in Sources */, + A5E9CE242C32BB8700509702 /* LoginUseCase.swift in Sources */, A55A8D822BA9B6B400C540D4 /* WindowColors.swift in Sources */, + A5A15FE62C256F850049AA73 /* AppDelegate.swift in Sources */, A5074BD12BCE72400081B6B1 /* FacadeBlindsVC.swift in Sources */, A5B3CC022B627ADC00F95AC3 /* HumidityValueStringProvider.swift in Sources */, A51BE8ED2AA713A500718F2F /* ThermostatDetailVM.swift in Sources */, A5B3CBDF2B62504D00F95AC3 /* GetChannelValueUseCase.swift in Sources */, + A5A15FED2C298A6A0049AA73 /* StatusVC.swift in Sources */, A58A9C072AA1D64A00D28848 /* ChannelRelationRepository.swift in Sources */, A5AE7A8B2A3ADF330097FA8B /* TitleSwitchCell.swift in Sources */, 01C1719A22C7F55B005983E1 /* SAMeasurementItem+CoreDataProperties.m in Sources */, @@ -6105,10 +6396,12 @@ 01B57DEC25BDA48F001F0DBC /* SAWizardVC.m in Sources */, A57668C12AE7996D0025509D /* HistoryDataSet.swift in Sources */, A5F29BA82A24B0FC00ED700A /* UpdateSceneStateUseCase.swift in Sources */, + A5D712612C37EAA400A8EF52 /* LockScreenViewState.swift in Sources */, A5F29BDC2A26901100ED700A /* ChannelCell+Ext.swift in Sources */, A5AE7A892A3ADE800097FA8B /* TemperatureUnitCell.swift in Sources */, AEF79D912713088100D7554B /* Strings.swift in Sources */, A55A8D982BAB28CC00C540D4 /* ScopeFunctions.swift in Sources */, + A5E9CE292C32CE4700509702 /* SALoginDialogVM.swift in Sources */, 01AB61E925B8E08E00DBEF74 /* SADigiglassController.m in Sources */, 40C7BA7920C047CF00ACEE42 /* SAChannelGroup+CoreDataProperties.m in Sources */, A57668C52AE79BA40025509D /* HorizontalyScrollableView.swift in Sources */, @@ -6128,6 +6421,7 @@ A56233F92AB85CA1001CB948 /* ExecuteThermostatActionUseCase.swift in Sources */, A57668E92AEA7AF50025509D /* SelectableList.swift in Sources */, A5074BAC2BC9489D0081B6B1 /* WebContentVC.swift in Sources */, + A5A15FEF2C298AAD0049AA73 /* StatusVM.swift in Sources */, A5B3CC062B62802600F95AC3 /* RainValueStringProvider.swift in Sources */, A530EE002A54417400F8DAEE /* SwitchTimerDetailVM.swift in Sources */, A52BFECE2B0796BC00A2F64C /* SuplaHomeScreenContentField.swift in Sources */, @@ -6142,6 +6436,7 @@ 01CEAC1E25B99DAC009C3BFD /* SADigiglassValue.m in Sources */, AEED1FD42737F71C00DE6289 /* MultiAccountProfileManager.swift in Sources */, A5B3CBE22B62525500F95AC3 /* DefaultDoubleValueProvider.swift in Sources */, + A5F5C3F22C3682390058E255 /* PinSetupFeature.swift in Sources */, A56D5ACF2A4C37AD004F45DA /* SingleCallWrapper.m in Sources */, A5812CBA2BBC2DB500FC5998 /* DarkModeSetting.swift in Sources */, 4004DE8D1DC4E270009C34E3 /* SAColorBrightnessPicker.m in Sources */, @@ -6152,27 +6447,27 @@ A51BE8F52AA7190500718F2F /* ThermostatGeneralVC.swift in Sources */, A5D837CB2AF1034E002A420D /* ThermometerDetailVC.swift in Sources */, A51BE8E72AA705AD00718F2F /* StandardDetailVM.swift in Sources */, + A5E9CE2F2C33D92900509702 /* SAAddWizardVC+NavigationSubcontroller.swift in Sources */, 019D9E2C25F271CF00881C99 /* SACaptionEditor.m in Sources */, - A55501F72B8384DF00FD3296 /* NotificationsLogNavigationCoordinator.swift in Sources */, A57668D72AE98FCA0025509D /* TemperatureAndHumidityMeasurement.swift in Sources */, A52BFED02B07AF5500A2F64C /* GetDeviceConfigUseCase.swift in Sources */, A5AE7A912A3B17AF0097FA8B /* PermissionCell.swift in Sources */, A5074BE42BD251590081B6B1 /* ExecuteFacadeBlindActionUseCase.swift in Sources */, A51BE9032AA746C100718F2F /* RoundedControlButtonView.swift in Sources */, - 401CA1C21BA067DB00117AF4 /* AppDelegate.m in Sources */, A58A63192C09C03500A9D02D /* GarageDoorState.swift in Sources */, A5E490632A4019E6006801FE /* UpdateGroupIconRelationsUseCase.swift in Sources */, A5F29BAE2A24BA5800ED700A /* LegacyWrapper.swift in Sources */, A5F29B9A2A20C26900ED700A /* BaseTableViewController.swift in Sources */, - A5D837CF2AF10374002A420D /* ThermometerDetailNavigatorCoordinator.swift in Sources */, A5D837E32AF3A728002A420D /* LinkedList.swift in Sources */, A52BFEF02B1754FD00A2F64C /* DeleteProfileUseCase.swift in Sources */, A50B5D042BEA4CDA00918D18 /* RoofWindowState.swift in Sources */, A530EE402A58417D00F8DAEE /* ExecuteSimpleActionUseCase.swift in Sources */, A530EE262A57011F00F8DAEE /* DimmerAndRgbLightningIconNameProducer.swift in Sources */, A5D837CD2AF1035E002A420D /* ThermometerDetailVM.swift in Sources */, + A5A1600D2C2AC17F0049AA73 /* SuplaAppState.swift in Sources */, 013D1C5D22CE47D5000C0784 /* SAChartHelper.m in Sources */, A5F29B832A1F8D9B00ED700A /* SALocation+Ext.swift in Sources */, + A5A15FEA2C2987450049AA73 /* UINavigationController+Ext.swift in Sources */, A566398B2ABAD52300BA51D7 /* LoadingTimeoutManager.swift in Sources */, 015A98412655958500B6E6C6 /* NSDictionary+SUPLA.m in Sources */, 019F4CCD23577C3700286139 /* SAImpulseCounterChartHelper.m in Sources */, @@ -6182,7 +6477,9 @@ A553862D29E021AE00B5CF3F /* DiContainer.swift in Sources */, A5477DBF2AA5B40C00220B4A /* SuplaHvacMode.swift in Sources */, A503ABC02B6BD27F008CDA1F /* ChartEntryDetails.swift in Sources */, + A5D7127C2C3E916C00A8EF52 /* DatabaseProxy.swift in Sources */, A50B5D062BEA4DA700918D18 /* RollerShutterWindowState.swift in Sources */, + A5F5C3FC2C3698040058E255 /* PinSetupVC.swift in Sources */, A58316FA2B64507D006113F8 /* ThermometerValueProvider.swift in Sources */, A5F29BC92A26168E00ED700A /* UpdateChannelGroupTotalValueUseCase.swift in Sources */, A5477DB92AA1F56400220B4A /* ChannelChild.swift in Sources */, @@ -6194,6 +6491,7 @@ A52BFEE22B113BD700A2F64C /* TrippleNumberSelectorView.swift in Sources */, AE74528B2827E38900A3AFAD /* ProfilesVC.swift in Sources */, A52BFEE02B109F5A00A2F64C /* ThermostatTimerConfigurationView.swift in Sources */, + A5A1600F2C2AC18F0049AA73 /* SuplaAppEvent.swift in Sources */, A55A8DB92BADB29A00C540D4 /* GetGroupOnlineSummaryUseCase.swift in Sources */, A503ABB82B6BB082008CDA1F /* ChannelValueFormatter.swift in Sources */, A5074BC72BCE66930081B6B1 /* BaseWallWindowDimens.swift in Sources */, @@ -6205,20 +6503,23 @@ A5F29B7E2A1E36C200ED700A /* SceneRepository.swift in Sources */, A5074BC22BCE61E60081B6B1 /* RoofWindowVC.swift in Sources */, A55501E62B7F952E00FD3296 /* GeneralPurposeMeasurementIconNameProducer.swift in Sources */, + A5E9CE2B2C32CE5D00509702 /* SACredentialsDialogBaseVC.swift in Sources */, 01567446232FCFD700397393 /* SAIncrementalMeterExtendedValue.m in Sources */, - AE5332E227648E770050E690 /* SuplaNavigationController.swift in Sources */, A5B3CC0A2B6282B800F95AC3 /* WindValueStringProvider.swift in Sources */, A503ABB52B6B7713008CDA1F /* BaseMeasurementRepository.swift in Sources */, + A5A1601D2C2BEE8D0049AA73 /* TextButton.swift in Sources */, 015A983C265594B300B6E6C6 /* NSNumber+SUPLA.m in Sources */, A54149352B63B91B00B44BD6 /* GeneralPurposeMeasurement.swift in Sources */, 01147CE4264BE317002B2E1C /* SAChannelFunctionSetResult.m in Sources */, - AE68E73227739D8500E55DB7 /* SwipeTransition.swift in Sources */, + A5F5C3FA2C36963B0058E255 /* PinTextField.swift in Sources */, + A5A160052C2AAFBD0049AA73 /* Font+Supla.swift in Sources */, A52BFEB32B063EAC00A2F64C /* RangeValueType.swift in Sources */, 017554F622F1A62500EB58B7 /* HomePlusDetailView.m in Sources */, A5A15FE32C22FEAE0049AA73 /* AlarmArmamentIconNameProducer.swift in Sources */, 013D1C6322CE75A3000C0784 /* SAElectricityChartHelper.m in Sources */, A5F29BFC2A2DC16100ED700A /* CoreDataMigration.swift in Sources */, A5F29BA12A20D6DD00ED700A /* ChannelBaseTableViewController.swift in Sources */, + A5F5C3F82C3683270058E255 /* PinSetupView.swift in Sources */, A530EE0D2A56C65D00F8DAEE /* UIImage+Supla.swift in Sources */, 406778551BCBF9F4008DD37F /* SectionCell.m in Sources */, A5F29B582A1CB7DC00ED700A /* CoreDataManager.swift in Sources */, @@ -6228,6 +6529,7 @@ A58A63172C09BFAF00A9D02D /* GarageDoorVM.swift in Sources */, 40B610CC20C7E263002B762A /* SAUIChannelStatus.m in Sources */, AECB1BE727109EA3001A9714 /* CheckBox.swift in Sources */, + A5D712652C37EB8F00A8EF52 /* LockScreenView.swift in Sources */, A5A14A3D2B6134C4004B1598 /* InsertChannelConfigUseCase.swift in Sources */, A597271129DAEA480090A044 /* SAScene+Ext.swift in Sources */, A57668C82AE7CE890025509D /* SuplaCombinedChartView.swift in Sources */, @@ -6240,6 +6542,7 @@ A56233E92AB31A26001CB948 /* CGPoint+Ext.swift in Sources */, A5B3CC0E2B62934D00F95AC3 /* SAChannelConfig+Ext.swift in Sources */, A530EE4C2A5C2A3E00F8DAEE /* UIBorderedButton.swift in Sources */, + A5D712692C37ECF400A8EF52 /* UnlockAction.swift in Sources */, AE535356276927690077BFFB /* LocationOrderingVC.swift in Sources */, A5F29B632A1E18FA00ED700A /* ChannelListVM.swift in Sources */, A5B3CBFB2B62693F00F95AC3 /* DepthValueStringProvider.swift in Sources */, @@ -6256,6 +6559,7 @@ A51BE9052AA754F200718F2F /* BaseControlButtonView.swift in Sources */, A5CE73272B45A6EB003F882C /* EspHtmlParser.swift in Sources */, A553862B29E0202700B5CF3F /* Singleton.swift in Sources */, + A5A160032C2AAEDE0049AA73 /* Text.swift in Sources */, A5074BC02BCE61C20081B6B1 /* RoofWindowVM.swift in Sources */, A5F29BBC2A24D62300ED700A /* UpdateChannelExtendedValueUseCase.swift in Sources */, 012F722E22DFA0BE00E5F72E /* SABarChartDataSet.swift in Sources */, @@ -6294,23 +6598,25 @@ A530EE202A56FE4F00F8DAEE /* IconType.swift in Sources */, A5F14B7629DED44600682FA6 /* ViewEvent.swift in Sources */, A5F29B6F2A1E305500ED700A /* Repository.swift in Sources */, - AE929A8B2761487700B75715 /* NavigationCoordinator.swift in Sources */, 01BDAC7522C518DE00915646 /* SARestApiClientTask.m in Sources */, A5AD70272C04656700A36318 /* LeftMiddleRightControlButton.swift in Sources */, 401CA1BF1BA067DB00117AF4 /* main.m in Sources */, A5F29BFE2A2DC19500ED700A /* CoreDataMigrationVersion.swift in Sources */, 018CFD2623281AF900888CB7 /* SAThermostatHPExtendedValue.m in Sources */, A530EE112A56E5F300F8DAEE /* ChannelState.swift in Sources */, + A5A15FF42C2994BF0049AA73 /* BorderedButton.swift in Sources */, A55A8D9D2BAC191800C540D4 /* CallSuplaClientOperationUseCase.swift in Sources */, A5B3A4B52BB558B70001D006 /* RoofWindowView.swift in Sources */, A5AE7A7F2A3998260097FA8B /* NewGestureInfoView.swift in Sources */, A57777CD29E6907C004513E6 /* RuntimeConfig.swift in Sources */, A58A631B2C09C09100A9D02D /* GarageDoorVC.swift in Sources */, + A5D7126E2C3D2F2000A8EF52 /* NavigationBarVisibilityController.swift in Sources */, A57668DD2AE995590025509D /* GeneralError.swift in Sources */, A55A8D882BAAFFE700C540D4 /* ChannelIssueItem.swift in Sources */, A5074BB52BCCFD2B0081B6B1 /* IconCell.swift in Sources */, A530EDEB2A53F60400F8DAEE /* SwitchDetailVM.swift in Sources */, 01F8857122E5E79100D18373 /* SATemperatureMeasurementItem+CoreDataClass.m in Sources */, + A5A15FFD2C2A161E0049AA73 /* SuplaCore.swift in Sources */, A56233EB2AB31A45001CB948 /* CALayer+Ext.swift in Sources */, A56233F02AB3949D001CB948 /* ReadChannelWithChildrenUseCase.swift in Sources */, A54A064C2AF3E55700C03DBC /* SuplaSchedulers.swift in Sources */, @@ -6328,9 +6634,11 @@ A58316FC2B64510A006113F8 /* ThermometerValueStringProvider.swift in Sources */, A55A8D8A2BAB06D600C540D4 /* WindowState.swift in Sources */, A57668E72AEA68A60025509D /* DaysRange.swift in Sources */, + A58472012C2D7F3200713D36 /* UIViewController+SuplaNavBar.swift in Sources */, 40C7BA8720C0492500ACEE42 /* SAChannelBase+CoreDataClass.m in Sources */, A54149332B63B73700B44BD6 /* GeneralPurposeMeasurementItemRepository.swift in Sources */, A530EDF02A53F8E100F8DAEE /* SuplaTabBarController.swift in Sources */, + A5D712702C3E68D400A8EF52 /* OptionalValue.swift in Sources */, A5F29BF72A2A29BE00ED700A /* LocationCaptionEditor.swift in Sources */, A5074BAA2BC945DA0081B6B1 /* WebContentVM.swift in Sources */, A503ABB32B6A9ED9008CDA1F /* GeneralPurposeMeterItemRepository.swift in Sources */, @@ -6362,6 +6670,7 @@ A51BE8EB2AA7136000718F2F /* ThermostatDetailVC.swift in Sources */, 010714732656A6FB009C119F /* SAZWaveWakeupSettingsDialog.m in Sources */, 01FCDA78264F28C4002D776F /* SAZWaveNodeResult.m in Sources */, + A5F5C3F62C36827B0058E255 /* PinSetupViewState.swift in Sources */, 01F8856F22E5E79100D18373 /* SAThermostatMeasurementItem+CoreDataClass.m in Sources */, A5B3CC002B62770F00F95AC3 /* DistanceValueStringProvider.swift in Sources */, A503ABAB2B67A094008CDA1F /* CandleChartData.swift in Sources */, @@ -6372,6 +6681,7 @@ A5FE67672A666F2200147D1F /* String+Ext.swift in Sources */, A52BFEB12B06163B00A2F64C /* SADateTimePicker.swift in Sources */, A57668F92AEAB12D0025509D /* HideableValue.swift in Sources */, + A5A15FF72C299C8A0049AA73 /* BackgroundStack.swift in Sources */, A58317002B645814006113F8 /* ThermometerAndHumidityValueStringProvider.swift in Sources */, A50E5D702BFF4CAF00303BAE /* ChannelBaseActionUseCase.swift in Sources */, A5477DCB2AA5EC4000220B4A /* CreateChannelWithChildrenUseCase.swift in Sources */, @@ -6382,6 +6692,7 @@ A5074BF62BDC42F30081B6B1 /* GroupTotalValueTransformer.swift in Sources */, A5F29BC62A24E5DE00ED700A /* ChangeChannelGroupRelationsVisibilityUseCase.swift in Sources */, A530EE532A5D437C00F8DAEE /* TimerProgressView.swift in Sources */, + A5A15FFF2C2AA44B0049AA73 /* StatusFeature.swift in Sources */, A55A8D962BAB14A700C540D4 /* SuplaRollerShutterFlag.swift in Sources */, A50E5D7A2BFF552C00303BAE /* CurtainView.swift in Sources */, A530EE082A557EE400F8DAEE /* CircleControlButtonView.swift in Sources */, @@ -6402,6 +6713,7 @@ A5B3CBEF2B625D9000F95AC3 /* WeightValueProvider.swift in Sources */, A50B5D4E2BFCBDAE00918D18 /* HeatpolThermostatGroupActivePercentageProvider.swift in Sources */, A56F15EF2A2E68BA00C2E21B /* Migration11to12ModelMapping.xcmappingmodel in Sources */, + A5A160132C2AD4470049AA73 /* Synchronization.swift in Sources */, A530EDF52A5401BE00F8DAEE /* SwitchGeneralVC.swift in Sources */, A5F29B602A1E18E600ED700A /* ChannelListVC.swift in Sources */, A5812CBC2BBC305200FC5998 /* NightModeCell.swift in Sources */, @@ -6409,16 +6721,20 @@ A5F29BD22A26239C00ED700A /* TemperatureMeasurementItemRepository.swift in Sources */, A5F29BB42A24C96600ED700A /* ChannelValueRepository.swift in Sources */, 401CA1F21BA0A28A00117AF4 /* SuplaClient.m in Sources */, + A5A1601A2C2BEC6B0049AA73 /* FilledButton.swift in Sources */, AE7CAAFD275BF87D0024095F /* BaseViewController.m in Sources */, A5D837E12AF132F6002A420D /* LoadChannelMeasurementsDateRangeUseCase.swift in Sources */, + A5A160152C2ADE270049AA73 /* SuplaAppCoordinator.swift in Sources */, A5F29B932A20B56500ED700A /* CreateProfileScenesListUseCase.swift in Sources */, A530EE502A5C991C00F8DAEE /* UIPlainButton.swift in Sources */, + A5A15FF12C298B700049AA73 /* StatusView.swift in Sources */, A5F29B6D2A1E26B400ED700A /* SceneListVM.swift in Sources */, 6EC0BF2C2875F97B0000CAA8 /* ChartSettings.swift in Sources */, A530EE1E2A56FD4300F8DAEE /* StaircaseTimerIconNameProducer.swift in Sources */, A55A8D9B2BAB802B00C540D4 /* RollerShutterAction.swift in Sources */, 4006E90A1DC62D0A00C4456D /* DetailView.m in Sources */, A5B3CBED2B625D6000F95AC3 /* WindValueProvider.swift in Sources */, + A5F5C3EA2C3580000058E255 /* LockScreenScope.swift in Sources */, A5F29B902A20B23E00ED700A /* BaseTableViewModel.swift in Sources */, A55A8DA42BAC2B0E00C540D4 /* SAAlertDialogVM.swift in Sources */, A5B3A4BB2BB5B8F20001D006 /* WindowType.swift in Sources */, @@ -6426,13 +6742,14 @@ A51BE90F2AAAFB1400718F2F /* SetChannelConfigUseCase.swift in Sources */, 01D6A0F2249BD2F7006B3757 /* SASuperuserAuthorizationDialog.m in Sources */, A57668D12AE918100025509D /* SuplaCloudService.swift in Sources */, - AE3DECE32761DB32005923E4 /* PresentationNavigationCoordinator.swift in Sources */, + A5E9CE2D2C32CE6D00509702 /* SACredentialsDialogBaseVM.swift in Sources */, 01BDAC7822C5200600915646 /* SAOAuthToken.m in Sources */, A553862F29E0297400B5CF3F /* Inject.swift in Sources */, A5F29BDA2A268DDE00ED700A /* DisposeBagContainer.swift in Sources */, A5B3CBFE2B6274B800F95AC3 /* BaseDistanceValueStringProvider.swift in Sources */, A5F29BD62A2623E500ED700A /* UserIconRepository.swift in Sources */, A5AD70252C012F5300A36318 /* WindowControls.swift in Sources */, + A5D712632C37EB4900A8EF52 /* LockScreenVM.swift in Sources */, A56D5ACD2A4C304C004F45DA /* SingleCall.swift in Sources */, A5A14A442B6143AC004B1598 /* SuplaChannelGeneralPurposeMeterConfig.swift in Sources */, AED6395E270CA3A800E5105B /* RoundedButton.swift in Sources */, @@ -6441,6 +6758,7 @@ A55A8D802BA9AD8000C540D4 /* RollerShutterView.swift in Sources */, 015A9846265595E300B6E6C6 /* UIButton+SUPLA.m in Sources */, A5F29BF12A28AE0B00ED700A /* NotificationView.swift in Sources */, + A5F5C3F42C3682500058E255 /* PinSetupVM.swift in Sources */, A5A14A382B61220C004B1598 /* GetChannelBaseCaptionUseCase.swift in Sources */, AE74528D2827E5D400A3AFAD /* ProfileChooser.swift in Sources */, 6E9F28C928436EE900142502 /* AddNewProfileCell.swift in Sources */, @@ -6452,7 +6770,6 @@ A55A8DA62BAC4F5500C540D4 /* AuthorizeUseCase.swift in Sources */, A5F29B982A20B9D200ED700A /* ChannelRepository.swift in Sources */, A57668EF2AEA9F150025509D /* DownloadTemperatureLogUseCase.swift in Sources */, - A5756F8429DC23A800C32A1B /* AccountRemovalNavigationCoordinator.swift in Sources */, 016157B6261CAAFA006AA0E8 /* SAPickerField.m in Sources */, 01E517C322C6AC1E000FE77A /* SADownloadIncrementalMeasurements.m in Sources */, 013D1C6022CE757F000C0784 /* SAIncrementalMeterChartHelper.m in Sources */, @@ -6502,16 +6819,17 @@ A58316FE2B6452AB006113F8 /* ThermometerAndHumidityValueProvider.swift in Sources */, A5B3CBF62B625E2400F95AC3 /* HumidityValueProvider.swift in Sources */, A50E5D732BFF543500303BAE /* CurtainVM.swift in Sources */, + A58472042C2EACA600713D36 /* SuplaResultCode.swift in Sources */, A56F15F12A2E698400C2E21B /* DeleteEntitiesMigrationPolicy.swift in Sources */, A56233E32AB1903C001CB948 /* Float+Ext.swift in Sources */, A5477DB62AA1F17600220B4A /* DeleteRemovableChannelRelationsUseCase.swift in Sources */, A55A8DB72BAC766A00C540D4 /* SALabeledTextField.swift in Sources */, A52BFEDE2B0F906900A2F64C /* ThermostatTimerDetailVC.swift in Sources */, + A5E9CE332C342ADC00509702 /* ReconnectUseCase.swift in Sources */, 403150FB1DC769430075D2D2 /* RGBWDetailView.m in Sources */, 01F8856A22E5E79100D18373 /* SATempHumidityMeasurementItem+CoreDataProperties.m in Sources */, 01478826265317B000CC8A01 /* SACalCfgProgressReport.m in Sources */, A5E40B4C2B84EA9300DB6ABE /* ApplicationEventsManager.swift in Sources */, - A57785BF29E80406001C631E /* SuplaAppWrapper.swift in Sources */, A5E490502A3C79A4006801FE /* UserNotificationCenter.swift in Sources */, A55A8D702BA831D900C540D4 /* WindowDetailVM.swift in Sources */, A50B5D3D2BFB6EA200918D18 /* ProjectorScreenView.swift in Sources */, @@ -6521,6 +6839,7 @@ A530EE062A5575D700F8DAEE /* DeviceStateView.swift in Sources */, 40E8BE881FC06A5600FB2FE6 /* TFHpple.m in Sources */, A5E4905F2A401020006801FE /* SAUserIcon+Ext.swift in Sources */, + A5D7126C2C380D7700A8EF52 /* CheckPinUseCase.swift in Sources */, A55501F92B838E7300FD3296 /* NotificationRepository.swift in Sources */, A5B3CBE92B625CB300F95AC3 /* DepthValueProvider.swift in Sources */, A57C4AB32AAF374900D9C695 /* EditProgramDialogVC.swift in Sources */, @@ -6536,6 +6855,7 @@ 40C7BA5320C007A300ACEE42 /* SAChannelValue+CoreDataProperties.m in Sources */, A5F29BAA2A24B23000ED700A /* UpdateSceneUseCase.swift in Sources */, A503ABA92B679C66008CDA1F /* BarChartData.swift in Sources */, + A5A160092C2AB42F0049AA73 /* SuplaCore+BaseViewController.swift in Sources */, A57668DF2AE99CCD0025509D /* LoadChannelWithChildrenMeasurementsUseCase.swift in Sources */, A56233F32AB3A386001CB948 /* CreateTemperaturesListUseCase.swift in Sources */, A50B5CFB2BEA144A00918D18 /* HeatpolThermostatValue.swift in Sources */, @@ -6544,15 +6864,15 @@ A52BFEEC2B173F7100A2F64C /* ReadProfileByIdUseCase.swift in Sources */, A530EE242A57006A00F8DAEE /* LiquidSensorIconNameProducer.swift in Sources */, A55A8D912BAB13B300C540D4 /* RollerShutterValue.swift in Sources */, - A530EDFD2A54153000F8DAEE /* SwitchDetailNavigationCoordinator.swift in Sources */, AEBCD8FC26E4D247001904F3 /* TemperaturePresenter.swift in Sources */, A5AD70212C00B6AF00A36318 /* WindowVerticalControls.swift in Sources */, A51BE91B2AAB0DF900718F2F /* SuplaConfigIntegrator.m in Sources */, A503ABBC2B6BB7FF008CDA1F /* ThermometerValueFormatter.swift in Sources */, - 407D4AFF1BC6E491009A5505 /* StatusVC.m in Sources */, + A5D7125F2C37EA8300A8EF52 /* LockScreenFeature.swift in Sources */, A5B3CBE52B62546800F95AC3 /* DoubleValueParser.swift in Sources */, A5E4904A2A3C6215006801FE /* ChannelHeight.swift in Sources */, A57668F72AEAA8870025509D /* ChartParameters.swift in Sources */, + A5A15FF92C299D230049AA73 /* Color+Supla.swift in Sources */, A50B5D222BEE167300918D18 /* DeleteChannelMeasurementsUseCase.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6566,12 +6886,15 @@ A5519B362B6D358300811747 /* RainValueProviderTest.swift in Sources */, A50B5D0F2BECB61900918D18 /* ExecuteFacadeBlindActionUseCaseTests.swift in Sources */, A59AB8DF2A309BBE00D91F1F /* CreateProfileGroupsListUseCaseTests.swift in Sources */, + A5F5C3E42C3545F90058E255 /* Observable+Test.swift in Sources */, A54A064F2AF3ED6200C03DBC /* RequestHelperTests.swift in Sources */, A54A06822AF9124B00C03DBC /* LoadChannelMeasurementsUseCaseTests.swift in Sources */, + A5D7127A2C3E8F8500A8EF52 /* InitializationUseCaseTests.swift in Sources */, A54A06712AF8CD4A00C03DBC /* DaysRangeTests.swift in Sources */, A5519B2E2B6D1D9000811747 /* WindValueProviderTest.swift in Sources */, A5E40B772B873D2100DB6ABE /* SAGeneralPurposeMeasurement+Mock.swift in Sources */, A59AB8D32A30929F00D91F1F /* TemperatureMeasurementItemRepositoryMock.swift in Sources */, + A5D712752C3E708900A8EF52 /* CheckPinUseCaseTests.swift in Sources */, A55A8DBF2BB2EED300C540D4 /* CompletableTestCase.swift in Sources */, A57638CE29E5EFDA003E15A3 /* GlobalSettingsMock.swift in Sources */, A59AB8CD2A30916F00D91F1F /* ImpulseCounterMeasurementItemRepositoryMock.swift in Sources */, @@ -6598,11 +6921,11 @@ A54A068E2AFB6F9200C03DBC /* LoadChannelWithChildrenMeasurementsUseCaseTests.swift in Sources */, A59AB8E82A30A81200D91F1F /* SwapChannelPositionsUseCaseTests.swift in Sources */, A530EE332A57E42100F8DAEE /* GetDefaultIconNameUseCaseTests.swift in Sources */, - A57785C329E80CF7001C631E /* SuplaAppWrapperMock.swift in Sources */, A59AB8CB2A30911A00D91F1F /* ElectricityMeasurementItemRepositoryMock.swift in Sources */, A5ABE5C92ABCD76300FFA50B /* ThermostatUseCasesMocks.swift in Sources */, A54A06582AF3FDCA00C03DBC /* ObservableTestCase.swift in Sources */, A59AB8BF2A30820900D91F1F /* ChannelRepositoryMock.swift in Sources */, + A5D712852C3EA3A700A8EF52 /* SuplaAppCoordinatorMock.swift in Sources */, A5E40B592B85FF8A00DB6ABE /* GetChannelBaseCaptionUseCaseTests.swift in Sources */, A50CD3D62A4D99E60012DD9B /* UpdateTokenTaskTests.swift in Sources */, A54A06772AF8E1B200C03DBC /* DownloadChannelMeasurementsUseCaseTests.swift in Sources */, @@ -6621,6 +6944,8 @@ A5ABE5C02ABC774C00FFA50B /* EditProgramDialogVMTests.swift in Sources */, A50B5D0D2BECA59000918D18 /* FacadeBlindValueTests.swift in Sources */, A54A065B2AF4E3C300C03DBC /* SAOAuthToken+Mock.swift in Sources */, + A5D712812C3EA2E300A8EF52 /* LockScreenVMTests.swift in Sources */, + A5F5C3EF2C358AF90058E255 /* LockScreenSettingsTests.swift in Sources */, A59AB8B52A30795B00D91F1F /* SceneUseCasesMocks.swift in Sources */, A5DA31072AC16B21008179DB /* InsertChannelRelationForProfileUseCaseTests.swift in Sources */, A5AD702C2C04773F00A36318 /* CurtainVMTests.swift in Sources */, @@ -6628,6 +6953,7 @@ A5074BA42BC90C800081B6B1 /* LoadActiveProfileUrlUseCaseTests.swift in Sources */, A5EC52442C0F33990022F055 /* RequestChannelConfigUseCaseTests.swift in Sources */, A57638C629E5D4C9003E15A3 /* XCTestCaseExtensions.swift in Sources */, + A5D712722C3E6FD700A8EF52 /* LockUseCasesMocks.swift in Sources */, A59AB8EB2A30AA2600D91F1F /* CreateProfileScenesListUseCaseTests.swift in Sources */, A55A8DC32BB2F4E000C540D4 /* NotificationCenterWrapperMock.swift in Sources */, 016ABE18236DA795001BF5FB /* KeychainTests.m in Sources */, @@ -6635,6 +6961,7 @@ A59AB8C72A30902D00D91F1F /* ChannelValueRepositoryMock.swift in Sources */, A5519B3C2B6D36BD00811747 /* GpmValueProviderTest.swift in Sources */, AEBCD8FE26E4D52C001904F3 /* TemperatureUnitTests.swift in Sources */, + A5F5C3E62C35501D0058E255 /* SuplaAppProviderMock.swift in Sources */, A54A06612AF50E2200C03DBC /* SuplaCloudServiceTests.swift in Sources */, A5ABE5CF2ABD696900FFA50B /* DataMocks.swift in Sources */, A59AB8F02A30ACB500D91F1F /* ToggleLocationUseCaseTests.swift in Sources */, @@ -6687,8 +7014,10 @@ A5ABE5C72ABCD52500FFA50B /* ThermostatGeneralVMTests.swift in Sources */, A59AB8C32A308AF700D91F1F /* ProvideDetailTypeUseCaseTests.swift in Sources */, A54A06692AF527DB00C03DBC /* UserStateHolderMock.swift in Sources */, + A5D7127E2C3E9D6D00A8EF52 /* DatabaseProxyMock.swift in Sources */, A59AB8B82A307F3200D91F1F /* MainVMTests.swift in Sources */, A58B2CB62AC197E300764388 /* ChannelBaseUseCasesMocks.swift in Sources */, + A5E9CE392C35385700509702 /* SuplaAppStateHolderMock.swift in Sources */, A59AB8E12A309C1500D91F1F /* UseCaseTest.swift in Sources */, A573B09E2A5FEF57001E19D0 /* SwitchGeneralVMTests.swift in Sources */, A5A23C312ABDBCAC00233542 /* CreateChannelWithChildrenUseCaseTests.swift in Sources */, @@ -6708,10 +7037,12 @@ A54A066C2AF5290900C03DBC /* DownloadEventsManagerMock.swift in Sources */, A5A72E432AC1787200405C41 /* ExecuteThermostatActionUseCaseTests.swift in Sources */, A5ABE5CB2ABCD80300FFA50B /* DelayedThermostatActionSubjectMock.swift in Sources */, + A5D7128B2C3EB58D00A8EF52 /* StatusVMTests.swift in Sources */, A5EC52422C0F2E280022F055 /* SuplaChannelFacadeBlindConfig+Mock.swift in Sources */, A5519B3A2B6D362A00811747 /* HumidityValueProviderTest.swift in Sources */, A55A8DD12BB4411400C540D4 /* SAAuthorizationDialogVMTests.swift in Sources */, A58D98C82AC1A3AD00DCB526 /* SetChannelConfigUseCaseTests.swift in Sources */, + A5D712882C3EB24900A8EF52 /* PinSetupVMTests.swift in Sources */, A5E40B5B2B8606F400DB6ABE /* GetChannelValueStringUseCaseTests.swift in Sources */, A52BFEFA2B18A43000A2F64C /* DeleteProfileUseCaseTests.swift in Sources */, A59AB8A62A305D8700D91F1F /* LocationUseCasesMocks.swift in Sources */, @@ -6761,6 +7092,7 @@ A54A068A2AF925FB00C03DBC /* ProfileUseCasesMocks.swift in Sources */, A59AB8A02A30535E00D91F1F /* ChannelUseCasesMocks.swift in Sources */, A573B0B12A602F5F001E19D0 /* ExecuteSimpleActionUseCaseTests.swift in Sources */, + A5D712772C3E769300A8EF52 /* SingleTestCase.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SUPLA/AddWizardVC.m b/SUPLA/AddWizardVC.m index 3ed953e50..dbf39780b 100644 --- a/SUPLA/AddWizardVC.m +++ b/SUPLA/AddWizardVC.m @@ -784,8 +784,7 @@ - (IBAction)cancelOrBackTouch:(nullable id)sender { [self cleanUp]; [self.OpQueue cancelAllOperations]; [self savePrefs]; - [[SAApp currentNavigationCoordinator] finish]; - [[SAApp SuplaClient] reconnect]; + [SuplaAppCoordinatorLegacyWrapper dismissWithAnimated: true]; } diff --git a/SUPLA/AppDelegate.m b/SUPLA/AppDelegate.m deleted file mode 100644 index 14d1b93dd..000000000 --- a/SUPLA/AppDelegate.m +++ /dev/null @@ -1,159 +0,0 @@ -/* - Copyright (C) AC SOFTWARE SP. Z O.O. - - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ - -#import "AppDelegate.h" -#import "SuplaApp.h" -#import "SUPLA-Swift.h" -#import "SADialog.h" - -@interface AppDelegate () - -@end - -@implementation AppDelegate - -- (id) init { - self = [super init]; - - // Setup dependency injection - [DiContainer start]; - - return self; -} - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [SALogWrapper setup]; - -#ifdef DEBUG - // Short-circuit starting app if running unit tests - BOOL isInTest = NSProcessInfo.processInfo.environment[@"XCTestConfigurationFilePath"] != nil; - if (isInTest) { - return YES; - } -#endif - - // Override point for customization after application launch. - self.navigation = [[MainNavigationCoordinator alloc] init]; - self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; - self.window.overrideUserInterfaceStyle = [GlobalSettingsLegacy new].darkMode; - [self.navigation attachTo: self.window]; - - [CoreDataManager.shared setupWithCompletion: ^() { - [self.navigation startFrom:nil]; - - // Start SuplaClient only after the status window is displayed. - // Otherwise - with empty settings, the user will see the message "Host not found" - // instead of the settings window. - [SAApp SuplaClient]; - }]; - - [self registerForNotifications]; - return YES; -} - -- (void)applicationWillResignActive:(UIApplication *)application { - id pc = SAApp.currentNavigationCoordinator.viewController.presentedViewController; - if([pc isKindOfClass: [SADialog class]]) { - [((SADialog*)pc) close]; - } -} - -- (void)applicationDidEnterBackground:(UIApplication *)application { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - if ( ![SAApp.currentNavigationCoordinator.viewController isKindOfClass: [SAAddWizardVC class]] ) { - [SAApp SuplaClientWaitForTerminate]; - } - -} - -- (void)applicationWillEnterForeground:(UIApplication *)application { - // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. -} - -- (void)applicationDidBecomeActive:(UIApplication *)application { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. -#ifdef DEBUG - // Short-circuit starting app if running unit tests - BOOL isInTest = NSProcessInfo.processInfo.environment[@"XCTestConfigurationFilePath"] != nil; - if (isInTest) { - return; - } -#endif - - id vc = [SAApp currentNavigationCoordinator].viewController; - if ( ![vc isKindOfClass: [SAAddWizardVC class]] ) { - // TODO: such checks should be solved in a generic way by coordintators - [SAApp SuplaClient]; - } - -} - -- (void)applicationWillTerminate:(UIApplication *)application { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - // Saves changes in the application's managed object context before the application terminates. -} - -#pragma mark Notifications - -- (void) registerForNotifications { - [UNUserNotificationCenter currentNotificationCenter].delegate = self; - [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions: (UNAuthorizationOptionAlert + UNAuthorizationOptionSound) completionHandler: ^(BOOL granted, NSError * _Nullable error) { - - if (granted) { - dispatch_async(dispatch_get_main_queue(), ^{ - [UIApplication.sharedApplication registerForRemoteNotifications]; - }); - } else { - NSLog(@"Notifications not allowed %@", error); - [DiContainer setPushTokenWithToken: nil]; - } - }]; -} - -- (void) application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { -#ifdef DEBUG - const char *data = [deviceToken bytes]; - NSMutableString *token = [NSMutableString string]; - for (NSUInteger i = 0; i < [deviceToken length]; i++) { - [token appendFormat:@"%02.2hhx", data[i]]; - } - NSLog(@"Push token: %@", token); -#endif - - [DiContainer setPushTokenWithToken: deviceToken]; - - [[[UpdateTokenTask alloc] init] updateWithToken: deviceToken completionHandler: ^{ - NSLog(@"Token update task finished"); - }]; -} - -- (void) application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { - NSLog(@"Failed to register for remote notifications with error %@", error); -} - -- (void) application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler { - [UseCaseLegacyWrapper insertNotification: userInfo]; - completionHandler(UIBackgroundFetchResultNewData); -} - -- (void) userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { - completionHandler(UNNotificationPresentationOptionAlert); -} - -@end diff --git a/SUPLA/AppDelegate.swift b/SUPLA/AppDelegate.swift index 0f51ac4b5..68ec5e83a 100644 --- a/SUPLA/AppDelegate.swift +++ b/SUPLA/AppDelegate.swift @@ -16,6 +16,104 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ - import Foundation + +class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { + + var window: UIWindow? = nil + + @Singleton private var settings: GlobalSettings + @Singleton private var insertNotificationUseCase: InsertNotificationUseCase + @Singleton private var coordinator: SuplaAppCoordinator + @Singleton private var suplaAppStateHolder: SuplaAppStateHolder + @Singleton private var disconnectUseCase: DisconnectUseCase + + private var clientStopWork: DispatchWorkItem? = nil + + override init() { + SALogWrapper.setup() + DiContainer.start() + } + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + #if DEBUG + // Short-circuit starting app if running unit tests + if (ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil) { + return true + } + #endif + + window = UIWindow(frame: UIScreen.main.bounds) + if let window = window { + window.overrideUserInterfaceStyle = settings.darkMode.interfaceStyle + coordinator.attachToWindow(window) + coordinator.start(animated: true) + } + + registerForNotifications() + + DispatchQueue.global(qos: .userInitiated).async { + InitializationUseCase.invoke() + } + + return true + } + + func applicationDidEnterBackground(_ application: UIApplication) { + disconnectUseCase.invokeSynchronous() + suplaAppStateHolder.handle(event: .finish(reason: .appInBackground)) + } + + func applicationDidBecomeActive(_ application: UIApplication) { + #if DEBUG + if (ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil) { + return + } + #endif + + suplaAppStateHolder.handle(event: .onStart) + } + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + #if DEBUG + let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() + SALog.debug("Push token: \(token)") + #endif + var settings = settings + + settings.pushToken = deviceToken + UpdateTokenTask().update(token: deviceToken) { SALog.info("Token update task finished") } + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) { + SALog.error("Failed to register for remote notifications with error \(error)") + } + + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + do { + try insertNotificationUseCase.invoke(userInfo: userInfo).subscribeSynchronous() + } catch { + SALog.error("Could not insert notification: \(String(describing: error))") + } + completionHandler(.newData) + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler(.alert) + } + + private func registerForNotifications() { + UNUserNotificationCenter.current().delegate = self + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { [weak self] (granted, error) in + + if (granted) { + DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } + } else { + SALog.error("Notifications not allowed \(String(describing: error))") + var settings = self?.settings + settings?.pushToken = nil + } + } + } +} diff --git a/SUPLA/Core/Config/GlobalSettings.swift b/SUPLA/Core/Config/GlobalSettings.swift index 6acb60736..7ca83793a 100644 --- a/SUPLA/Core/Config/GlobalSettings.swift +++ b/SUPLA/Core/Config/GlobalSettings.swift @@ -34,6 +34,7 @@ protocol GlobalSettings { var channelHeight: ChannelHeight { get set } var showOpeningPercent: Bool { get set } var darkMode: DarkModeSetting { get set } + var lockScreenSettings: LockScreenSettings { get set } } class GlobalSettingsImpl: GlobalSettings { @@ -156,6 +157,12 @@ class GlobalSettingsImpl: GlobalSettings { get { return DarkModeSetting.from(defaults.integer(forKey: darkModeKey)) } set { defaults.set(newValue.rawValue, forKey: darkModeKey) } } + + private let lockScreenKey = "supla_config_lock_screen" + var lockScreenSettings: LockScreenSettings { + get { return LockScreenSettings.from(string: defaults.string(forKey: lockScreenKey)) } + set { defaults.set(newValue.asString(), forKey: lockScreenKey) } + } } @objc class GlobalSettingsLegacy: NSObject { diff --git a/SUPLA/Core/DI/DiContainer.swift b/SUPLA/Core/DI/DiContainer.swift index 8775c35f8..e19d712d9 100644 --- a/SUPLA/Core/DI/DiContainer.swift +++ b/SUPLA/Core/DI/DiContainer.swift @@ -59,10 +59,11 @@ extension DiContainer { @objc static func start() { // MARK: General + register(SuplaAppCoordinator.self, SuplaAppCoordinatorImpl()) register(GlobalSettings.self, GlobalSettingsImpl()) register(RuntimeConfig.self, RuntimeConfigImpl()) register(SuplaClientProvider.self, SuplaClientProviderImpl()) - register(SuplaAppWrapper.self, SuplaAppWrapperImpl()) + register(SuplaAppProvider.self, SuplaAppProviderImpl()) register(VibrationService.self, VibrationServiceImpl()) register(SingleCall.self, SingleCallImpl()) register(DateProvider.self, DateProviderImpl()) @@ -77,6 +78,8 @@ extension DiContainer { register(SessionResponseProvider.self, SessionResponseProviderImpl()) register(SuplaSchedulers.self, SuplaSchedulersImpl()) register(ThreadHandler.self, ThreadHandlerImpl()) + register(SuplaAppStateHolder.self, SuplaAppStateHolderImpl()) + register(DatabaseProxy.self, DatabaseProxyImpl()) // Managers register(UpdateEventsManager.self, UpdateEventsManagerImpl()) register(ChannelConfigEventsManager.self, ChannelConfigEventsManagerImpl()) @@ -167,7 +170,10 @@ extension DiContainer { register(CallSuplaClientOperationUseCase.self, CallSuplaClientOperationUseCaseImpl()) register(ExecuteRollerShutterActionUseCase.self, ExecuteRollerShutterActionUseCaseImpl()) register(AuthorizeUseCase.self, AuthorizeUseCaseImpl()) + register(LoginUseCase.self, LoginUseCaseImpl()) register(ExecuteFacadeBlindActionUseCase.self, ExecuteFacadeBlindActionUseCaseImpl()) + register(DisconnectUseCase.self, DisconnectUseCaseImpl()) + register(ReconnectUseCase.self, ReconnectUseCaseImpl()) // Usecases - Detail register(ProvideDetailTypeUseCase.self, ProvideDetailTypeUseCaseImpl()) // Usecases - Group @@ -196,6 +202,8 @@ extension DiContainer { // Usecases - Notification register(InsertNotificationUseCase.self, InsertNotificationUseCaseImpl()) register(NotificationCenterWrapper.self, NotificationCenterWrapperImpl()) + // Usecases - Lock + register(CheckPinUseCase.self, CheckPinUseCaseImpl()) // MARK: Not singletons @@ -218,11 +226,6 @@ extension DiContainer { return DiContainer.shared.resolve(type: DeviceConfigEventsManager.self) } - @objc static func setPushToken(token: Data?) { - var settings = DiContainer.shared.resolve(type: GlobalSettings.self) - settings?.pushToken = token - } - @objc static func getPushToken() -> Data? { DiContainer.shared.resolve(type: GlobalSettings.self)?.pushToken } diff --git a/SUPLA/Core/Events/ListsEventsManager.swift b/SUPLA/Core/Events/ListsEventsManager.swift index bb3fa9cf9..79d514b26 100644 --- a/SUPLA/Core/Events/ListsEventsManager.swift +++ b/SUPLA/Core/Events/ListsEventsManager.swift @@ -37,6 +37,8 @@ protocol UpdateEventsManager: UpdateEventsManagerEmitter { func observeChannelsUpdate() -> Observable func observeGroupsUpdate() -> Observable func observeScenesUpdate() -> Observable + + func cleanup() } final class UpdateEventsManagerImpl: UpdateEventsManager { @@ -127,6 +129,10 @@ final class UpdateEventsManagerImpl: UpdateEventsManager { func observeScenesUpdate() -> Observable { sceneUpdatesSubject.asObservable() } + func cleanup() { + subjects.removeAll() + } + private func getSubjectForScene(sceneId: Int) -> BehaviorRelay { return syncedQueue.sync(execute: { getSubject(id: sceneId, type: .scene) diff --git a/SUPLA/Features/NotificationsLog/NotificationsLogNavigationCoordinator.swift b/SUPLA/Core/Extensions/Completable+Supla.swift similarity index 76% rename from SUPLA/Features/NotificationsLog/NotificationsLogNavigationCoordinator.swift rename to SUPLA/Core/Extensions/Completable+Supla.swift index 4cf9e4d34..baa62d018 100644 --- a/SUPLA/Features/NotificationsLog/NotificationsLogNavigationCoordinator.swift +++ b/SUPLA/Core/Extensions/Completable+Supla.swift @@ -1,3 +1,4 @@ +// /* Copyright (C) AC SOFTWARE SP. Z O.O. @@ -16,10 +17,13 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -class NotificationsLogNavigationCoordinator: BaseNavigationCoordinator { - override var viewController: UIViewController { - _viewController - } +import RxSwift - private lazy var _viewController: NotificationsLogVC = .init(navigator: self) +extension Completable { + static func complete() -> Completable { + Completable.create { completable in + completable(.completed) + return Disposables.create() + } + } } diff --git a/SUPLA/Core/Extensions/ObservableType+Ext.swift b/SUPLA/Core/Extensions/ObservableType+Ext.swift index 8e48ec35b..a08824e07 100644 --- a/SUPLA/Core/Extensions/ObservableType+Ext.swift +++ b/SUPLA/Core/Extensions/ObservableType+Ext.swift @@ -64,6 +64,13 @@ extension Observable { return element } } + + func flatMapCompletable(_ selector: @escaping (Element) throws -> Completable) -> Completable { + flatMap { + try selector($0).asObservable() + } + .asCompletable() + } } final class SynchronousSubscriber { diff --git a/SUPLA/Core/Extensions/String+Ext.swift b/SUPLA/Core/Extensions/String+Ext.swift index c3c353de8..6b9477745 100644 --- a/SUPLA/Core/Extensions/String+Ext.swift +++ b/SUPLA/Core/Extensions/String+Ext.swift @@ -16,6 +16,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +import CommonCrypto import Foundation extension String { @@ -51,11 +52,17 @@ extension String { withCString { _ = snprintf(ptr: pointer, lenght, $0) } - } } } + func sha1() -> String { + let data = Data(self.utf8) + var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH)) + data.withUnsafeBytes { _ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest) } + return digest.map { String(format: "%02hhx", $0) }.joined() + } + static func fromC(_ address: T) -> String { return withUnsafePointer(to: address) { $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) { diff --git a/SUPLA/Core/Extensions/Synchronization.swift b/SUPLA/Core/Extensions/Synchronization.swift new file mode 100644 index 000000000..bc6dc6fb2 --- /dev/null +++ b/SUPLA/Core/Extensions/Synchronization.swift @@ -0,0 +1,25 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + + +func synced(_ lock: Any, closure: () -> ()) { + objc_sync_enter(lock) + defer { objc_sync_exit(lock) } + closure() +} diff --git a/SUPLA/Core/Extensions/UINavigationController+Ext.swift b/SUPLA/Core/Extensions/UINavigationController+Ext.swift new file mode 100644 index 000000000..b8dae405f --- /dev/null +++ b/SUPLA/Core/Extensions/UINavigationController+Ext.swift @@ -0,0 +1,26 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +extension UINavigationController { + func popToViewController(ofClass: AnyClass, animated: Bool = true) { + if let viewController = viewControllers.last(where: { $0.isKind(of: ofClass) }) { + popToViewController(viewController, animated: animated) + } + } +} diff --git a/SUPLA/Core/Extensions/UIViewController+SuplaNavBar.swift b/SUPLA/Core/Extensions/UIViewController+SuplaNavBar.swift new file mode 100644 index 000000000..751346590 --- /dev/null +++ b/SUPLA/Core/Extensions/UIViewController+SuplaNavBar.swift @@ -0,0 +1,41 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + + +import Foundation + +extension UIViewController { + func setupToolbar(toolbarFont: UIFont = .suplaSubtitleFont) { + let appearance = UINavigationBarAppearance() + appearance.backgroundColor = .toolbar + appearance.titleTextAttributes = [ + .foregroundColor: UIColor.onPrimary, + .font: toolbarFont + ] + + navigationController?.navigationBar.standardAppearance = appearance + navigationController?.navigationBar.compactAppearance = appearance + navigationController?.navigationBar.scrollEdgeAppearance = appearance + navigationController?.navigationBar.tintColor = .onPrimary + + if #available(iOS 14.0, *) { + navigationController?.navigationBar.topItem?.backButtonDisplayMode = .minimal + } + } +} diff --git a/SUPLA/AppDelegate.h b/SUPLA/Core/Infrastructure/DatabaseProxy.swift similarity index 70% rename from SUPLA/AppDelegate.h rename to SUPLA/Core/Infrastructure/DatabaseProxy.swift index 7c29225f7..ad4056724 100644 --- a/SUPLA/AppDelegate.h +++ b/SUPLA/Core/Infrastructure/DatabaseProxy.swift @@ -1,29 +1,29 @@ +// /* Copyright (C) AC SOFTWARE SP. Z O.O. - + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. - + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - + You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ + -#import - -@import UserNotifications; -@protocol NavigationCoordinator; - -@interface AppDelegate : UIResponder - -@property (strong, nonatomic) UIWindow *window; -@property (strong, nonatomic) id navigation; -@end +protocol DatabaseProxy { + func setup() +} +final class DatabaseProxyImpl: DatabaseProxy { + func setup() { + CoreDataManager.shared.setup() + } +} diff --git a/SUPLA/Core/Infrastructure/ThreadHandler.swift b/SUPLA/Core/Infrastructure/ThreadHandler.swift index c309b9dba..316b8a03f 100644 --- a/SUPLA/Core/Infrastructure/ThreadHandler.swift +++ b/SUPLA/Core/Infrastructure/ThreadHandler.swift @@ -18,11 +18,15 @@ protocol ThreadHandler { func sleep(_ timeInterval: TimeInterval) + func usleepProxy(_ microseconds: UInt32) } final class ThreadHandlerImpl: ThreadHandler { - func sleep(_ timeInterval: TimeInterval) { Thread.sleep(forTimeInterval: timeInterval) } + + func usleepProxy(_ microseconds: UInt32) { + usleep(microseconds) + } } diff --git a/SUPLA/Core/Navigation/Coordinator.swift b/SUPLA/Core/Navigation/Coordinator.swift new file mode 100644 index 000000000..e5e1201d1 --- /dev/null +++ b/SUPLA/Core/Navigation/Coordinator.swift @@ -0,0 +1,46 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +protocol Coordinator: AnyObject { + var navigationController: UINavigationController { get set } + + func start(animated: Bool) +} + +extension Coordinator { + func navigateTo(_ view: UIViewController, animated: Bool = true) { + navigationController.pushViewController(view, animated: animated) + } + + func popViewController(animated: Bool = true) { + navigationController.popViewController(animated: animated) + } + + func popToViewController(ofClass: AnyClass, animated: Bool = true) { + navigationController.popToViewController(ofClass: ofClass, animated: animated) + } + + func present(_ view: UIViewController, animated: Bool = false) { + navigationController.present(view, animated: animated) + } + + func dismiss(animated: Bool = false) { + navigationController.dismiss(animated: animated) + } +} diff --git a/SUPLA/Core/Navigation/SAAddWizardVC+NavigationSubcontroller.swift b/SUPLA/Core/Navigation/SAAddWizardVC+NavigationSubcontroller.swift new file mode 100644 index 000000000..3112a0638 --- /dev/null +++ b/SUPLA/Core/Navigation/SAAddWizardVC+NavigationSubcontroller.swift @@ -0,0 +1,23 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + + +extension SAAddWizardVC: NavigationSubcontroller { + func screenTakeoverAllowed() -> Bool { false } +} diff --git a/SUPLA/Core/Navigation/SuplaAppCoordinator.swift b/SUPLA/Core/Navigation/SuplaAppCoordinator.swift new file mode 100644 index 000000000..56be7e2f6 --- /dev/null +++ b/SUPLA/Core/Navigation/SuplaAppCoordinator.swift @@ -0,0 +1,316 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import RxSwift + +protocol SuplaAppCoordinator: Coordinator { + func attachToWindow(_ window: UIWindow) + func currentController() -> UIViewController? + func navigateToMain() + func navigateToSettings() + func navigateToLocationOrdering() + func navigateToProfiles() + func navigateToAddWizard() + func navigateToAbout() + func navigateToNotificationsLog() + func navigateToDeviceCatalog() + func navigateToProfile(profileId: ProfileID?) + func navigateToProfile(profileId: ProfileID?, withLockCheck: Bool) + func navigateToCreateAccountWeb() + func navigateToRemoveAccountWeb(needsRestart: Bool, serverAddress: String?) + func navigateToLegacyDetail(_ detailType: LegacyDetailType, channelBase: SAChannelBase) + func navigateToSwitchDetail(item: ItemBundle, pages: [DetailPage]) + func navigateToThermostatDetail(item: ItemBundle, pages: [DetailPage]) + func navigateToThermometerDetail(item: ItemBundle, pages: [DetailPage]) + func navigateToGpmDetail(item: ItemBundle, pages: [DetailPage]) + func navigateToWindowDetail(item: ItemBundle, pages: [DetailPage]) + func navigateToPinSetup(lockScreenScope: LockScreenScope) + func navigateToLockScreen(unlockAction: LockScreenFeature.UnlockAction) + + func popToStatus() + + func showMenu() + func showAuthorization() + func showLogin() + + func openForum() + func openCloud() + func openUrl(url: String) + func openUrl(url: URL) +} + +protocol NavigationSubcontroller { + func screenTakeoverAllowed() -> Bool +} + +final class SuplaAppCoordinatorImpl: NSObject, SuplaAppCoordinator { + @Singleton private var stateHolder + @Singleton private var schedulers + @Singleton private var settings + + private var stateDisposable: Disposable? = nil + + lazy var navigationController: UINavigationController = { + let controller = SuplaAppNavigationController() + return controller + }() + + func attachToWindow(_ window: UIWindow) { + window.rootViewController = navigationController + window.makeKeyAndVisible() + } + + func start(animated: Bool = false) { + stateDisposable = stateHolder.state() + .subscribe(on: schedulers.background) + .observe(on: schedulers.main) + .subscribe(onNext: { + switch ($0) { + case .initialization, .connecting(_), .finished: + self.navigateToStatusView() + default: + break + } + }) + } + + func currentController() -> UIViewController? { + navigationController.viewControllers.last + } + + func navigateToMain() { + navigateTo(MainVC()) + } + + func navigateToSettings() { + navigateTo(AppSettingsVC()) + } + + func navigateToLocationOrdering() { + let viewController = LocationOrderingVC() + viewController.bind(viewModel: LocationOrderingVM()) + navigateTo(viewController) + } + + func navigateToProfiles() { + let profiles = ProfilesVC() + profiles.bind(viewModel: ProfilesVM(profileManager: SAApp.profileManager())) + navigateTo(profiles) + } + + func navigateToAddWizard() { + let avc = SAAddWizardVC(nibName: "AddWizardVC", bundle: nil) + avc.modalPresentationStyle = .fullScreen + avc.modalTransitionStyle = .crossDissolve + present(avc, animated: true) + } + + func navigateToAbout() { + navigateTo(SAAboutVC(nibName: "AboutVC", bundle: nil)) + } + + func navigateToNotificationsLog() { + navigateTo(NotificationsLogVC()) + } + + func navigateToDeviceCatalog() { + navigateTo(DeviceCatalogVC()) + } + + func navigateToProfile(profileId: ProfileID?) { + navigateToProfile(profileId: profileId, withLockCheck: true) + } + + func navigateToProfile(profileId: ProfileID?, withLockCheck: Bool) { + if (withLockCheck && settings.lockScreenSettings.pinForAccountsRequired) { + if let profileId = profileId { + navigateToLockScreen(unlockAction: .authorizeAccountsEdit(profileId: profileId)) + } else { + navigateToLockScreen(unlockAction: .authorizeAccountsCreate) + } + } else { + navigateTo(AccountCreationVC(profileId: profileId)) + } + } + + func navigateToCreateAccountWeb() { + navigateTo(SACreateAccountVC(nibName: "CreateAccountVC", bundle: nil)) + } + + func navigateToRemoveAccountWeb(needsRestart: Bool, serverAddress: String?) { + navigateTo(AccountRemovalVC(needsRestart: needsRestart, serverAddress: serverAddress)) + } + + func navigateToLegacyDetail(_ detailType: LegacyDetailType, channelBase: SAChannelBase) { + navigateTo(DetailViewController(detailViewType: detailType, channelBase: channelBase)) + } + + func navigateToSwitchDetail(item: ItemBundle, pages: [DetailPage]) { + navigateTo(SwitchDetailVC(item: item, pages: pages)) + } + + func navigateToThermostatDetail(item: ItemBundle, pages: [DetailPage]) { + navigateTo(ThermostatDetailVC(item: item, pages: pages)) + } + + func navigateToThermometerDetail(item: ItemBundle, pages: [DetailPage]) { + navigateTo(ThermometerDetailVC(item: item, pages: pages)) + } + + func navigateToGpmDetail(item: ItemBundle, pages: [DetailPage]) { + navigateTo(GpmDetailVC(item: item, pages: pages)) + } + + func navigateToWindowDetail(item: ItemBundle, pages: [DetailPage]) { + navigateTo(WindowDetailVC(item: item, pages: pages)) + } + + func navigateToPinSetup(lockScreenScope: LockScreenScope) { + navigateTo(PinSetupFeature.ViewController.create(scope: lockScreenScope)) + } + + func navigateToLockScreen(unlockAction: LockScreenFeature.UnlockAction) { + navigateTo(LockScreenFeature.ViewController.create(unlockAction: unlockAction)) + } + + func popToStatus() { + popToViewController(ofClass: StatusFeature.ViewController.self) + } + + func showMenu() { + present(SuplaMenuController()) + } + + func showAuthorization() { + present(SAAuthorizationDialogVC {}) + } + + func showLogin() { + present(SALoginDialogVC {}) + } + + func openForum() { + openUrl(url: NSLocalizedString("https://en-forum.supla.org", comment: "")) + } + + func openCloud() { + openUrl(url: "https://cloud.supla.org") + } + + func openUrl(url: String) { + if let url = URL(string: url) { + openUrl(url: url) + } + } + + func openUrl(url: URL) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + + private func navigateToStatusView() { + if (navigationController.viewControllers.isEmpty) { + navigateTo(StatusFeature.ViewController.create()) + } else if (navigationToStatusAllowed()) { + popToViewController(ofClass: StatusFeature.ViewController.self) + } + } + + private func navigationToStatusAllowed() -> Bool { + if (navigationController.viewControllers.last is StatusFeature.ViewController) { + return false // Already in + } + + if let subcontroller = navigationController.viewControllers.last as? NavigationSubcontroller { + return subcontroller.screenTakeoverAllowed() + } + + return true + } +} + +final class SuplaAppNavigationController: UINavigationController { + override var preferredStatusBarStyle: UIStatusBarStyle { statusBarStyle } + + private var statusBarStyle: UIStatusBarStyle = .lightContent + private var navigationBarHiddenOverride: Bool = true + + override func pushViewController(_ viewController: UIViewController, animated: Bool) { + statusBarStyle = viewController.preferredStatusBarStyle + if let navBarController = viewController as? NavigationBarVisibilityController { + SALog.debug("[PUSH] Setting navigation bar hidden: \(navBarController.navigationBarHidden)") + navigationBarHiddenOverride = navBarController.navigationBarHidden + super.setNavigationBarHidden(navigationBarHiddenOverride, animated: false) + } + super.pushViewController(viewController, animated: animated) + } + + override func popViewController(animated: Bool) -> UIViewController? { + let viewController = super.popViewController(animated: animated) + statusBarStyle = viewControllers.last?.preferredStatusBarStyle ?? .lightContent + if let navBarController = viewControllers.last as? NavigationBarVisibilityController { + SALog.debug("[POP] Setting navigation bar hidden: \(navBarController.navigationBarHidden)") + navigationBarHiddenOverride = navBarController.navigationBarHidden + super.setNavigationBarHidden(navigationBarHiddenOverride, animated: false) + } + return viewController + } + + override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? { + let viewControllers = super.popToViewController(viewController, animated: animated) + statusBarStyle = viewController.preferredStatusBarStyle + if let navBarController = viewController as? NavigationBarVisibilityController { + SALog.debug("[POP] Setting navigation bar hidden: \(navBarController.navigationBarHidden)") + navigationBarHiddenOverride = navBarController.navigationBarHidden + super.setNavigationBarHidden(navigationBarHiddenOverride, animated: false) + } + return viewControllers + } + + override func setNavigationBarHidden(_ hidden: Bool, animated: Bool) { + SALog.debug("setNavigationBarHidden(hidden: \(hidden), animated: \(animated)) overriden by \(navigationBarHiddenOverride)") + super.setNavigationBarHidden(navigationBarHiddenOverride, animated: animated) + } +} + +@objc +final class SuplaAppCoordinatorLegacyWrapper: NSObject { + @objc + static func finish() { + @Singleton var coordinator + coordinator.popViewController() + } + + @objc + static func dismiss(animated: Bool = true) { + @Singleton var coordinator + coordinator.dismiss(animated: true) + } + + @objc + static func currentViewController() -> UIViewController? { + @Singleton var coordinator + return coordinator.currentController() + } + + @objc + static func push(_ viewController: UIViewController) { + @Singleton var coordinator + coordinator.navigateTo(viewController) + } +} diff --git a/SUPLA/Core/Networking/SuplaAppProvider.swift b/SUPLA/Core/Networking/SuplaAppProvider.swift new file mode 100644 index 000000000..ebd486f0b --- /dev/null +++ b/SUPLA/Core/Networking/SuplaAppProvider.swift @@ -0,0 +1,39 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +protocol SuplaAppProvider { + func provide() -> SuplaAppApi + func revokeOAuthToken() + func initClientWithOneTimePassword(_ password: String) + func initSuplaClient() +} + +final class SuplaAppProviderImpl: SuplaAppProvider { + func provide() -> SuplaAppApi { SAApp.instance() } + + func revokeOAuthToken() { SAApp.revokeOAuthToken() } + + func initClientWithOneTimePassword(_ password: String) { + SAApp.suplaClient(withOneTimePassword: password) + } + + func initSuplaClient() { + SAApp.suplaClient() + } +} diff --git a/SUPLA/Core/Networking/SuplaResultCode.swift b/SUPLA/Core/Networking/SuplaResultCode.swift new file mode 100644 index 000000000..3ceb36c1f --- /dev/null +++ b/SUPLA/Core/Networking/SuplaResultCode.swift @@ -0,0 +1,55 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +enum SuplaResultCode: Int32, CaseIterable { + case unknwon = -1 + case temporarilyUnavailable = 4 + case badCredentials = 5 + case accessIdDisabled = 9 + case clientDisabled = 11 + case clientLimitExceeded = 12 + case registrationDisabled = 17 + case accessIdNotAssigned = 18 + case accessIdInactive = 31 + + func getTextMessage(authDialog: Bool = false) -> String { + switch (self) { + case .unknwon: Strings.Status.errorUnknown + case .temporarilyUnavailable: Strings.Status.errorUnavailable + case .badCredentials: + authDialog ? Strings.Status.errorInvalidData : Strings.Status.errorBadCredentials + case .accessIdDisabled: Strings.Status.errorAccessIdDisabled + case .clientDisabled: Strings.Status.errorDeviceDisabled + case .clientLimitExceeded: Strings.Status.errorClientLimitExceeded + case .registrationDisabled: Strings.Status.errorRegistrationDisabled + case .accessIdNotAssigned: Strings.Status.errorAccessIdNotAssigned + case .accessIdInactive: Strings.Status.errorAccessIdInactive + } + } + + static func from(value: Int32) -> SuplaResultCode { + for code in SuplaResultCode.allCases { + if (code.rawValue == value) { + return code + } + } + + return .unknwon + } +} diff --git a/SUPLA/Core/State/SuplaAppEvent.swift b/SUPLA/Core/State/SuplaAppEvent.swift new file mode 100644 index 000000000..2f313d56f --- /dev/null +++ b/SUPLA/Core/State/SuplaAppEvent.swift @@ -0,0 +1,34 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + + +enum SuplaAppEvent: Equatable { + case initialized + case noAccount + case connecting + case connected + case cancel + case lock + case unlock + case onStart + case networkConnected + + case finish(reason: SuplaAppState.Reason? = nil) + case error(reason: SuplaAppState.Reason) +} diff --git a/SUPLA/Core/State/SuplaAppState.swift b/SUPLA/Core/State/SuplaAppState.swift new file mode 100644 index 000000000..3c713699d --- /dev/null +++ b/SUPLA/Core/State/SuplaAppState.swift @@ -0,0 +1,141 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +enum SuplaAppState { + case initialization + case locked + case firstProfileCreation + case connecting(reason: Reason? = nil) + case connected + case disconnecting + case locking + case finished(reason: Reason? = nil) + + enum Reason: Equatable { + case connectionError(code: Int32) + case registerError(code: Int32) + case versionError + case noNetwork + case appInBackground + + var shouldAuthorize: Bool { + switch (self) { + case .registerError(let code): + code == SUPLA_RESULTCODE_REGISTRATION_DISABLED || code == SUPLA_RESULTCODE_ACCESSID_NOT_ASSIGNED + default: false + } + } + } + + func nextState(event: SuplaAppEvent) -> SuplaAppState? { + switch (self) { + case .initialization: initializationNextState(for: event) + case .locked: lockedNextState(for: event) + case .firstProfileCreation: firstProfileCreationNextState(for: event) + case .connecting(let reason): connectingNextState(for: event, previousReason: reason) + case .connected: connectedNextState(for: event) + case .disconnecting: disconnectingNextState(for: event) + case .locking: lockingNextState(for: event) + case .finished(let reason): finishedNextState(for: event, previousReason: reason) + } + } + + private func initializationNextState(for event: SuplaAppEvent) -> SuplaAppState? { + switch (event) { + case .onStart, .networkConnected: nil + case .lock: .locked + case .initialized: .connecting() + case .noAccount: .firstProfileCreation + default: fatalError("Unexpected event in Initialization: \(event)") + } + } + + private func lockedNextState(for event: SuplaAppEvent) -> SuplaAppState? { + switch (event) { + case .lock, .onStart, .networkConnected, .finish: nil + case .unlock: .connecting() + case .noAccount: .firstProfileCreation + default: fatalError("Unexpected event in Locked: \(event)") + } + } + + private func firstProfileCreationNextState(for event: SuplaAppEvent) -> SuplaAppState? { + switch (event) { + case .onStart, .networkConnected: nil + case .connecting: .connecting() + default: fatalError("Unexpected event in FirstProfileCreation: \(event)") + } + } + + private func connectingNextState(for event: SuplaAppEvent, previousReason: Reason?) -> SuplaAppState? { + switch (event) { + case .connecting, .initialized, .onStart: nil + case .connected: .connected + case .lock: .locked + case .cancel: .disconnecting + case .networkConnected: .connecting() + case .error(let reason): .connecting(reason: reason) + case .finish(let reason): .finished(reason: reason == nil ? previousReason : reason) + default: fatalError("Unexpected event in Connecting: \(event)") + } + } + + private func connectedNextState(for event: SuplaAppEvent) -> SuplaAppState? { + switch (event) { + case .onStart, .networkConnected: nil + case .connecting: .connecting() + case .lock: .locked + case .cancel: .disconnecting + case .finish(let reason): .finished(reason: reason) + case .error(let reason): .finished(reason: reason) + default: fatalError("Unexpected event in Connected: \(event)") + } + } + + private func disconnectingNextState(for event: SuplaAppEvent) -> SuplaAppState? { + switch (event) { + case .onStart, .cancel, .networkConnected, .connecting, .connected: nil + case .lock: .locking + case .finish(let reason): .finished(reason: reason) + case .error(let reason): .finished(reason: reason) + default: fatalError("Unexpected event in Disconnecting: \(event)") + } + } + + private func lockingNextState(for event: SuplaAppEvent) -> SuplaAppState? { + switch (event) { + case .onStart, .lock, .cancel, .networkConnected: nil + case .finish(_): .locked + default: fatalError("Unexpected event in Locking: \(event)") + } + } + + private func finishedNextState(for event: SuplaAppEvent, previousReason: Reason?) -> SuplaAppState? { + switch (event) { + case .cancel, .networkConnected: nil + case .initialized, .connecting: .connecting() + case .lock: .locked + case .onStart: previousReason == .noNetwork ? .connecting(reason: previousReason) : .connecting() + case .noAccount: .firstProfileCreation + case .error(let reason): reason != previousReason ? .finished(reason: reason) : nil + case .finish(let reason): reason != previousReason ? .finished(reason: reason ?? previousReason) : nil + default: fatalError("Unexpected event in Finished: \(event)") + } + } +} diff --git a/SUPLA/Core/State/SuplaAppStateHolder.swift b/SUPLA/Core/State/SuplaAppStateHolder.swift new file mode 100644 index 000000000..333214fac --- /dev/null +++ b/SUPLA/Core/State/SuplaAppStateHolder.swift @@ -0,0 +1,109 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import RxSwift + +protocol SuplaAppStateHolder { + func state() -> Observable + func handle(event: SuplaAppEvent) +} + +final class SuplaAppStateHolderImpl: SuplaAppStateHolder { + @Singleton private var schedulers + + private let stateSubject: BehaviorSubject = .init(value: .initialization) + + init() { + _ = stateSubject + .subscribe(on: schedulers.background) + .observe(on: schedulers.main) + .subscribe( + onNext: { + SALog.info("Supla client state: \($0)") + + switch ($0) { + // The connecting state may result as a change from many different states + // and we want that in this state always SuplaClient tries to connect + // that's why this initialization is added here. + case .connecting: SAApp.suplaClient() + default: break + } + } + ) + } + + func state() -> Observable { stateSubject.asObservable() } + + func handle(event: SuplaAppEvent) { + synced(self) { + let state = try? stateSubject.value() + if let nextState = state?.nextState(event: event) { + SALog.info("Got event: \(event) -> state: \(nextState)") + stateSubject.on(.next(nextState)) + } else { + SALog.info("Got event: \(event)") + } + } + } +} + +@objc +final class SuplaAppStateHolderProxy: NSObject { + @objc + static func versionError() { + @Singleton var stateHolder + stateHolder.handle(event: .error(reason: .versionError)) + } + + @objc + static func connecting() { + @Singleton var stateHolder + stateHolder.handle(event: .connecting) + } + + @objc + static func connectionError(code: Int32) { + @Singleton var stateHolder + stateHolder.handle(event: .error(reason: .connectionError(code: code))) + } + + @objc + static func connected() { + @Singleton var stateHolder + stateHolder.handle(event: .connected) + } + + @objc + static func registerError(code: Int32) { + @Singleton var stateHolder + stateHolder.handle(event: .error(reason: .registerError(code: code))) + } + + @objc + static func cancel() { + @Singleton var stateHolder + stateHolder.handle(event: .cancel) + } + + @objc + static func finish() { + @Singleton var stateHolder + stateHolder.handle(event: .finish()) + } +} diff --git a/SUPLA/Core/SuplaCore.swift b/SUPLA/Core/SuplaCore.swift new file mode 100644 index 000000000..168b62df3 --- /dev/null +++ b/SUPLA/Core/SuplaCore.swift @@ -0,0 +1,23 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import RxSwift +import SwiftUI + +struct SuplaCore {} diff --git a/SUPLA/Core/SwiftUiComponents/Architecture/SuplaCore+BaseViewController.swift b/SUPLA/Core/SwiftUiComponents/Architecture/SuplaCore+BaseViewController.swift new file mode 100644 index 000000000..8fc91799b --- /dev/null +++ b/SUPLA/Core/SwiftUiComponents/Architecture/SuplaCore+BaseViewController.swift @@ -0,0 +1,115 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import SwiftUI + +extension SuplaCore { + class BaseViewController>: UIViewController, NavigationBarVisibilityController { + @Singleton private var settings + + override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } + + var navigationBarHidden: Bool { false } + + var viewModel: VM + var state: S + var contentView: V! + + private lazy var hostingController: UIHostingController! = { + let controller = UIHostingController(rootView: contentView) + controller.view.translatesAutoresizingMaskIntoConstraints = false + return controller + }() + + init(viewModel: VM) { + self.viewModel = viewModel + self.state = viewModel.state + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + overrideUserInterfaceStyle = settings.darkMode.interfaceStyle + viewModel.onViewDidLoad() + + addChild(hostingController) + view.addSubview(hostingController.view) + hostingController.didMove(toParent: self) + + setupConstraints() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + overrideUserInterfaceStyle = settings.darkMode.interfaceStyle + viewModel.onViewWillAppear() + + NotificationCenter.default.addObserver(self, selector: #selector(onViewAppeared), name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(onViewDisappeared), name: UIApplication.didEnterBackgroundNotification, object: nil) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + viewModel.onViewAppeared() + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + if (!navigationBarHidden) { + setupToolbar() + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewModel.onViewWillDisappear() + + NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + viewModel.onViewDisappeared() + } + + private func setupConstraints() { + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.rightAnchor.constraint(equalTo: view.rightAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + hostingController.view.leftAnchor.constraint(equalTo: view.leftAnchor) + ]) + } + + @objc func onViewAppeared() { + viewModel.onViewAppeared() + } + + @objc func onViewDisappeared() { + viewModel.onViewDisappeared() + } + } +} diff --git a/SUPLA/Core/SwiftUiComponents/Architecture/SuplaCore+BaseViewModel.swift b/SUPLA/Core/SwiftUiComponents/Architecture/SuplaCore+BaseViewModel.swift new file mode 100644 index 000000000..1b1c0054b --- /dev/null +++ b/SUPLA/Core/SwiftUiComponents/Architecture/SuplaCore+BaseViewModel.swift @@ -0,0 +1,52 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import RxSwift + +extension SuplaCore { + class BaseViewModel: BaseViewModelBinder { + let disposeBag = DisposeBag() + var visibilityScopedDisposeBag = DisposeBag() + + var state: S + + init(state: S) { + self.state = state + } + + func onViewDidLoad() {} + + func onViewWillAppear() {} + + func onViewWillDisappear() { + // release all disposables when going to background + visibilityScopedDisposeBag = DisposeBag() + } + + func onViewAppeared() {} + + func onViewDisappeared() {} + } +} + +extension Disposable { + func disposedWhenDisappear(by viewModel: SuplaCore.BaseViewModel) { + disposed(by: viewModel.visibilityScopedDisposeBag) + } +} diff --git a/SUPLA/Core/SwiftUiComponents/Button/BackgroundStack.swift b/SUPLA/Core/SwiftUiComponents/Button/BackgroundStack.swift new file mode 100644 index 000000000..a94e848b3 --- /dev/null +++ b/SUPLA/Core/SwiftUiComponents/Button/BackgroundStack.swift @@ -0,0 +1,46 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import SwiftUI + +struct BackgroundStack: View { + var color: Color = .Supla.background + var content: () -> Content + + init(color: Color = .Supla.background, @ViewBuilder content: @escaping () -> Content) { + self.color = color + self.content = content + } + + var body: some View { + ZStack { + if #available(iOS 14.0, *) { + Color.Supla.background.ignoresSafeArea() + } else { + Color.Supla.background + } + content() + } + } +} + +#Preview { + BackgroundStack { + SwiftUI.Text("Example background view") + } +} diff --git a/SUPLA/Core/SwiftUiComponents/Button/BorderedButton.swift b/SUPLA/Core/SwiftUiComponents/Button/BorderedButton.swift new file mode 100644 index 000000000..ced245258 --- /dev/null +++ b/SUPLA/Core/SwiftUiComponents/Button/BorderedButton.swift @@ -0,0 +1,46 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import SwiftUI + +struct BorderedButton: View { + var title: String + var action: () -> Void + + var body: some View { + Button(title, action: action) + .buttonStyle(BorderedButtonStyle()) + } +} + +#Preview { + BorderedButton(title: "Title") {} +} + +struct BorderedButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + let color = configuration.isPressed ? Color(UIColor.primaryVariant) : Color(UIColor.primary) + configuration.label + .foregroundColor(color) + .font(.Supla.labelLarge) + .cornerRadius(Dimens.buttonRadius) + .padding(EdgeInsets(top: 8, leading: 24, bottom: 8, trailing: 24)) + .overlay(RoundedRectangle(cornerRadius: Dimens.buttonRadius).stroke(color, lineWidth: 1)) + } +} diff --git a/SUPLA/Core/SwiftUiComponents/Button/TextButton.swift b/SUPLA/Core/SwiftUiComponents/Button/TextButton.swift new file mode 100644 index 000000000..b48834a8e --- /dev/null +++ b/SUPLA/Core/SwiftUiComponents/Button/TextButton.swift @@ -0,0 +1,56 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import SwiftUI + +struct TextButton: View { + var title: String + + var normalColor: Color = .Supla.primary + var pressedColor: Color = .Supla.primaryVariant + + var action: () -> Void + + var body: some View { + Button(title, action: action) + .buttonStyle(TextButtonStyle(normalColor: normalColor, pressedColor: pressedColor)) + } +} + +#Preview { + TextButton(title: "Title") {} +} + +#Preview("Blue") { + TextButton(title: "Title", normalColor: .blue) {} +} + +struct TextButtonStyle: ButtonStyle { + var normalColor: Color + var pressedColor: Color + + func makeBody(configuration: Configuration) -> some View { + let color = configuration.isPressed ? pressedColor : normalColor + configuration.label + .foregroundColor(color) + .font(.Supla.labelLarge) + .cornerRadius(Dimens.buttonRadius) + .padding(EdgeInsets(top: 8, leading: 24, bottom: 8, trailing: 24)) + } +} diff --git a/SUPLA/Core/SwiftUiComponents/FilledButton.swift b/SUPLA/Core/SwiftUiComponents/FilledButton.swift new file mode 100644 index 000000000..75a5c394d --- /dev/null +++ b/SUPLA/Core/SwiftUiComponents/FilledButton.swift @@ -0,0 +1,72 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import SwiftUI + +struct FilledButton: View { + var title: String + var fullWidth: Bool = false + var action: () -> Void + + @Environment(\.isEnabled) var isEnabled + + var body: some View { + return Button(action: action) { + if (fullWidth) { + Text.LabelLarge(text: title) + .frame(maxWidth: .infinity) + } else { + Text.LabelLarge(text: title) + } + } + .buttonStyle(FilledButtonStyle(isEnabled: isEnabled)) + } +} + +#Preview { + VStack { + FilledButton(title: "Title") {} + FilledButton(title: "Title", fullWidth: true) {} + } +} + +struct FilledButtonStyle: ButtonStyle { + private let isEnabled: Bool + + init(isEnabled: Bool) { + self.isEnabled = isEnabled + } + + func makeBody(configuration: Configuration) -> some View { + let backgroundColor = if(!isEnabled) { + Color.Supla.disabled + } else if (configuration.isPressed) { + Color.Supla.primaryVariant + } else { + Color.Supla.primary + } + + configuration.label + .foregroundColor(.Supla.onPrimary) + .font(.Supla.labelLarge) + .padding(EdgeInsets(top: 8, leading: 24, bottom: 8, trailing: 24)) + .background(backgroundColor) + .cornerRadius(Dimens.buttonRadius) + } +} diff --git a/SUPLA/Core/SwiftUiComponents/PinTextField.swift b/SUPLA/Core/SwiftUiComponents/PinTextField.swift new file mode 100644 index 000000000..cd8886011 --- /dev/null +++ b/SUPLA/Core/SwiftUiComponents/PinTextField.swift @@ -0,0 +1,191 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import Combine +import SwiftUI + +let PIN_LENGTH = 4 + +struct PinTextFieldModifier: ViewModifier { + @Binding var value: String + + @Binding private var focusedField: Value? + + @State private var previousValue: String = "" + private var isError: Bool = false + + private var pinLength = PIN_LENGTH + private var focusValue: Value? = nil + private var onChange: (String) -> Void = { _ in } + private var keyboardType: UIKeyboardType = .numberPad + + init(_ value: Binding) { + self._value = value + self._focusedField = .constant(nil) + } + + func focused(_ binding: Binding, equals: Value) -> Self { + var copy = self + copy._focusedField = binding + copy.focusValue = equals + return copy + } + + func error(_ binding: Bool) -> Self { + var copy = self + copy.isError = binding + return copy + } + + func onChange(_ onChange: @escaping (String) -> Void) -> Self { + var copy = self + copy.onChange = onChange + return copy + } + + func keyboardType(_ keyboardType: UIKeyboardType) -> Self { + var copy = self + copy.keyboardType = keyboardType + return copy + } + + func pinLenght(_ length: Int) -> Self { + var copy = self + copy.pinLength = length + return copy + } + + func body(content: Content) -> some View { + ZStack(alignment: .center) { + HStack { + ForEach(0 ..< pinLength, id: \.self) { index in + PinItem(index, focused: focusedField == focusValue) + } + } + + if let focusValue = focusValue { + HiddenTextField(content) + .focused($focusedField, equals: focusValue) + } else { + HiddenTextField(content) + } + } + } + + @ViewBuilder + private func HiddenTextField(_ content: Content) -> some View { + content + .frame(width: 192, height: 56, alignment: .center) + .font(.system(size: 0)) + .accentColor(.clear) + .foregroundColor(.clear) + .multilineTextAlignment(.center) + .keyboardType(keyboardType) + .padding() + .onReceive(Just(value)) { _ in + if (value.count > pinLength) { + value = String(value.prefix(pinLength)) + } + if (previousValue != value) { + previousValue = value + onChange(previousValue) + } + } + } + + @ViewBuilder + private func PinItem(_ index: Int, focused: Bool = false) -> some View { + let itemFocused = value.count == index || (value.count == pinLength && index == 3) + let color = if (focused && itemFocused) { Color.Supla.primary } + else if (isError) { Color.Supla.error } + else { Color.Supla.disabled } + + ZStack(alignment: .center) { + if (index < value.count) { + Circle() + .foregroundColor(color) + .frame(width: 8, height: 8) + } + } + .frame(width: 48, height: 56) + .background(RoundedRectangle(cornerRadius: 4).fill(Color.Supla.surface)) + .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(color, lineWidth: 1)) + } +} + +private extension View { + @ViewBuilder + func focused(_ binding: Binding, equals value: Value) -> some View where Value: Hashable { + if #available(iOS 15.0, *) { + self.modifier(TextFieldFocused(binding: binding, value: value)) + } else { + self + } + } +} + +@available(iOS 15.0, *) +private struct TextFieldFocused: ViewModifier where Value: Hashable { + private let value: Value + @FocusState private var focused: Value? + @Binding private var binding: Value? + + init(binding: Binding, value: Value) { + self._binding = binding + self.value = value + } + + func body(content: Content) -> some View { + content + .focused($focused, equals: value) + .onChange(of: binding) { newValue in + focused = newValue + } + .onChange(of: focused) { newValue in + if newValue != nil { + binding = newValue + } + } + .onAppear { + focused = binding + } + } +} + +private enum TestFieldFocus: Hashable {} + +#Preview("Empty") { + @State var text = "" + return TextField("", text: $text) + .modifier(PinTextFieldModifier($text)) +} + +#Preview("One item") { + @State var text = "1" + return TextField("", text: $text) + .modifier(PinTextFieldModifier($text)) +} + +#Preview("error") { + @State var text = "11" + return TextField("", text: $text) + .modifier( + PinTextFieldModifier($text) + .error(true) + ) +} diff --git a/SUPLA/Core/SwiftUiComponents/Text.swift b/SUPLA/Core/SwiftUiComponents/Text.swift new file mode 100644 index 000000000..459b559ae --- /dev/null +++ b/SUPLA/Core/SwiftUiComponents/Text.swift @@ -0,0 +1,124 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import SwiftUI + +struct Text { + protocol SuplaText: View {} + + struct HeadlineSmall: SuplaText { + var text: String + var alignment: SwiftUI.TextAlignment = .center + var color: Color = Color.Supla.onBackground + + var body: some View { + if #available(iOS 15.0, *) { + SwiftUI.Text(text) + .font(.Supla.headlineSmall) + .foregroundStyle(color) + .multilineTextAlignment(alignment) + } else { + SwiftUI.Text(text) + .font(.Supla.headlineSmall) + .foregroundColor(color) + .multilineTextAlignment(alignment) + } + } + } + + struct BodyLarge: SuplaText { + var text: String + var alignment: SwiftUI.TextAlignment = .center + var color: Color = Color.Supla.onBackground + + var body: some View { + if #available(iOS 15.0, *) { + SwiftUI.Text(text) + .font(.Supla.bodyLarge) + .foregroundStyle(color) + .multilineTextAlignment(alignment) + } else { + SwiftUI.Text(text) + .font(.Supla.bodyLarge) + .foregroundColor(color) + .multilineTextAlignment(alignment) + } + } + } + + struct BodyMedium: SuplaText { + var text: String + var alignment: SwiftUI.TextAlignment = .center + var color: Color = Color.Supla.onBackground + + var body: some View { + if #available(iOS 15.0, *) { + SwiftUI.Text(text) + .font(.Supla.bodyMedium) + .foregroundStyle(color) + .multilineTextAlignment(alignment) + } else { + SwiftUI.Text(text) + .font(.Supla.bodyMedium) + .foregroundColor(color) + .multilineTextAlignment(alignment) + } + } + } + + struct BodySmall: SuplaText { + var text: String + var alignment: SwiftUI.TextAlignment = .center + var color: Color = Color.Supla.onBackground + + var body: some View { + if #available(iOS 15.0, *) { + SwiftUI.Text(text) + .font(.Supla.bodySmall) + .foregroundStyle(color) + .multilineTextAlignment(alignment) + } else { + SwiftUI.Text(text) + .font(.Supla.bodySmall) + .foregroundColor(color) + .multilineTextAlignment(alignment) + } + } + } + + struct LabelLarge: SuplaText { + var text: String + var alignment: SwiftUI.TextAlignment = .center + var color: Color = Color.Supla.onPrimary + + var body: some View { + if #available(iOS 15.0, *) { + SwiftUI.Text(text) + .font(.Supla.labelLarge) + .foregroundStyle(color) + .multilineTextAlignment(alignment) + } else { + SwiftUI.Text(text) + .font(.Supla.labelLarge) + .foregroundColor(color) + .multilineTextAlignment(alignment) + } + } + } +} diff --git a/SUPLA/Core/UI/BaseViewController.swift b/SUPLA/Core/UI/BaseViewController.swift index ff82b9eb8..eb64935f7 100644 --- a/SUPLA/Core/UI/BaseViewController.swift +++ b/SUPLA/Core/UI/BaseViewController.swift @@ -19,7 +19,12 @@ import Foundation import RxSwift -class BaseViewControllerVM> : BaseViewController { +class BaseViewControllerVM>: UIViewController, NavigationBarVisibilityController { + + override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } + + var navigationBarHidden: Bool { false } + var navigationBarMaintainedByParent: Bool = false @Singleton private var settings @@ -29,7 +34,9 @@ class BaseViewControllerVM Bool { false } - func handle(event: E) { fatalError("handle(event:) has not been implemented!") } func handle(state: S) { } // default empty implementation + func getToolbarFont() -> UIFont { .suplaSubtitleFont } func observeNotification(name: NSNotification.Name?, selector: Selector) { NotificationCenter.default.addObserver(self, selector: selector, name: name, object: nil) } + func showInfoDialog(title: String, message: String) { + let infoDialog = UIAlertController(title: title, message: message, preferredStyle: .alert) + infoDialog.addAction(UIAlertAction(title: Strings.General.close, style: .default)) + self.present(infoDialog, animated: true) + } + #if DEBUG deinit { let className = NSStringFromClass(type(of: self)) diff --git a/SUPLA/Core/UI/BaseViewModel.swift b/SUPLA/Core/UI/BaseViewModel.swift index 5dbfffe21..f923f877a 100644 --- a/SUPLA/Core/UI/BaseViewModel.swift +++ b/SUPLA/Core/UI/BaseViewModel.swift @@ -20,14 +20,52 @@ import Foundation import RxCocoa import RxSwift -class BaseViewModel { - fileprivate let disposeBag = DisposeBag() +protocol BaseViewModelBinder { + var disposeBag: DisposeBag { get } +} + +extension BaseViewModelBinder { + func bind(_ observable: Observable, _ action: @escaping () -> Void) { + observable + .subscribe(onNext: { action() }) + .disposed(by: disposeBag) + } + + func bind(_ single: Single, _ action: @escaping () -> Void) { + single + .subscribe(onSuccess: { action() }) + .disposed(by: disposeBag) + } + + func bind(_ observable: Observable, _ action: @escaping (T) -> Void) { + observable + .subscribe(onNext: { action($0) }) + .disposed(by: disposeBag) + } + + func bind(_ observable: ControlEvent, _ action: @escaping () -> Void) { + observable + .subscribe(onNext: { action() }) + .disposed(by: disposeBag) + } + + func bind(_ observable: ControlProperty, _ action: @escaping (T) -> Void) { + observable + .subscribe(onNext: { action($0) }) + .disposed(by: disposeBag) + } +} + +class BaseViewModel: BaseViewModelBinder { + let disposeBag = DisposeBag() + private let events = PublishSubject() lazy var state: BehaviorSubject = BehaviorSubject(value: defaultViewState()) func defaultViewState() -> S { fatalError("defaultViewState() has not been implemented!") } func onViewDidLoad() {} + func onViewWillAppear() {} func eventsObervable() -> Observable { events.asObserver() } func stateObservable() -> Observable { state.asObserver() } @@ -74,36 +112,6 @@ class BaseViewModel { .disposed(by: disposeBag) } - func bind(_ observable: Observable, _ action: @escaping () -> Void) { - observable - .subscribe(onNext: { action() }) - .disposed(by: disposeBag) - } - - func bind(_ single: Single, _ action: @escaping () -> Void) { - single - .subscribe(onSuccess: { action() }) - .disposed(by: disposeBag) - } - - func bind(_ observable: Observable, _ action: @escaping (T) -> Void) { - observable - .subscribe(onNext: { action($0) }) - .disposed(by: disposeBag) - } - - func bind(_ observable: ControlEvent, _ action: @escaping () -> Void) { - observable - .subscribe(onNext: { action() }) - .disposed(by: disposeBag) - } - - func bind(_ observable: ControlProperty, _ action: @escaping (T) -> Void) { - observable - .subscribe(onNext: { action($0) }) - .disposed(by: disposeBag) - } - #if DEBUG deinit { let className = NSStringFromClass(type(of: self)) diff --git a/SUPLA/Core/UI/Components/SuplaTabBarController.swift b/SUPLA/Core/UI/Components/SuplaTabBarController.swift index 5d25ad562..0e5391346 100644 --- a/SUPLA/Core/UI/Components/SuplaTabBarController.swift +++ b/SUPLA/Core/UI/Components/SuplaTabBarController.swift @@ -18,17 +18,17 @@ import RxSwift -class SuplaTabBarController>: UITabBarController, NavigationCoordinatorAware { +class SuplaTabBarController>: UITabBarController, NavigationBarVisibilityController { - fileprivate let disposeBag = DisposeBag() let viewModel: VM - weak var navigationCoordinator: NavigationCoordinator? + var navigationBarHidden: Bool { false } + override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } + + fileprivate let disposeBag = DisposeBag() - init(navigationCoordinator: NavigationCoordinator, viewModel: VM) { + init(viewModel: VM) { self.viewModel = viewModel - self.navigationCoordinator = navigationCoordinator super.init(nibName: nil, bundle: nil) - setupView() } required init?(coder: NSCoder) { @@ -37,11 +37,9 @@ class SuplaTabBarController UIFont { UIFont.suplaTitleBarFont } private func setupView() { tabBar.barTintColor = .background diff --git a/SUPLA/Core/UI/Details/StandardDetailVC.swift b/SUPLA/Core/UI/Details/StandardDetailVC.swift index 519aa872a..b850694b6 100644 --- a/SUPLA/Core/UI/Details/StandardDetailVC.swift +++ b/SUPLA/Core/UI/Details/StandardDetailVC.swift @@ -24,10 +24,10 @@ class StandardDetailVC private let item: ItemBundle private let pages: [DetailPage] - init(navigator: NavigationCoordinator, viewModel: VM, item: ItemBundle, pages: [DetailPage]) { + init(viewModel: VM, item: ItemBundle, pages: [DetailPage]) { self.item = item self.pages = pages - super.init(navigationCoordinator: navigator, viewModel: viewModel) + super.init(viewModel: viewModel) } required init?(coder: NSCoder) { @@ -36,14 +36,16 @@ class StandardDetailVC override func viewDidLoad() { super.viewDidLoad() - edgesForExtendedLayout = [] - view.backgroundColor = .background - viewModel.loadData(remoteId: item.remoteId, type: item.subjectType) setupViewController() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + setupToolbar() + } + override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { var openedPage = 0 if let viewControllers = self.viewControllers { @@ -116,29 +118,28 @@ class StandardDetailVC private func switchGeneral() -> SwitchGeneralVC { let vc = SwitchGeneralVC(remoteId: item.remoteId) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabGeneral : nil, image: .iconGeneral, tag: DetailTabTag.Switch.rawValue ) + vc.navigationBarMaintainedByParent = true return vc } private func switchTimerDetail() -> SwitchTimerDetailVC { let vc = SwitchTimerDetailVC(remoteId: item.remoteId) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabTimer : nil, image: .iconTimer, tag: DetailTabTag.Timer.rawValue ) + vc.navigationBarMaintainedByParent = true return vc } private func legacyDetail(type: LegacyDetailType) -> DetailViewController { let vc = DetailViewController(detailViewType: type, remoteId: item.remoteId) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabMetrics : nil, image: .iconMetrics, @@ -149,157 +150,155 @@ class StandardDetailVC private func thermostatGeneral() -> ThermostatGeneralVC { let vc = ThermostatGeneralVC(item: item) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabGeneral : nil, image: .iconGeneral, tag: DetailTabTag.Thermostat.rawValue ) + vc.navigationBarMaintainedByParent = true return vc } private func scheduleDetail() -> ScheduleDetailVC { let vc = ScheduleDetailVC(item: item) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabSchedule : nil, image: .iconSchedule, tag: DetailTabTag.Schedule.rawValue ) + vc.navigationBarMaintainedByParent = true return vc } private func thermostatHistoryDetail() -> ThermostatHistoryDetailVC { let vc = ThermostatHistoryDetailVC(remoteId: item.remoteId, navigationItemProvider: self) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabHistory : nil, image: .iconHistory, tag: DetailTabTag.ThermostatHistory.rawValue ) + vc.navigationBarMaintainedByParent = true return vc } private func thermometerHistoryDetail() -> ThermometerHistoryDetailVC { let vc = ThermometerHistoryDetailVC(remoteId: item.remoteId, navigationItemProvider: self) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabHistory : nil, image: .iconHistory, tag: DetailTabTag.ThermostatHistory.rawValue ) + vc.navigationBarMaintainedByParent = true return vc } private func thermostatTimerDetail() -> ThermostatTimerDetailVC { let vc = ThermostatTimerDetailVC(remoteId: item.remoteId) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabTimer : nil, image: .iconTimer, tag: DetailTabTag.Timer.rawValue ) - + vc.navigationBarMaintainedByParent = true return vc } private func gpmHistoryDetail() -> GpmHistoryDetailVC { let vc = GpmHistoryDetailVC(remoteId: item.remoteId, navigationItemProvider: self) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabHistory : nil, image: .iconHistory, tag: DetailTabTag.History.rawValue ) - + vc.navigationBarMaintainedByParent = true return vc } private func rollerShutterDetail() -> RollerShutterVC { let vc = RollerShutterVC(itemBundle: item) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabGeneral : nil, image: .iconGeneral, tag: DetailTabTag.Window.rawValue ) + vc.navigationBarMaintainedByParent = true return vc } private func roofWindowDetail() -> RoofWindowVC { let vc = RoofWindowVC(itemBundle: item) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabGeneral : nil, image: .iconGeneral, tag: DetailTabTag.Window.rawValue ) + vc.navigationBarMaintainedByParent = true return vc } private func facadeBlindDetail() -> FacadeBlindsVC { let vc = FacadeBlindsVC(itemBundle: item) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabGeneral : nil, image: .iconGeneral, tag: DetailTabTag.Window.rawValue ) + vc.navigationBarMaintainedByParent = true return vc } private func terraceAwningDetail() -> TerraceAwningVC { let vc = TerraceAwningVC(itemBundle: item) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabGeneral : nil, image: .iconGeneral, tag: DetailTabTag.Window.rawValue ) + vc.navigationBarMaintainedByParent = true return vc } private func projectorScreenDetail() -> ProjectorScreenVC { let vc = ProjectorScreenVC(itemBundle: item) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabGeneral : nil, image: .iconGeneral, tag: DetailTabTag.Window.rawValue ) + vc.navigationBarMaintainedByParent = true return vc } private func curtainDetail() -> CurtainVC { let vc = CurtainVC(itemBundle: item) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabGeneral : nil, image: .iconGeneral, tag: DetailTabTag.Window.rawValue ) + vc.navigationBarMaintainedByParent = true return vc } private func verticalBlindDetail() -> VerticalBlindsVC { let vc = VerticalBlindsVC(itemBundle: item) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabGeneral : nil, image: .iconGeneral, tag: DetailTabTag.Window.rawValue ) + vc.navigationBarMaintainedByParent = true return vc } private func garageDoorDetail() -> GarageDoorVC { let vc = GarageDoorVC(itemBundle: item) - vc.navigationCoordinator = navigationCoordinator vc.tabBarItem = UITabBarItem( title: settings.showBottomLabels ? Strings.StandardDetail.tabGeneral : nil, image: .iconGeneral, tag: DetailTabTag.Window.rawValue ) + vc.navigationBarMaintainedByParent = true return vc } } diff --git a/SUPLA/Core/UI/Dialogs/Base/SACustomDialogVC.swift b/SUPLA/Core/UI/Dialogs/Base/SACustomDialogVC.swift index 0602b89b5..5986f9e94 100644 --- a/SUPLA/Core/UI/Dialogs/Base/SACustomDialogVC.swift +++ b/SUPLA/Core/UI/Dialogs/Base/SACustomDialogVC.swift @@ -51,7 +51,6 @@ class SACustomDialogVC>: Bas override func viewDidLoad() { super.viewDidLoad() - statusBarBackgroundView.isHidden = true view.addGestureRecognizer(backgroundTapGestureRecognizer) view.backgroundColor = .dialogScrim diff --git a/SUPLA/Core/UI/Dialogs/SAAlertDialogVC.swift b/SUPLA/Core/UI/Dialogs/SAAlertDialogVC.swift index f247e0abf..2b9ed6aa7 100644 --- a/SUPLA/Core/UI/Dialogs/SAAlertDialogVC.swift +++ b/SUPLA/Core/UI/Dialogs/SAAlertDialogVC.swift @@ -19,7 +19,6 @@ import RxSwift final class SAAlertDialogVC: SACustomDialogVC { - private lazy var titleLabel: UILabel = SADialogTitleLabel() private lazy var topSeparatorView: SeparatorView = .init() @@ -35,16 +34,22 @@ final class SAAlertDialogVC: SACustomDialogVC { - @Singleton private var profileRepository - @Singleton private var suplaClientProvider - @Singleton private var authorizationUseCase - @Singleton private var schedulers - - override func defaultViewState() -> SAAuthorizationDialogViewState { SAAuthorizationDialogViewState() } - - override func onViewDidLoad() { - profileRepository.getActiveProfile() - .asDriverWithoutError() - .drive( - onNext: { [weak self] profile in - let isCloud = profile.authInfo?.serverForCurrentAuthMethod.contains(".supla.org") == true - let nameEnabled = self?.suplaClientProvider.provide().isRegistered() == true - - self?.updateView { - $0.changing(path: \.userName, to: profile.authInfo?.emailAddress ?? "") - .changing(path: \.isCloudAccount, to: isCloud) - .changing(path: \.userNameEnabled, to: nameEnabled) - } - } - ) - .disposed(by: self) - } - - func isAuthorized() -> Bool { - let client = suplaClientProvider.provide() - return client.isRegistered() && client.isSuperuserAuthorized() - } - - func onOk(userName: String, password: String, _ onAuthorized: @escaping () -> Void) { - if (isAuthorized()) { - onAuthorized() - return - } - - authorizationUseCase.invoke(userName: userName, password: password) - .subscribe(on: schedulers.background) - .observe(on: schedulers.main) - .do( - onSubscribe: { [weak self] in self?.updateView { $0.changing(path: \.loading, to: true) }}, - onDispose: { [weak self] in self?.updateView { $0.changing(path: \.loading, to: false) }} - ) - .subscribe( - onCompleted: onAuthorized, - onError: { [weak self] error in - if let authorizationError = error as? AuthorizationError { - self?.updateView { $0.changing(path: \.error, to: authorizationError.errorMessage) } - } else { - self?.updateView { $0.changing(path: \.error, to: Strings.General.unknownError) } - } - } - ) - .disposed(by: self) - } -} - -struct SAAuthorizationDialogViewState: ViewState { - var userName: String = "" - var isCloudAccount: Bool = false - var userNameEnabled: Bool = false - var error: String? = nil - var loading = false -} - -enum SAAuthorizationDialogViewEvent: ViewEvent { - case dismiss -} diff --git a/SUPLA/Core/UI/NavigationBarVisibilityController.swift b/SUPLA/Core/UI/NavigationBarVisibilityController.swift new file mode 100644 index 000000000..4851055b5 --- /dev/null +++ b/SUPLA/Core/UI/NavigationBarVisibilityController.swift @@ -0,0 +1,30 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +@objc +protocol NavigationBarVisibilityController { + var navigationBarHidden: Bool { get } +} + +extension SAAddWizardVC: NavigationBarVisibilityController { + var navigationBarHidden: Bool { true } +} + +extension SAZWaveConfigurationWizardVC: NavigationBarVisibilityController { + var navigationBarHidden: Bool { true } +} diff --git a/SUPLA/Core/UI/TableView/BaseTableViewController.swift b/SUPLA/Core/UI/TableView/BaseTableViewController.swift index 664d82827..10081fa66 100644 --- a/SUPLA/Core/UI/TableView/BaseTableViewController.swift +++ b/SUPLA/Core/UI/TableView/BaseTableViewController.swift @@ -47,10 +47,6 @@ class BaseTableViewController UIFont { .suplaTitleBarFont } + func setupTableView() { tableView.register(UINib(nibName: Nibs.locationCell, bundle: nil), forCellReuseIdentifier: cellIdForLocation) tableView.delegate = self diff --git a/SUPLA/CoreDataManager.swift b/SUPLA/CoreDataManager.swift index 66e1808a6..ad802f5f2 100644 --- a/SUPLA/CoreDataManager.swift +++ b/SUPLA/CoreDataManager.swift @@ -16,12 +16,11 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -import Foundation import CoreData +import Foundation @objc class CoreDataManager: NSObject { - @Singleton private var settings let migrator: CoreDataMigrator @@ -70,7 +69,7 @@ class CoreDataManager: NSObject { self.migrator = migrator } - @objc func setup(completion: @escaping () -> Void) { + @objc func setup() { removeOldDatabases() ValueTransformer.setValueTransformer( @@ -78,61 +77,39 @@ class CoreDataManager: NSObject { forName: NSValueTransformerName("GroupTotalValueTransformer") ) - loadPersistentStore { - completion() - } - } - - private func loadPersistentStore(completion: @escaping () -> Void) { - migrateStoreIfNeeded { - self.persistentContainer.loadPersistentStores { description, error in - guard error == nil else { - fatalError("Not able to load store \(error!)") - } + migrateStoreIfNeeded() + + persistentContainer.loadPersistentStores { _, error in + guard error == nil else { + fatalError("Not able to load store \(error!)") + } - if(self.tryRecreateAccount) { - DispatchQueue.global(qos: .userInitiated).async { - if (SAApp.profileManager().restoreProfileFromDefaults()) { - var settings = self.settings - settings.anyAccountRegistered = true - } - DispatchQueue.main.async { - completion() - } - } - } - else { - completion() + if (self.tryRecreateAccount) { + if (SAApp.profileManager().restoreProfileFromDefaults()) { + var settings = self.settings + settings.anyAccountRegistered = true } } } } - private func migrateStoreIfNeeded(completion: @escaping () -> Void) { + private func migrateStoreIfNeeded() { guard let storeUrl = persistentContainer.persistentStoreDescriptions.first?.url else { fatalError("persistentContainer was not set up properly") } if migrator.requiresMigration(at: storeUrl, toVersion: CoreDataMigrationVersion.current) { - DispatchQueue.global(qos: .userInitiated).async { - do { - try self.migrator.migrateStore(at: storeUrl, toVersion: CoreDataMigrationVersion.current) - } catch { + do { + try migrator.migrateStore(at: storeUrl, toVersion: CoreDataMigrationVersion.current) + } catch { #if DEBUG - fatalError("Migration failed with error \(error)") + fatalError("Migration failed with error \(error)") #else - // If migration fails in production we want to delete the database so the - // user is able to create account again - self.removeCurrentDatabase() + // If migration fails in production we want to delete the database so the + // user is able to create account again + removeCurrentDatabase() #endif - } - - DispatchQueue.main.async { - completion() - } } - } else { - completion() } } @@ -150,8 +127,8 @@ class CoreDataManager: NSObject { if let removed = try? removeDatabase(with: "SUPLA_DB.sqlite"), removed { tryRecreateAccount = true } - for i in 0..<14 { - if let removed = try? removeDatabase(with: String.init(format: "SUPLA_DB%i.sqlite", i)), removed { + for i in 0 ..< 14 { + if let removed = try? removeDatabase(with: String(format: "SUPLA_DB%i.sqlite", i)), removed { tryRecreateAccount = true } } @@ -168,4 +145,3 @@ class CoreDataManager: NSObject { return false } } - diff --git a/SUPLA/CreateAccountVC.m b/SUPLA/CreateAccountVC.m index 624c4ea37..005d0a9e5 100644 --- a/SUPLA/CreateAccountVC.m +++ b/SUPLA/CreateAccountVC.m @@ -31,7 +31,7 @@ @implementation SACreateAccountVC { - (void)viewDidLoad { [super viewDidLoad]; [self.webView setDelegate:self]; - self.statusBarBackgroundView.backgroundColor = [UIColor primary]; + self.statusBarBackgroundView.backgroundColor = [UIColor toolbar]; self.title = @"supla"; } diff --git a/SUPLA/Features/AccountCreation/AccountCreationNavigationCoordinator.swift b/SUPLA/Features/AccountCreation/AccountCreationNavigationCoordinator.swift deleted file mode 100644 index 4898821f8..000000000 --- a/SUPLA/Features/AccountCreation/AccountCreationNavigationCoordinator.swift +++ /dev/null @@ -1,98 +0,0 @@ -/* - Copyright (C) AC SOFTWARE SP. Z O.O. - - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -import UIKit -import RxSwift - -class AuthCfgNavigationCoordinator: BaseNavigationCoordinator { - override var wantsAnimatedTransitions: Bool { - return !_immediate - } - override var viewController: UIViewController { - return _viewController - } - - private let _immediate: Bool - private var _profileId: NSManagedObjectID? - - private lazy var _viewController: AccountCreationVC = { - return AccountCreationVC(navigationCoordinator: self, - profileId: _profileId) - }() - - init(immediate: Bool, profileId: ProfileID? = nil) { - _immediate = immediate - _profileId = profileId - } - - @objc private func onDismissSubview(_ sender: AnyObject) { - _viewController.navigationController?.popToViewController(_viewController, - animated: true) - } - - override func startFlow(coordinator child: NavigationCoordinator) { - _viewController.present(child.viewController, animated: true) { - super.startFlow(coordinator: child) - } - } - - func restartAppFlow() { - // Go back to main navigator, finish all inbetween and start from beginning. - let navigated = goTo(MainNavigationCoordinator.self) { navigator in - navigator.start(from: nil) - } - if (!navigated) { - finish() - } - } - - func navigateToCreateAccount() { - let cavc = SACreateAccountVC(nibName: "CreateAccountVC", bundle: nil) - cavc.navigationCoordinator = self - self._viewController.navigationController?.pushViewController(cavc, animated: true) - } - - func navigateToRemoveAccount(needsRestart: Bool, serverAddress: String?) { - finish() - (parentCoordinator as? ProfilesNavigationCoordinator)?.navigateToRemoveAccount(needsRestart: needsRestart, serverAddress: serverAddress) - } - - func finish(shouldReauthenticate: Bool) { - if (shouldReauthenticate) { - let navigated = goTo(MainNavigationCoordinator.self) { navigator in - //navigator.showStatusView(progress: 0) - } - if (!navigated) { - finish() - } - } else { - finish() - } - } -} - -extension AuthCfgNavigationCoordinator: NavigationAnimationSupport { - func animationControllerFor(operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { - if toVC is SACreateAccountVC { - return FadeTransition(isPresenting: true) - } else if fromVC is SACreateAccountVC { - return FadeTransition(isPresenting: false) - } else { - return nil - } - } -} diff --git a/SUPLA/Features/AccountCreation/AccountCreationVC.swift b/SUPLA/Features/AccountCreation/AccountCreationVC.swift index 4ee96615e..53dfba044 100644 --- a/SUPLA/Features/AccountCreation/AccountCreationVC.swift +++ b/SUPLA/Features/AccountCreation/AccountCreationVC.swift @@ -27,6 +27,8 @@ import RxCocoa */ class AccountCreationVC: BaseViewControllerVM { + @Singleton private var coordinator + // MARK: UI variables @IBOutlet private var controlStack: UIStackView! @IBOutlet private var loadingStack: UIStackView! @@ -89,15 +91,9 @@ class AccountCreationVC: BaseViewControllerVM Bool { - return true - } - - override func hidesNavigationBar() -> Bool { - return !adjustsStatusBarBackground() - } - private func configureUI() { [vBasic, vAdvanced, adFormHostView, adFormEmailAuth, adFormAccessIdAuth].forEach { @@ -304,18 +292,17 @@ class AccountCreationVC: BaseViewControllerVM Bool { + return false + } +} diff --git a/SUPLA/Features/AccountCreation/Base.lproj/AccountCreationVC.xib b/SUPLA/Features/AccountCreation/Base.lproj/AccountCreationVC.xib index 2383acf25..a80f8dbe3 100644 --- a/SUPLA/Features/AccountCreation/Base.lproj/AccountCreationVC.xib +++ b/SUPLA/Features/AccountCreation/Base.lproj/AccountCreationVC.xib @@ -88,7 +88,7 @@ - + @@ -730,7 +730,7 @@ When the connection has been set, go back to the application and continue addin - + @@ -760,5 +760,8 @@ When the connection has been set, go back to the application and continue addin + + + diff --git a/SUPLA/Resources/Base.lproj/SAZWaveConfigurationWizardVC.xib b/SUPLA/Resources/Base.lproj/SAZWaveConfigurationWizardVC.xib index d97d764de..97790d6aa 100644 --- a/SUPLA/Resources/Base.lproj/SAZWaveConfigurationWizardVC.xib +++ b/SUPLA/Resources/Base.lproj/SAZWaveConfigurationWizardVC.xib @@ -170,7 +170,7 @@ - + @@ -189,7 +189,7 @@ - + @@ -247,7 +247,7 @@ - + @@ -288,7 +288,7 @@ - + @@ -373,7 +373,7 @@ - + @@ -588,7 +588,7 @@ - + @@ -640,7 +640,7 @@ - + @@ -901,7 +901,7 @@ - + @@ -982,7 +982,7 @@ - + @@ -1008,12 +1008,12 @@ - - - + + + diff --git a/SUPLA/Resources/Default.strings b/SUPLA/Resources/Default.strings index be70ad17c..f39ef109e 100644 --- a/SUPLA/Resources/Default.strings +++ b/SUPLA/Resources/Default.strings @@ -92,6 +92,10 @@ "app_settings.location_label" = "Location"; "settings_show_labels" = "Show bottom menu labels"; "settings_dark_mode" = "Dark mode"; +"settings_lock_screen" = "Lock screen"; +"settings_lock_screen_none" = "None"; +"settings_lock_screen_app" = "App"; +"settings_lock_screen_accounts" = "Accounts"; /* Standard Detail */ "standard_detail_general_tab" = "General"; @@ -222,3 +226,43 @@ /* Facade blind detail */ "facade_blinds_slat_tilt" = "Tilt: "; "facade_blinds_no_tilt" = "Missing configuration"; + +/* Status */ +"status_initializing" = "Starting..."; +"status_connecting" = "Connecting..."; +"status_disconnecting" = "Disconnecting..."; +"status_awaiting_network" = "Awaiting network..."; +"status_try_again" = "Try again"; +"status_unknown_error" = "Unknown error"; +"status_temporarily_unavailable" = "Service temporarily unavailable"; +"status_incorrect_data" = "Incorrect Email Address or Password"; +"status_bad_credentials" = "Bad credentials"; +"status_client_limit_exceeded" = "Client limit exceeded"; +"status_device_disabled" = "Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website."; +"status_access_id_disabled" = "Access Identifier is disabled"; +"status_registration_disabled" = "New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website."; +"status_access_id_not_assigned" = "Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website."; +"status_access_id_inactive" = "Access Identifier inactive."; +"status_host_not_found" = "Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created."; + +/* Pin setup */ +"pin_setup_title" = "Configure PIN"; +"pin_setup_header" = "Define your PIN code"; +"pin_setup_repeat" = "Repeat the PIN"; +"pin_setup_entry_different" = "PIN's are not equal!"; +"pin_setup_use_biometric" = "Biometric Authentication: "; +"pin_setup_biometric_not_enrolled" = "Biometric not available. If you want to use biometric go to settings and enrol your biometrics."; + +/* Lock screen */ +"lock_screen_hello" = "Welcome back!"; +"lock_screen_enter_pin" = "Enter PIN"; +"lock_screen_remove_pin" = "PIN removal"; +"lock_screen_confirm_authorize_app" = "Authorization scope change - application"; +"lock_screen_confirm_authorize_accounts" = "Authorization scope change - accounts"; +"lock_screen_wrong_pin" = "Entered PIN is wrong!"; +"lock_screen_forgotten_code" = "I don\'t remember my PIN code"; +"lock_screen_forgotten_code_title" = "Access code"; +"lock_screen_forgotten_code_message" = "If you don\'t remember the code, uninstall app and install it again. After that configure your Supla Cloud account again.\n\nDon\'t create new account. All your data and devices will stay registered in your Supla Cloud account."; +"lock_screen_forgotten_code_button" = "OK"; +"biometric_prompt_subtitle" = "Unlock using biometric"; +"lock_screen_pin_locked" = "To many failures. PIN entry locked for %@."; diff --git a/SUPLA/Features/Details/LegacyDetail/LegacyDetailNavigationCoordinator.swift b/SUPLA/Resources/Extensions/Color+Supla.swift similarity index 56% rename from SUPLA/Features/Details/LegacyDetail/LegacyDetailNavigationCoordinator.swift rename to SUPLA/Resources/Extensions/Color+Supla.swift index b193d6642..d71c920c5 100644 --- a/SUPLA/Features/Details/LegacyDetail/LegacyDetailNavigationCoordinator.swift +++ b/SUPLA/Resources/Extensions/Color+Supla.swift @@ -1,3 +1,4 @@ +// /* Copyright (C) AC SOFTWARE SP. Z O.O. @@ -15,24 +16,23 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ + -import Foundation +import SwiftUI -class LegacyDetailNavigationCoordinator: BaseNavigationCoordinator { - - private let detailType: LegacyDetailType - private let channelBase: SAChannelBase - - private lazy var _viewController: DetailViewController = { - DetailViewController(detailViewType: detailType, channelBase: channelBase) - }() - - override var viewController: UIViewController { _viewController } - - init(detailType: LegacyDetailType, channelBase: SAChannelBase) { - self.detailType = detailType - self.channelBase = channelBase +extension Color { + struct Supla { + static let primary = Color(UIColor.primary) + static let primaryVariant = Color(UIColor.primaryVariant) + + static let background = Color(UIColor.background) + static let surface = Color(UIColor.surface) + + static let onBackground = Color(UIColor.onBackground) + static let onPrimary = Color(UIColor.onPrimary) + + static let blue = Color(UIColor.blue) + static let disabled = Color(UIColor.disabled) + static let error = Color(UIColor.error) } - } - diff --git a/SUPLA/Resources/Extensions/Font+Supla.swift b/SUPLA/Resources/Extensions/Font+Supla.swift new file mode 100644 index 000000000..5c4eb72fc --- /dev/null +++ b/SUPLA/Resources/Extensions/Font+Supla.swift @@ -0,0 +1,44 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import SwiftUI + +extension Font { + enum Supla { + static let displayLarge: Font = .custom("OpenSans-Light", size: 60) + static let displayMedium: Font = .custom("OpenSans", size: 48) + static let displaySmall: Font = .custom("OpenSans", size: 34) + + static let headlineLarge: Font = .custom("OpenSans", size: 34) + static let headlineMedium: Font = .custom("OpenSans", size: 24) + static let headlineSmall: Font = .custom("OpenSans", size: 17) + + static let titleLarge: Font = .custom("OpenSans", size: 22) + static let titleMedium: Font = .custom("OpenSans-SemiBold", size: 16) + static let titleSmall: Font = .custom("OpenSans-SemiBold", size: 14) + + static let bodyLarge: Font = .custom("OpenSans", size: 16) + static let bodyMedium: Font = .custom("OpenSans", size: 14) + static let bodySmall: Font = .custom("OpenSans", size: 12) + + static let labelLarge: Font = .custom("OpenSans-Medium", size: 17) + static let labelMedium: Font = .custom("OpenSans-SemiBold", size: 12) + static let labelSmall: Font = .custom("OpenSans-SemiBold", size: 10) + } +} diff --git a/SUPLA/Resources/Extensions/String+Icons.swift b/SUPLA/Resources/Extensions/String+Icons.swift index c1b6c21ba..1b0baa68d 100644 --- a/SUPLA/Resources/Extensions/String+Icons.swift +++ b/SUPLA/Resources/Extensions/String+Icons.swift @@ -55,6 +55,7 @@ extension String { static let warning = "channel_warning_level1" static let error = "channel_warning_level2" + static let statusError = "icon_status_error" static let arrowRight = "icon_arrow_right" static let arrowDoubleRight = "icon_arrow_double_right" @@ -67,6 +68,8 @@ extension String { static let arrowRevealTap = "icon_arrow_reveal_tap" static let arrowRevealHold = "icon_arrow_reveal_hold" + static let fingerprint = "icon_fingerprint" + // MARK: Functions static let fncUnknown = "unknown_channel" // Electricitymeter @@ -142,6 +145,8 @@ extension String { struct Image { static let logo = "logo" + static let logoLight = "logo_light" + static let logoWithName = "logo_with_name" static let garageContent = "garage_content" } } diff --git a/SUPLA/Resources/Extensions/UIColor+Supla.swift b/SUPLA/Resources/Extensions/UIColor+Supla.swift index 059414b7d..e6481bd4d 100644 --- a/SUPLA/Resources/Extensions/UIColor+Supla.swift +++ b/SUPLA/Resources/Extensions/UIColor+Supla.swift @@ -53,6 +53,8 @@ extension UIColor { static let chartGpmBorder = UIColor(argb: 0xFF005F6E) static let chartGpmShadow = UIColor(argb: 0x3398C4CA) + @objc static let toolbar = UIColor(named: "Colors/toolbar")! + @objc static let separator = UIColor(named: "Colors/separator")! static let separatorLight = UIColor(named: "Colors/separator_light")! diff --git a/SUPLA/Resources/Extensions/UIImage+Supla.swift b/SUPLA/Resources/Extensions/UIImage+Supla.swift index cdec96ad2..fd03c675a 100644 --- a/SUPLA/Resources/Extensions/UIImage+Supla.swift +++ b/SUPLA/Resources/Extensions/UIImage+Supla.swift @@ -52,6 +52,7 @@ extension UIImage { static let iconWarning = UIImage(named: .Icons.warning) static let iconError = UIImage(named: .Icons.error) + static let iconStatusError = UIImage(named: .Icons.statusError) static let iconArrowRight = UIImage(named: .Icons.arrowRight) static let iconArrowDoubleRight = UIImage(named: .Icons.arrowDoubleRight) @@ -64,6 +65,8 @@ extension UIImage { static let iconArrowRevealTap = UIImage(named: .Icons.arrowRevealTap) static let iconArrowRevealHold = UIImage(named: .Icons.arrowRevealHold) + static let iconFingerprint = UIImage(named: .Icons.fingerprint) + // MARK: Functions static let fncUnknown = UIImage(named: .Icons.fncUnknown) // Thermostat @@ -77,5 +80,6 @@ extension UIImage { static let thumbCool = UIImage(named: .Icons.thumbCool) @objc static let logo = UIImage(named: .Image.logo) + @objc static let logoLight = UIImage(named: .Image.logoLight) @objc static let garageContent = UIImage(named: .Image.garageContent) } diff --git a/SUPLA/Resources/Resources.xcassets/Colors/primary.colorset/Contents.json b/SUPLA/Resources/Resources.xcassets/Colors/primary.colorset/Contents.json index 89cac0ec5..25c097f48 100644 --- a/SUPLA/Resources/Resources.xcassets/Colors/primary.colorset/Contents.json +++ b/SUPLA/Resources/Resources.xcassets/Colors/primary.colorset/Contents.json @@ -11,24 +11,6 @@ } }, "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x03", - "green" : "0x3A", - "red" : "0x00" - } - }, - "idiom" : "universal" } ], "info" : { diff --git a/SUPLA/Resources/Resources.xcassets/Colors/toolbar.colorset/Contents.json b/SUPLA/Resources/Resources.xcassets/Colors/toolbar.colorset/Contents.json new file mode 100644 index 000000000..89cac0ec5 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Colors/toolbar.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x1E", + "green" : "0xA7", + "red" : "0x12" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x03", + "green" : "0x3A", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_fingerprint.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_fingerprint.imageset/Contents.json new file mode 100644 index 000000000..6cf75f4ac --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_fingerprint.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_fingerprint.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_fingerprint.imageset/ic_fingerprint.svg b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_fingerprint.imageset/ic_fingerprint.svg new file mode 100644 index 000000000..e1e832ac9 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_fingerprint.imageset/ic_fingerprint.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_status_error.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_status_error.imageset/Contents.json new file mode 100644 index 000000000..a2f87247e --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_status_error.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "error.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_status_error.imageset/error.svg b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_status_error.imageset/error.svg new file mode 100644 index 000000000..59ce06f69 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_status_error.imageset/error.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/logo_light.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/logo_light.imageset/Contents.json new file mode 100644 index 000000000..f57fc3bbf --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/logo_light.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Supla logo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/logo_light.imageset/Supla logo.svg b/SUPLA/Resources/Resources.xcassets/Images/logo_light.imageset/Supla logo.svg new file mode 100644 index 000000000..c3fe9daa3 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/logo_light.imageset/Supla logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/logo_with_name.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/logo_with_name.imageset/Contents.json new file mode 100644 index 000000000..d08528a0a --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/logo_with_name.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "logo_with_icon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/logo_with_name.imageset/logo_with_icon.svg b/SUPLA/Resources/Resources.xcassets/Images/logo_with_name.imageset/logo_with_icon.svg new file mode 100644 index 000000000..7d12bff76 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/logo_with_name.imageset/logo_with_icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/SUPLA/Resources/Strings.swift b/SUPLA/Resources/Strings.swift index ea288c151..2978d732a 100644 --- a/SUPLA/Resources/Strings.swift +++ b/SUPLA/Resources/Strings.swift @@ -56,6 +56,10 @@ struct Strings { static let locationLabel = "app_settings.location_label".toLocalized() static let showLabels = "settings_show_labels".toLocalized() static let nightMode = "settings_dark_mode".toLocalized() + static let lockScreen = "settings_lock_screen".toLocalized() + static let lockScreenNone = "settings_lock_screen_none".toLocalized() + static let lockScreenApp = "settings_lock_screen_app".toLocalized() + static let lockScreenAccounts = "settings_lock_screen_accounts".toLocalized() } struct AccountCreation { @@ -264,7 +268,6 @@ struct Strings { static let cancel = NSLocalizedString("Cancel", comment: "") static let close = NSLocalizedString("Close", comment: "") static let open = NSLocalizedString("Open", comment: "") - static let unknownError = NSLocalizedString("Unknown error", comment: "") static let save = "save".toLocalized() static let hourFormat = "general_hour_format".toLocalized() @@ -333,8 +336,6 @@ struct Strings { } struct AuthorizationDialog { - static let unauthorized = NSLocalizedString("Incorrect Email Address or Password", comment: "") - static let unavailable = NSLocalizedString("Service temporarily unavailable", comment: "") static let timeout = NSLocalizedString("Time exceeded. Try again.", comment: "") static let cloudTitle = NSLocalizedString("Please enter your Supla Cloud login details.", comment: "") static let privateTitle = NSLocalizedString("Enter superuser credentials", comment: "") @@ -346,6 +347,49 @@ struct Strings { struct DeviceCatalog { static let menu = "menu_device_catalog".toLocalized() } + + struct Status { + static let initializing = "status_initializing".toLocalized() + static let connecting = "status_connecting".toLocalized() + static let disconnecting = "status_disconnecting".toLocalized() + static let awaitingNetwork = "status_awaiting_network".toLocalized() + static let tryAgain = "status_try_again".toLocalized() + static let errorUnknown = "status_unknown_error".toLocalized() + static let errorUnavailable = "status_temporarily_unavailable".toLocalized() + static let errorInvalidData = "status_incorrect_data".toLocalized() + static let errorBadCredentials = "status_bad_credentials".toLocalized() + static let errorClientLimitExceeded = "status_client_limit_exceeded".toLocalized() + static let errorDeviceDisabled = "status_device_disabled".toLocalized() + static let errorAccessIdDisabled = "status_access_id_disabled".toLocalized() + static let errorRegistrationDisabled = "status_registration_disabled".toLocalized() + static let errorAccessIdNotAssigned = "status_access_id_not_assigned".toLocalized() + static let errorAccessIdInactive = "status_access_id_inactive".toLocalized() + static let errorHostNotFound = "status_host_not_found".toLocalized() + } + + struct PinSetup { + static let title = "pin_setup_title".toLocalized() + static let header = "pin_setup_header".toLocalized() + static let repeatPin = "pin_setup_repeat".toLocalized() + static let different = "pin_setup_entry_different".toLocalized() + static let useBiometric = "pin_setup_use_biometric".toLocalized() + static let biometricNotEnrolled = "pin_setup_biometric_not_enrolled".toLocalized() + } + + struct LockScreen { + static let hello = "lock_screen_hello".toLocalized() + static let enterPin = "lock_screen_enter_pin".toLocalized() + static let removePin = "lock_screen_remove_pin".toLocalized() + static let confirmAuthorizeApp = "lock_screen_confirm_authorize_app".toLocalized() + static let confirmAuthorizeAccounts = "lock_screen_confirm_authorize_accounts".toLocalized() + static let wrongPin = "lock_screen_wrong_pin".toLocalized() + static let forgottenCode = "lock_screen_forgotten_code".toLocalized() + static let forgottenCodeTitle = "lock_screen_forgotten_code_title".toLocalized() + static let forgottenCodeMessage = "lock_screen_forgotten_code_message".toLocalized() + static let forgottenCodeButton = "lock_screen_forgotten_code_button".toLocalized() + static let biometricPromptReason = "biometric_prompt_subtitle".toLocalized() + static let pinLocked = "lock_screen_pin_locked".toLocalized() + } } extension String { diff --git a/SUPLA/Resources/cs.lproj/Localizable.strings b/SUPLA/Resources/cs.lproj/Localizable.strings index f4182d326..56e2f31e3 100644 --- a/SUPLA/Resources/cs.lproj/Localizable.strings +++ b/SUPLA/Resources/cs.lproj/Localizable.strings @@ -45,17 +45,7 @@ "Garage door opening sensor" = "CSnímač otevření garážových dveří"; "Door opening sensor" = "Snímač otevření dveří"; "Roller shutter opening sensor" = "Snímač otevření rolet"; -"Connecting..." = "Připojování..."; -"Unknown error" = "Neznámá chyba"; -"Service temporarily unavailable" = "Služba dočasně nedostupná"; -"Bad credentials" = "Chybné pověření"; -"Client limit exceeded" = "Limit klientské aplikace překročen"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "Zařízení je vypnuté"; -"Access Identifier is disabled" = "Identifikátor přístupu je vypnutý"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "Registrace je momentálně zakázána. Chcete-li ji povolit, přejděte na kartu \"Smartphone\" na stránce \"Supla Cloud\""; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "Toto zařízení nemá přiřazené ID přístupu. Přejděte na \"Smartphone\" na \"Supla Cloud\" a vyberte příslušné ID na kartě"; "Incompatible server version" = "Nekompatibilní verze serveru"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "Adresa serveru nebyla nalezena"; "RGB Lighting" = "Osvětlení RGB"; "Dimmer and RGB lighting" = "Reostat a osvětlení RGB"; "Dimmer" = "Reostat"; @@ -199,7 +189,6 @@ "The glass sections are exposed for more than 20 hours, which may adversely affect their life. It is recommended to cover all sections for a minimum of 4 hours in order to regenerate them." = "Části skla jsou vystaveny světlu déle než 20 hodin, což může ovlivnit jejich životnost. Pro jejich obnovu se doporučuje zakrýt všechny části na minimálně 4 hodiny."; "Planned regeneration is in progress." = "Probíhá plánovaná obnova."; "Regeneration initiated after 20 hours of operation is in progress." = "Obnova byla zahájena po 20h provozní činnosti."; -"Incorrect Email Address or Password" = "Nesprávná e-mailová adresa nebo heslo"; "Channel Id" = "Id kanálu"; "Back" = "Zpět"; "The z-wave bridge is not responding. Check if the bridge is connected to the server." = "Z-wave můstek neodpovídá. Zkontrolujte zda je můstek připojen k serveru."; @@ -279,7 +268,6 @@ "Closing" = "Zavření"; "Percent of opening" = "Procento otevření"; "Percent of closing" = "Procento zavření"; -"Access Identifier inactive." = "Identifikátor přístupu není aktivní."; "Your account" = "Váš účet"; "Your accounts" = "Vaše účty"; "Accounts" = "Účty"; @@ -313,3 +301,17 @@ "roller_shutter_motor_problem" = "Problém s motorem / Neočekávané zastavení."; "roller_shutter_calibration" = "Kalibrace"; "roller_shutter_start_calibration_message" = "Určitě chcete začít s kalibrací?"; + +/* Status */ +"status_connecting" = "Připojování..."; +"status_unknown_error" = "Neznámá chyba"; +"status_temporarily_unavailable" = "Služba dočasně nedostupná"; +"status_incorrect_data" = "Nesprávná e-mailová adresa nebo heslo"; +"status_bad_credentials" = "Chybné pověření"; +"status_client_limit_exceeded" = "Limit klientské aplikace překročen"; +"status_device_disabled" = "Zařízení je vypnuté"; +"status_access_id_disabled" = "Identifikátor přístupu je vypnutý"; +"status_registration_disabled" = "Registrace je momentálně zakázána. Chcete-li ji povolit, přejděte na kartu \"Smartphone\" na stránce \"Supla Cloud\""; +"status_access_id_not_assigned" = "Toto zařízení nemá přiřazené ID přístupu. Přejděte na \"Smartphone\" na \"Supla Cloud\" a vyberte příslušné ID na kartě"; +"status_access_id_inactive" = "Identifikátor přístupu není aktivní."; +"status_host_not_found" = "Adresa serveru nebyla nalezena"; diff --git a/SUPLA/Resources/de.lproj/Localizable.strings b/SUPLA/Resources/de.lproj/Localizable.strings index ca6a4988a..fc0ebd9ae 100644 --- a/SUPLA/Resources/de.lproj/Localizable.strings +++ b/SUPLA/Resources/de.lproj/Localizable.strings @@ -45,17 +45,7 @@ "Garage door opening sensor" = "Garagentor-Öffnungssensor"; "Door opening sensor" = "Tür-Öffnungssensor"; "Roller shutter opening sensor" = "Rolladen-Öffnungssensor"; -"Connecting..." = "Verbindung..."; -"Unknown error" = "Unbekannter Fehler"; -"Service temporarily unavailable" = "Service momentan nicht verfügbar"; -"Bad credentials" = "Schlechte Referenzen"; -"Client limit exceeded" = "Klient-Grenze überschritten"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "Gerät ist deaktiviert"; -"Access Identifier is disabled" = "Die Zugriffskennung ist deaktiviert"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "Die Client-Registrierung ist deaktiviert. Bitte gehen Sie zu \"Smartphones\" auf \"Supla Cloud\", um es zu aktivieren."; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "Diesem Client wurde keine Zugriffskennung zugewiesen. Bitte gehen Sie zu \"Smartphones\" auf \"Supla Cloud\" und holen Sie sich eine gültige ID."; "Incompatible server version" = "Nicht kompatible Server-Version"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "Host nicht gefunden"; "RGB Lighting" = "RGB- Beleuchtungskontroller"; "Dimmer and RGB lighting" = "Helligkeitsregler und RGB-Beleuchtungskontroller"; "Dimmer" = "Helligkeitsregler"; @@ -199,7 +189,6 @@ "The glass sections are exposed for more than 20 hours, which may adversely affect their life. It is recommended to cover all sections for a minimum of 4 hours in order to regenerate them." = "Die Scheibensektionen sind über 20 h aufgedeckt, was negativ ihre Lebensdauer beeinflussen kann. Verdeckung aller Sektionen für mind. 4 h zu ihrer Regeneration ist empfohlen."; "Planned regeneration is in progress." = "Geplante Regeneration läuft."; "Regeneration initiated after 20 hours of operation is in progress." = "Es läuft die nach 20 Betriebsstunden begonnene Regeneration."; -"Incorrect Email Address or Password" = "Falsche E-Mail-Adresse oder Kennwort"; "Channel Id" = "Kanal-ID"; "Back" = "Zurück"; "The z-wave bridge is not responding. Check if the bridge is connected to the server." = "Die Z-Wave-Brücke reagiert nicht."; @@ -279,7 +268,6 @@ "Closing" = "Schließungen"; "Percent of opening" = "Prozentsatz der Öffnung"; "Percent of closing" = "Prozentsatz des Schließens"; -"Access Identifier inactive." = "Zugriffskennung nicht aktiv"; "Your account" = "Ihr Konto"; "Your accounts" = "Ihre Kontos"; "Accounts" = "Kontos"; @@ -344,6 +332,10 @@ /* App Settings */ "settings_dark_mode" = "Nacht-Modus"; +"settings_lock_screen" = "PIN Eingabe"; +"settings_lock_screen_none" = "Keine"; +"settings_lock_screen_app" = "Anwendung"; +"settings_lock_screen_accounts" = "Konten"; /* Standard Detail */ "standard_detail_general_tab" = "Allgemein"; @@ -461,3 +453,43 @@ /* Facade blind detail */ "facade_blinds_slat_tilt" = "Neigung: "; "facade_blinds_no_tilt" = "Fehlende Konfiguration"; + +/* Status */ +"status_initializing" = "Starten..."; +"status_connecting" = "Verbindung..."; +"status_disconnecting" = "Abschalten..."; +"status_awaiting_network" = "Warten auf netzwerk..."; +"status_try_again" = "Erneut versuchen"; +"status_unknown_error" = "Unbekannter Fehler"; +"status_temporarily_unavailable" = "Service momentan nicht verfügbar"; +"status_incorrect_data" = "Falsche E-Mail-Adresse oder Kennwort"; +"status_bad_credentials" = "Schlechte Referenzen"; +"status_client_limit_exceeded" = "Klient-Grenze überschritten"; +"status_device_disabled" = "Gerät ist deaktiviert. Bitte in \"Supla Cloud\" einlogen und dieses Gerät einschalten."; +"status_access_id_disabled" = "Die Zugriffskennung ist deaktiviert"; +"status_registration_disabled" = "Die Client-Registrierung ist deaktiviert. Bitte gehen Sie zu \"Smartphones\" auf \"Supla Cloud\", um es zu aktivieren."; +"status_access_id_not_assigned" = "Diesem Client wurde keine Zugriffskennung zugewiesen. Bitte gehen Sie zu \"Smartphones\" auf \"Supla Cloud\" und holen Sie sich eine gültige ID."; +"status_access_id_inactive" = "Zugriffskennung nicht aktiv."; +"status_host_not_found" = "Host nicht gefunden. Überprüfen Sie bitte ob die Internetverbindung korrekt funktioniert und das Konto korrekt erstellt wurde."; + +/* Pin setup */ +"pin_setup_title" = "PIN konfigurieren"; +"pin_setup_header" = "Definieren Sie Ihren PIN-Code"; +"pin_setup_repeat" = "PIN-Code wiederholen"; +"pin_setup_entry_different" = "PIN-Codes sind unterschiedlich!"; +"pin_setup_use_biometric" = "Biometrische Authentifizierung: "; +"pin_setup_biometric_not_enrolled" = "Biometrische Authentifizierung nicht verfügbar. Wenn Sie die biometrische Authentifizierung nuthen wollen, bitte zuerst in System Einstellungen konfigurieren."; + +/* Lock screen */ +"lock_screen_hello" = "Willkommen zurück!"; +"lock_screen_enter_pin" = "PIN Eingabe"; +"lock_screen_remove_pin" = "PIN Löschen"; +"lock_screen_confirm_authorize_app" = "Änderung des Berechtigungsumfangs - Anwendung"; +"lock_screen_confirm_authorize_accounts" = "Änderung des Berechtigungsumfangs - Konten"; +"lock_screen_wrong_pin" = "Eingegebenes PIN-Code ist falsch!"; +"lock_screen_forgotten_code" = "PIN-Code vergessen"; +"lock_screen_forgotten_code_title" = "Zugangscode"; +"lock_screen_forgotten_code_message" = "Wenn Sie sich nicht an Ihren Zugangscode erinnern können, bitte die App löschen und neu installieren. Wenn die App neu installiert wird, bitte Ihres Kontodaten nochmal eingeben.\n\nSie sollten kein neues Konto anlegen. Die Daten und Geräte bleiben verfügbar in das Supla Cloud Konto."; +"lock_screen_forgotten_code_button" = "Einverstanden"; +"biometric_prompt_subtitle" = "Entsperren mittels Biometrie"; +"lock_screen_pin_locked" = "Zu viele Fehlversuche. PIN-Eingabe für %@ gesperrt."; diff --git a/SUPLA/Resources/el.lproj/Localizable.strings b/SUPLA/Resources/el.lproj/Localizable.strings index 070713db0..3abd50038 100644 --- a/SUPLA/Resources/el.lproj/Localizable.strings +++ b/SUPLA/Resources/el.lproj/Localizable.strings @@ -45,17 +45,7 @@ "Garage door opening sensor" = "Αισθητήρας ανοίγματος της γκαραζόπορτας"; "Door opening sensor" = "Αισθητήρας ανοίγματος πόρτας"; "Roller shutter opening sensor" = ">Αισθητήρας ανοίγματος περσίδων"; -"Connecting..." = "Σύνδεση..."; -"Unknown error" = "Άγνωστο σφάλμα"; -"Service temporarily unavailable" = "Υπηρεσία προσωρινά μη διαθέσιμη"; -"Bad credentials" = "Λανθασμένα διαπιστευτήρια"; -"Client limit exceeded" = "Υπέρβαση ορίου των εφαρμογών πελάτη"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "Η συσκευή είναι απενεργοποιημένη"; -"Access Identifier is disabled" = "Το αναγνωριστικό πρόσβασης είναι απενεργοποιημένο"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "Η εγγραφή είναι αυτή τη στιγμή απενεργοποιημένη. Για να την ενεργοποιήσετε, μεταβείτε στην καρτέλα \"Smartphones\" στη σελίδα \"Supla Cloud\""; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "Αυτή η συσκευή δεν έχει παραχωρημένο αναγνωριστικό πρόσβασης. Μεταβείτε στη σελίδα \"Supla Cloud\" στην καρτέλα \"Smartphones\" και παραχωρήστε το κατάλληλο αναγνωριστικό."; "Incompatible server version" = "Μη συμβατή έκδοση διακομιστή"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "Η διεύθυνση του διακομιστή δεν βρέθηκε"; "RGB Lighting" = "Φωτισμός RGB"; "Dimmer and RGB lighting" = "Ρεοστάτης και φωτισμός RGB"; "Dimmer" = "Ρεοστάτης"; @@ -199,7 +189,6 @@ "The glass sections are exposed for more than 20 hours, which may adversely affect their life. It is recommended to cover all sections for a minimum of 4 hours in order to regenerate them." = "Τμήματα του γυαλιού εκτίθενται για περισσότερο από 20 ώρες, γεγονός που μπορεί να επηρεάσει αρνητικά τη διάρκεια ζωής τους. Συνιστάται η κάλυψη όλων των τμημάτων για τουλάχιστον 4 ώρες προκειμένου να αναγεννηθούν."; "Planned regeneration is in progress." = "Η προγραμματισμένη αναγέννηση βρίσκεται σε εξέλιξη."; "Regeneration initiated after 20 hours of operation is in progress." = "Η αναγέννηση βρίσκεται σε εξέλιξη μετά από 20 ώρες εργασίας."; -"Incorrect Email Address or Password" = "Λάθος διεύθυνση ηλεκτρονικού ταχυδρομείου ή κωδικός πρόσβασης"; "Channel Id" = "Αναγνωριστικό καναλιού"; "Back" = "Επιστροφή"; "The z-wave bridge is not responding. Check if the bridge is connected to the server." = "Η γέφυρα του κύματος Ζ δεν αποκρίνεται. Ελέγξτε εάν η γέφυρα είναι συνδεδεμένη στον διακομιστή."; @@ -279,7 +268,6 @@ "Closing" = "Κλείσιμο"; "Percent of opening" = "Ποσοστό ανοίγματος"; "Percent of closing" = "Ποσοστό κλεισίματος"; -"Access Identifier inactive." = "Το αναγνωριστικό πρόσβασης είναι ανενεργό."; "Your account" = "Ο λογαριασμός σας"; "Your accounts" = "Οι λογαριασμοί σας"; "Accounts" = "Λογαριασμοί"; @@ -313,3 +301,17 @@ "roller_shutter_motor_problem" = "Πρόβλημα κινητήρα / Απροσδόκητη διακοπή."; "roller_shutter_calibration" = "Βαθμονόμηση"; "roller_shutter_start_calibration_message" = "Είστε βέβαιοι ότι θέλετε να ξεκινήσετε τη βαθμονόμηση;"; + +/* Status */ +"status_connecting" = "Σύνδεση..."; +"status_unknown_error" = "Άγνωστο σφάλμα"; +"status_temporarily_unavailable" = "Υπηρεσία προσωρινά μη διαθέσιμη"; +"status_incorrect_data" = "Λάθος διεύθυνση ηλεκτρονικού ταχυδρομείου ή κωδικός πρόσβασης"; +"status_bad_credentials" = "Λανθασμένα διαπιστευτήρια"; +"status_client_limit_exceeded" = "Υπέρβαση ορίου των εφαρμογών πελάτη"; +"status_device_disabled" = "Η συσκευή είναι απενεργοποιημένη"; +"status_access_id_disabled" = "Το αναγνωριστικό πρόσβασης είναι απενεργοποιημένο"; +"status_registration_disabled" = "Η εγγραφή είναι αυτή τη στιγμή απενεργοποιημένη. Για να την ενεργοποιήσετε, μεταβείτε στην καρτέλα \"Smartphones\" στη σελίδα \"Supla Cloud\""; +"status_access_id_not_assigned" = "Αυτή η συσκευή δεν έχει παραχωρημένο αναγνωριστικό πρόσβασης. Μεταβείτε στη σελίδα \"Supla Cloud\" στην καρτέλα \"Smartphones\" και παραχωρήστε το κατάλληλο αναγνωριστικό."; +"status_access_id_inactive" = "Το αναγνωριστικό πρόσβασης είναι ανενεργό."; +"status_host_not_found" = "Η διεύθυνση του διακομιστή δεν βρέθηκε"; diff --git a/SUPLA/Resources/es.lproj/Localizable.strings b/SUPLA/Resources/es.lproj/Localizable.strings index 92d00a9a1..1c8238670 100644 --- a/SUPLA/Resources/es.lproj/Localizable.strings +++ b/SUPLA/Resources/es.lproj/Localizable.strings @@ -45,17 +45,7 @@ "Garage door opening sensor" = "Sensor de apertura de la puerta de garaje"; "Door opening sensor" = "Sensor de apertura de la puerta"; "Roller shutter opening sensor" = "Sensor de apertura de las persianas"; -"Connecting..." = "Conectando..."; -"Unknown error" = "Error desconocido"; -"Service temporarily unavailable" = "Servicio temporalmente no disponible"; -"Bad credentials" = "Credenciales incorrectas"; -"Client limit exceeded" = "Se ha superado el límite de aplicaciones del cliente"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "El dispositivo está deshabilitado"; -"Access Identifier is disabled" = "El identificador de acceso está deshabilitado"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "El registro está actualmente desactivado. Para activarlo, vaya a la pestaña \"Smartphones\" en la página \"Supla Cloud\""; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "Este dispositivo no tiene un identificador de acceso asignado. Vaya a la página \"Supla Cloud\" y asigne el identificador adecuado en la pestaña \"Smartphones\"."; "Incompatible server version" = "Versión del servidor incompatible"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "No se ha encontrado la dirección del servidor"; "RGB Lighting" = "Alumbrado RGB"; "Dimmer and RGB lighting" = "Atenuador y alumbrado RGB"; "Dimmer" = "Atenuador"; @@ -199,7 +189,6 @@ "The glass sections are exposed for more than 20 hours, which may adversely affect their life. It is recommended to cover all sections for a minimum of 4 hours in order to regenerate them." = "Las secciones de vidrio están expuestas durante más de 20 horas, lo que puede tener un impacto negativo en su vida útil. Se recomienda cubrir todas las secciones durante un mínimo de 4 horas para regenerarlas."; "Planned regeneration is in progress." = "La regeneración planificada está en curso."; "Regeneration initiated after 20 hours of operation is in progress." = "La regeneración está en curso, iniciada después de 20 horas de funcionamiento."; -"Incorrect Email Address or Password" = "Dirección de correo electrónico o contraseña incorrecta"; "Channel Id" = "ID del Canal"; "Back" = "Atrás"; "The z-wave bridge is not responding. Check if the bridge is connected to the server." = "Puente z-wave no responde. Verifica si el puente está conectado con al servidor."; @@ -279,7 +268,6 @@ "Closing" = "Cierres"; "Percent of opening" = "Porcentaje de apertura"; "Percent of closing" = "Porcentaje de cierre"; -"Access Identifier inactive." = "Identificación de acceso inactiva."; "Your account" = "Tu cuenta"; "Your accounts" = "Tus cuentas"; "Accounts" = "Cuentas"; @@ -313,3 +301,17 @@ "roller_shutter_motor_problem" = "Problema de motor / Parada inesperada."; "roller_shutter_calibration" = "Calibración"; "roller_shutter_start_calibration_message" = "¿Está seguro de que desea iniciar la calibración?"; + +/* Status */ +"status_connecting" = "Conectando..."; +"status_unknown_error" = "Error desconocido"; +"status_temporarily_unavailable" = "Servicio temporalmente no disponible"; +"status_incorrect_data" = "Dirección de correo electrónico o contraseña incorrecta"; +"status_bad_credentials" = "Credenciales incorrectas"; +"status_client_limit_exceeded" = "Se ha superado el límite de aplicaciones del cliente"; +"status_device_disabled" = "El dispositivo está deshabilitado"; +"status_access_id_disabled" = "El identificador de acceso está deshabilitado"; +"status_registration_disabled" = "El registro está actualmente desactivado. Para activarlo, vaya a la pestaña \"Smartphones\" en la página \"Supla Cloud\""; +"status_access_id_not_assigned" = "Este dispositivo no tiene un identificador de acceso asignado. Vaya a la página \"Supla Cloud\" y asigne el identificador adecuado en la pestaña \"Smartphones\"."; +"status_access_id_inactive" = "Identificación de acceso inactiva."; +"status_host_not_found" = "No se ha encontrado la dirección del servidor"; diff --git a/SUPLA/Resources/fr.lproj/Localizable.strings b/SUPLA/Resources/fr.lproj/Localizable.strings index 7ba107157..58c8ee487 100644 --- a/SUPLA/Resources/fr.lproj/Localizable.strings +++ b/SUPLA/Resources/fr.lproj/Localizable.strings @@ -45,17 +45,7 @@ "Garage door opening sensor" = "Capteur d'ouverture de porte garage"; "Door opening sensor" = "Capteur d'ouverture de porte"; "Roller shutter opening sensor" = "Capteur d'ouverture des volet"; -"Connecting..." = "Connexion en cours..."; -"Unknown error" = "Erreur inconnue"; -"Service temporarily unavailable" = "Ce service est temporairement indisponible"; -"Bad credentials" = "Informations d'identification invalides"; -"Client limit exceeded" = "Vous avez dépassé la limite des applications client-side"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "Le dispositif est éteint"; -"Access Identifier is disabled" = "L'identifiant d'accès est désactivé"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "L'inscription est actuellement désactivée. Pour l'activer, allez vers l'onglet \"Smartphones\" à la page \"Supla Cloud\""; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "Ce dispositif n'a pas d'identifiant d'accès attribué. Allez à la page \"Supla Cloud\" et dans l'onglet \"Smartphones\" attribuez l'identifiant approprié."; "Incompatible server version" = "Incompatibilité de la version du serveur"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "Nous n'avons pas pu trouver cette adresse du serveur"; "RGB Lighting" = "Éclairage RGB"; "Dimmer and RGB lighting" = "Variateur de luminosité et éclairage RGB"; "Dimmer" = "Variateur de luminosité"; @@ -199,7 +189,6 @@ "The glass sections are exposed for more than 20 hours, which may adversely affect their life. It is recommended to cover all sections for a minimum of 4 hours in order to regenerate them." = "Des sections du verre sont exposées pendant plus de 20 heures, ce qui peut affecter leur durée de vie. Il est conseillé de couvrir toutes les sections pendant un minimum de 4 heures pour la régénération."; "Planned regeneration is in progress." = "Régénération programmée en cours."; "Regeneration initiated after 20 hours of operation is in progress." = "Régénération initiée après 20h de fonctionnement en cours."; -"Incorrect Email Address or Password" = "Adresse e-mail ou mot de passe incorrect(e)"; "Channel Id" = "Id du canal"; "Back" = "Retour"; "The z-wave bridge is not responding. Check if the bridge is connected to the server." = "Le pont z-wave ne répond pas. Vérifiez si le pont est connecté au serveur."; @@ -279,7 +268,6 @@ "Closing" = "Fermetures"; "Percent of opening" = "Pourcentage d\'ouverture"; "Percent of closing" = "Pourcentage de clôture"; -"Access Identifier inactive." = "ID d'accès inactif."; "Your account" = "Votre compte"; "Your accounts" = "Vos comptes"; "Accounts" = "Comptes"; @@ -313,3 +301,17 @@ "roller_shutter_motor_problem" = "Problème de moteur / Arrêt inattendu."; "roller_shutter_calibration" = "Calibration"; "roller_shutter_start_calibration_message" = "Êtes-vous sûr de vouloir commencer le calibrage ?"; + +/* Status */ +"status_connecting" = "Connexion en cours..."; +"status_unknown_error" = "Erreur inconnue"; +"status_temporarily_unavailable" = "Ce service est temporairement indisponible"; +"status_incorrect_data" = "Adresse e-mail ou mot de passe incorrect(e)"; +"status_bad_credentials" = "Informations d'identification invalides"; +"status_client_limit_exceeded" = "Vous avez dépassé la limite des applications client-side"; +"status_device_disabled" = "Le dispositif est éteint"; +"status_access_id_disabled" = "L'identifiant d'accès est désactivé"; +"status_registration_disabled" = "L'inscription est actuellement désactivée. Pour l'activer, allez vers l'onglet \"Smartphones\" à la page \"Supla Cloud\"."; +"status_access_id_not_assigned" = "Ce dispositif n'a pas d'identifiant d'accès attribué. Allez à la page \"Supla Cloud\" et dans l'onglet \"Smartphones\" attribuez l'identifiant approprié."; +"status_access_id_inactive" = "ID d'accès inactif."; +"status_host_not_found" = "Nous n'avons pas pu trouver cette adresse du serveur"; diff --git a/SUPLA/Resources/it.lproj/Localizable.strings b/SUPLA/Resources/it.lproj/Localizable.strings index 08b73bece..56f0a9e9a 100644 --- a/SUPLA/Resources/it.lproj/Localizable.strings +++ b/SUPLA/Resources/it.lproj/Localizable.strings @@ -46,13 +46,7 @@ "Garage door opening sensor" = "Sensore di apertura del cancello per garage"; "Door opening sensor" = "Sensore di apertura della porta"; "Roller shutter opening sensor" = "Sensore di apertura di avvolgibili"; -"Connecting..." = "Connessione in corso..."; -"Unknown error" = "Errore sconosciuto"; -"Service temporarily unavailable" = "Servizio temporaneamente non disponibile"; -"Bad credentials" = "Credenziali non valide"; -"Client limit exceeded" = "Limite delle applicazioni dei clienti superato"; "Incompatible server version" = "Versione del server non compatibile"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "L’indirizzo del server non è stato trovato"; "RGB Lighting" = "Illuminazione RGB"; "Dimmer and RGB lighting" = "Varialuce e illuminazione RGB"; "Dimmer" = "Varialuce"; @@ -84,10 +78,6 @@ "https://cloud.supla.org/register" = "https://cloud.supla.org/register?lang=it"; "Create an account" = "Crea un Account"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "Il dispositivo è disabilitato"; -"Access Identifier is disabled" = "L'ID di accesso è disabilitato"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "La registrazione è attualmente disabilitata. Per abilitarlo, vai alla scheda \"Smartphone\" a pagina \"Supla Cloud\""; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "A questo dispositivo non è stato assegnato un ID di accesso. Vai a pagina \"Supla Cloud\" e assegna l'identificatore appropriato nella scheda \"Smartphone\""; "Retrieving data from the server..." = "Scaricamento dati da server..."; "No chart data available." = "Nessun dato per il grafico."; @@ -189,7 +179,6 @@ "Timeout waiting for z-wave bridge response." = "Il tempo di attesa per la risposta del ponte z-wave è scaduto."; "Unexpected bridge response. Code: %i" = "Risposta del ponte inaspettata. Codice: %i"; "Z-Wave bridge" = "Ponte z-wave"; -"Incorrect Email Address or Password" = "Indirizzo Mail o Password errato"; "Channel name" = "Nome Canale"; "Location name" = "Nome Ambiente"; "Default" = "Default"; @@ -329,3 +318,16 @@ "roller_shutter_motor_problem" = "Problema al motore / Arresto inaspettato."; "roller_shutter_calibration" = "Calibrazione"; "roller_shutter_start_calibration_message" = "Confermi di voler avviare la calibrazione?"; + +/* Status */ +"status_connecting" = "Connessione in corso..."; +"status_unknown_error" = "Errore sconosciuto"; +"status_temporarily_unavailable" = "Servizio temporaneamente non disponibile"; +"status_incorrect_data" = "Indirizzo Mail o Password errato"; +"status_bad_credentials" = "Credenziali non valide"; +"status_client_limit_exceeded" = "Limite delle applicazioni dei clienti superato"; +"status_device_disabled" = "Il dispositivo è disabilitato"; +"status_access_id_disabled" = "L'ID di accesso è disabilitato"; +"status_registration_disabled" = "La registrazione è attualmente disabilitata. Per abilitarlo, vai alla scheda \"Smartphone\" a pagina \"Supla Cloud\"."; +"status_access_id_not_assigned" = "A questo dispositivo non è stato assegnato un ID di accesso. Vai a pagina \"Supla Cloud\" e assegna l'identificatore appropriato nella scheda \"Smartphone\""; +"status_host_not_found" = "L’indirizzo del server non è stato trovato"; diff --git a/SUPLA/Resources/lt.lproj/Localizable.strings b/SUPLA/Resources/lt.lproj/Localizable.strings index a524695cc..eb2d8f962 100644 --- a/SUPLA/Resources/lt.lproj/Localizable.strings +++ b/SUPLA/Resources/lt.lproj/Localizable.strings @@ -47,17 +47,7 @@ "Garage door opening sensor" = "Garažo vartų atidarymo jutiklis"; "Door opening sensor" = "Durų atidarymo jutiklis"; "Roller shutter opening sensor" = "Žaliuzių atidengimo jutiklis"; -"Connecting..." = "Jungimasis..."; -"Unknown error" = "Nenustatyta klaida"; -"Service temporarily unavailable" = "Paslauga laikinai nepasiekiama"; -"Bad credentials" = "Klaidingas autentifikavimas"; -"Client limit exceeded" = "Viršutinė klientų programų riba viršyta"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "Įrenginys išjungtas"; -"Access Identifier is disabled" = "rieigos identifikatorius išjungtas"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "Šiuo metu registracija išjungta. Norėdami ją įjungti eikite prie žymės Išmanieji telefonai puslapyje \"Supla Cloud\""; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "Šis įrenginys neturi priskirto prieigos identifikatoriaus. Eikite į puslapį \"Supla Cloud\" ir žymėje Išmanieji telefonai priskirkite tinkamą identifikatorių."; "Incompatible server version" = "Nesuderinama serverio versija"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "Serverio adresas nerastas"; "RGB Lighting" = "RGB apšvietimas"; "Dimmer and RGB lighting" = "Šviesos srauto reguliatorius ir RGB apšvietimas"; "Dimmer" = "Dimmer"; @@ -201,7 +191,6 @@ "The glass sections are exposed for more than 20 hours, which may adversely affect their life. It is recommended to cover all sections for a minimum of 4 hours in order to regenerate them." = "Stiklo sekcijos atidengtos virš 20 valandų, kas gali neigiamai įtakoti jų tarnavimo laiką. Rekomenduojama uždengti visas sekcijas bent 4 valandoms regeneravimui."; "Planned regeneration is in progress." = "Vyksta suplanuotas regeneravimas."; "Regeneration initiated after 20 hours of operation is in progress." = "Vyksta regeneravimas, kuris paleidžiamas po 20 darbo valandų."; -"Incorrect Email Address or Password" = "Neteisingas el. pašto adresas arba slaptažodis"; "Channel Id" = "Kanalo ID"; "Back" = "Atgal"; "The z-wave bridge is not responding. Check if the bridge is connected to the server." = "Z-wave tinklas neatsako. Patikrinkite, ar tiltas prijungtas prie serverio."; @@ -281,7 +270,6 @@ "Closing" = "Uždarymo"; "Percent of opening" = "Atidarymo procentas"; "Percent of closing" = "Uždarymo procentas"; -"Access Identifier inactive." = "Prieigos identifikatorius neaktyvus."; "Your account" = "Jūsų paskyra"; "Your accounts" = "Jūsų paskyros"; "Accounts" = "Paskyros"; @@ -315,3 +303,17 @@ "roller_shutter_motor_problem" = "Variklio problema / Netikėtas sustojimas."; "roller_shutter_calibration" = "Kalibravimas"; "roller_shutter_start_calibration_message" = "Ar tikrai norite pradėti kalibravimą?"; + +/* Status */ +"status_connecting" = "Jungimasis..."; +"status_unknown_error" = "Nenustatyta klaida"; +"status_temporarily_unavailable" = "Paslauga laikinai nepasiekiama"; +"status_incorrect_data" = "Neteisingas el. pašto adresas arba slaptažodis"; +"status_bad_credentials" = "Klaidingas autentifikavimas"; +"status_client_limit_exceeded" = "Viršutinė klientų programų riba viršyta"; +"status_device_disabled" = "Įrenginys išjungtas"; +"status_access_id_disabled" = "Rieigos identifikatorius išjungtas"; +"status_registration_disabled" = "Šiuo metu registracija išjungta. Norėdami ją įjungti eikite prie žymės Išmanieji telefonai puslapyje \"Supla Cloud\""; +"status_access_id_not_assigned" = "Šis įrenginys neturi priskirto prieigos identifikatoriaus. Eikite į puslapį \"Supla Cloud\" ir žymėje Išmanieji telefonai priskirkite tinkamą identifikatorių."; +"status_access_id_inactive" = "Prieigos identifikatorius neaktyvus."; +"status_host_not_found" = "Serverio adresas nerastas"; diff --git a/SUPLA/Resources/nb.lproj/Localizable.strings b/SUPLA/Resources/nb.lproj/Localizable.strings index bfc1fabbc..141dbfab6 100644 --- a/SUPLA/Resources/nb.lproj/Localizable.strings +++ b/SUPLA/Resources/nb.lproj/Localizable.strings @@ -45,17 +45,7 @@ "Garage door opening sensor" = "Garasjeportåpner sensor"; "Door opening sensor" = "Døråpner sensor"; "Roller shutter opening sensor" = "Rullegardineråpner sensor"; -"Connecting..." = "Kobling..."; -"Unknown error" = "Ukjent feil"; -"Service temporarily unavailable" = "Tjenesten er midlertidig utilgjengelig"; -"Bad credentials" = "Feil bekreftelser"; -"Client limit exceeded" = "Grensen på antall kundeapplikasjoner er overskredet"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "Enheten er skrudd av"; -"Access Identifier is disabled" = "Tilgang identifikator er av"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "Registering er for tiden ikke på. For å skru den på gå til fanen \"Smarttelefoner\" på nettsiden \"Supla Cloud\""; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "Denne enheten har ikke tilknyttet tilgang identifikator. Gå til nettsiden \"Supla Cloud\" tilpass riktig identifikator i fanen \"Smarttelefoner\"."; "Incompatible server version" = "Inkompatibel serverversjon"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "Serveradresse ikke funnet"; "RGB Lighting" = "RGB belysning"; "Dimmer and RGB lighting" = "Dimmer og RGB belysning"; "Dimmer" = "Dimmer"; @@ -199,7 +189,6 @@ "The glass sections are exposed for more than 20 hours, which may adversely affect their life. It is recommended to cover all sections for a minimum of 4 hours in order to regenerate them." = "Glasspartiene er eksponert i mer enn 20 timer, noe som kan ha negativ innvirkning på levetiden. Det anbefales å dekke alle seksjoner i minimum 4 timer for å regenerere dem."; "Planned regeneration is in progress." = "Planlagt regenerering pågår."; "Regeneration initiated after 20 hours of operation is in progress." = "Regenerering pågår, startet etter 20 timers drift."; -"Incorrect Email Address or Password" = "Feil e-postadresse eller passord"; "Channel Id" = "Kanal-ID"; "Back" = "Tilbake"; "The z-wave bridge is not responding. Check if the bridge is connected to the server." = "Z-wave bro svarer ikke. Kontroller om broen er tilkoblet serveren."; @@ -279,7 +268,6 @@ "Closing" = "Nedleggelser"; "Percent of opening" = "Åpningsprosent"; "Percent of closing" = "Avslutningsprosent"; -"Access Identifier inactive." = "Tilgangs-ID inaktiv."; "Your account" = "Kontoen din"; "Your accounts" = "Dine kontoer"; "Accounts" = "Kontoer"; @@ -313,3 +301,17 @@ "roller_shutter_motor_problem" = "Motorproblem / Uventet stopp."; "roller_shutter_calibration" = "Kalibrering"; "roller_shutter_start_calibration_message" = "Er du sikker på at du vil starte kalibrering?"; + +/* Status */ +"status_connecting" = "Kobling..."; +"status_unknown_error" = "Ukjent feil"; +"status_temporarily_unavailable" = "Tjenesten er midlertidig utilgjengelig"; +"status_incorrect_data" = "Feil e-postadresse eller passord"; +"status_bad_credentials" = "Feil bekreftelser"; +"status_client_limit_exceeded" = "Grensen på antall kundeapplikasjoner er overskredet"; +"status_device_disabled" = "Enheten er skrudd av"; +"status_access_id_disabled" = "Tilgang identifikator er av"; +"status_registration_disabled" = "istering er for tiden ikke på. For å skru den på gå til fanen \"Smarttelefoner\" på nettsiden \"Supla Cloud\""; +"status_access_id_not_assigned" = "Denne enheten har ikke tilknyttet tilgang identifikator. Gå til nettsiden \"Supla Cloud\" tilpass riktig identifikator i fanen \"Smarttelefoner\"."; +"status_access_id_inactive" = "Tilgangs-ID inaktiv."; +"status_host_not_found" = "Serveradresse ikke funnet"; diff --git a/SUPLA/Resources/nl.lproj/Localizable.strings b/SUPLA/Resources/nl.lproj/Localizable.strings index 56080dc5c..571aa4e58 100644 --- a/SUPLA/Resources/nl.lproj/Localizable.strings +++ b/SUPLA/Resources/nl.lproj/Localizable.strings @@ -45,17 +45,7 @@ "Garage door opening sensor" = "Garagedeur openingssensor"; "Door opening sensor" = "Sensor deuropening"; "Roller shutter opening sensor" = "Rolluiken openen sensor"; -"Connecting..." = "Verbinden..."; -"Unknown error" = "Onbekende fout"; -"Service temporarily unavailable" = "Service tijdelijk niet beschikbaar"; -"Bad credentials" = "Onjuiste inloggegevens"; -"Client limit exceeded" = "Limiet clienttoepassing overschreden"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "Het apparaat is uitgeschakeld"; -"Access Identifier is disabled" = "Toegangs-ID is uitgeschakeld"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "Registratie is momenteel uitgeschakeld. Om dit in te schakelen, gaat u naar het tabblad \"Smartphones\" op pagina \"Supla Cloud\""; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "Aan dit apparaat is geen toegangs-ID toegewezen. Ga naar de pagina \"Supla Cloud\" en wijs de juiste ID toe op het tabblad \"Smartphones\"."; "Incompatible server version" = "Incompatibele serverversie"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "Serveeradres werd niet gevonden"; "RGB Lighting" = "RGB-verlichting"; "Dimmer and RGB lighting" = "Dimmer en RGB-verlichting"; "Dimmer" = "Dimmer"; @@ -199,7 +189,6 @@ "The glass sections are exposed for more than 20 hours, which may adversely affect their life. It is recommended to cover all sections for a minimum of 4 hours in order to regenerate them." = "Beglazingssecties worden meer dan 20 uur blootgesteld, wat hun levensduur nadelig kan beïnvloeden. Aanbevolen wordt alle secties minimaal 4 uur af te dekken om ze te laten regenereren."; "Planned regeneration is in progress." = "Geplande regeneratie is aan de gang."; "Regeneration initiated after 20 hours of operation is in progress." = "De regeneratie die na 20h in werking is gesteld, is aan de gang."; -"Incorrect Email Address or Password" = "Onjuist e-mailadres of wachtwoord"; "Channel Id" = "Kanaal id"; "Back" = "Terug"; "The z-wave bridge is not responding. Check if the bridge is connected to the server." = "Z-wave brug reageert niet. Controleer of de brug met de server is verbonden."; @@ -279,7 +268,6 @@ "Closing" = "Sluitingen"; "Percent of opening" = "Openingspercentage"; "Percent of closing" = "Sluitingspercentage"; -"Access Identifier inactive." = "Toegangs ID inactief."; "Your account" = "Uw rekening"; "Your accounts" = "Uw rekeningen"; "Accounts" = "Rekeningen"; @@ -313,3 +301,17 @@ "roller_shutter_motor_problem" = "Motorprobleem / Onverwachte stilstand."; "roller_shutter_calibration" = "Kalibratie"; "roller_shutter_start_calibration_message" = "Weet je zeker dat je met het ijken wilt beginnen?"; + +/* Status */ +"status_connecting" = "Verbinden..."; +"status_unknown_error" = "Onbekende fout"; +"status_temporarily_unavailable" = "Service tijdelijk niet beschikbaar"; +"status_incorrect_data" = "Onjuist e-mailadres of wachtwoord"; +"status_bad_credentials" = "Onjuiste inloggegevens"; +"status_client_limit_exceeded" = "Limiet clienttoepassing overschreden"; +"status_device_disabled" = "Het apparaat is uitgeschakeld"; +"status_access_id_disabled" = "Toegangs-ID is uitgeschakeld"; +"status_registration_disabled" = "Registratie is momenteel uitgeschakeld. Om dit in te schakelen, gaat u naar het tabblad \"Smartphones\" op pagina \"Supla Cloud\""; +"status_access_id_not_assigned" = "Aan dit apparaat is geen toegangs-ID toegewezen. Ga naar de pagina \"Supla Cloud\" en wijs de juiste ID toe op het tabblad \"Smartphones\"."; +"status_access_id_inactive" = "Toegangs ID inactief."; +"status_host_not_found" = "Serveeradres werd niet gevonden"; diff --git a/SUPLA/Resources/pl.lproj/Localizable.strings b/SUPLA/Resources/pl.lproj/Localizable.strings index e2bec7901..44de0f471 100644 --- a/SUPLA/Resources/pl.lproj/Localizable.strings +++ b/SUPLA/Resources/pl.lproj/Localizable.strings @@ -45,17 +45,7 @@ "Garage door opening sensor" = "Czujnik otwarcia bramy garażowej"; "Door opening sensor" = "Czujnik otwarcia drzwi"; "Roller shutter opening sensor" = "Czujnik otwarcia rolet"; -"Connecting..." = "Łączenie..."; -"Unknown error" = "Nieznany błąd"; -"Service temporarily unavailable" = "Usługa tymczasowo niedostępna"; -"Bad credentials" = "Błędne poświadczenia"; -"Client limit exceeded" = "Przekroczony limit aplikacji klienckich"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "Urządzenie jest wyłączone. Zaloguj się do \"Supla Cloud\" i włącz ten smrtfon w zakładce \"Smartfony\"."; -"Access Identifier is disabled" = "Identyfikator dostępu jest wyłączony"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "Rejestracja jest obecnie wyłączona. Zaloguj się do \"Supla Cloud\" i przełącz rejestrację klientów na AKTYWNĄ w zakładce \"Smartfony\"."; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "To urządzenie nie ma przypisanego identyfikatora dostępu. Przejdź na stronę \"Supla Cloud\" i w zakładce \"Smartfony\" przypisz odpowiedni identyfikator."; "Incompatible server version" = "Niekompatybilna wersja serwera"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "Adres serwera nie został znaleziony. Upewnij się, że posiadasz połączenie z internetem oraz, że konto o wprowadzonym adresie email zostało utworzone."; "RGB Lighting" = "Oświetlenie RGB"; "Dimmer and RGB lighting" = "Ściemniacz i oświetlenie RGB"; "Dimmer" = "Ściemniacz"; @@ -199,7 +189,6 @@ "The glass sections are exposed for more than 20 hours, which may adversely affect their life. It is recommended to cover all sections for a minimum of 4 hours in order to regenerate them." = "Sekcje szyby są odsłonięte dłużej niż 20 godzin co może negatywnie wpłynąć na ich żywotność. Zalecane jest zasłonięcie wszystkich sekcji na minimum 4 godziny w celu ich regeneracji."; "Planned regeneration is in progress." = "Trwa zaplanowana regeneracja."; "Regeneration initiated after 20 hours of operation is in progress." = "Trwa regeneracja zainicjowana po 20h pracy."; -"Incorrect Email Address or Password" = "Błędny adres email lub hasło"; "Channel Id" = "Id kanału"; "Back" = "Wstecz"; "The z-wave bridge is not responding. Check if the bridge is connected to the server." = "Most z-wave nie odpowiada. Sprawdź czy most jest połączony z serwerem."; @@ -281,7 +270,6 @@ "Closing" = "Zamknięcia"; "Percent of opening" = "Procent otwarcia"; "Percent of closing" = "Procent zamknięcia"; -"Access Identifier inactive." = "Identyfikator dostępu nieaktywny."; "Your account" = "Twoje konto"; "Your accounts" = "Twoje konta"; "Accounts" = "Konta"; @@ -373,6 +361,10 @@ "app_settings.location_label" = "Lokalizacja"; "settings_show_labels" = "Pokazuj etykiety w dolnym menu"; "settings_dark_mode" = "Tryb nocny"; +"settings_lock_screen" = "Ekran blokady"; +"settings_lock_screen_none" = "Brak"; +"settings_lock_screen_app" = "Aplikacja"; +"settings_lock_screen_accounts" = "Konta"; /* Standard Detail */ "standard_detail_general_tab" = "Ogólne"; @@ -493,3 +485,43 @@ /* Facade blind detail */ "facade_blinds_slat_tilt" = "Nachylenie: "; "facade_blinds_no_tilt" = "Brak konfiguracji"; + +/* Status */ +"status_initializing" = "Uruchamianie..."; +"status_connecting" = "Łączenie..."; +"status_disconnecting" = "Rozłączanie..."; +"status_awaiting_network" = "Oczekiwanie na sieć..."; +"status_try_again" = "Spróbuj ponownie"; +"status_unknown_error" = "Nieznany błąd"; +"status_temporarily_unavailable" = "Usługa tymczasowo niedostępna"; +"status_incorrect_data" = "Błędny adres email lub hasło"; +"status_bad_credentials" = "Błędne poświadczenia"; +"status_client_limit_exceeded" = "Przekroczony limit aplikacji klienckich"; +"status_device_disabled" = "Urządzenie jest wyłączone. Zaloguj się do \"Supla Cloud\" i włącz ten smrtfon w zakładce \"Smartfony\"."; +"status_access_id_disabled" = "Identyfikator dostępu jest wyłączony"; +"status_registration_disabled" = "Rejestracja jest obecnie wyłączona. Zaloguj się do \"Supla Cloud\" i przełącz rejestrację klientów na AKTYWNĄ w zakładce \"Smartfony\"."; +"status_access_id_not_assigned" = "To urządzenie nie ma przypisanego identyfikatora dostępu. Przejdź na stronę \"Supla Cloud\" i w zakładce \"Smartfony\" przypisz odpowiedni identyfikator."; +"status_access_id_inactive" = "Identyfikator dostępu nieaktywny."; +"status_host_not_found" = "Adres serwera nie został znaleziony. Upewnij się, że posiadasz połączenie z internetem oraz, że konto o wprowadzonym adresie email zostało utworzone."; + +/* Pin setup */ +"pin_setup_title" = "Konfiguracja PIN"; +"pin_setup_header" = "Zdefiniuj swój kod PIN"; +"pin_setup_repeat" = "Powtórz PIN"; +"pin_setup_entry_different" = "Wprowadzone kody różnią się!"; +"pin_setup_use_biometric" = "Uwierzytelnianie biometryczne: "; +"pin_setup_biometric_not_enrolled" = "Uwierzytalnienie biometryczne nie zostało skonfigurowane. Jeżeli chcesz skorzystać z biometrii skonfiguruj ją najpierw w ustawieniach systemowych."; + +/* Lock screen */ +"lock_screen_hello" = "Witaj ponownie!"; +"lock_screen_enter_pin" = "Wprowadź PIN"; +"lock_screen_remove_pin" = "Usuwanie kodu PIN"; +"lock_screen_confirm_authorize_app" = "Zmiana zakresu autoryzacji - aplikacja"; +"lock_screen_confirm_authorize_accounts" = "Zmiana zakresu autoryzacji - konta"; +"lock_screen_wrong_pin" = "Wprowadzono nieprawidłowy kod PIN!"; +"lock_screen_forgotten_code" = "Nie pamiętam kodu dostępu"; +"lock_screen_forgotten_code_title" = "Kod dostępu"; +"lock_screen_forgotten_code_message" = "Jeśli nie pamiętasz kodu dostępu, odinstaluj aplikację z telefonu, a następnie zainstaluj ją ponownie. Po ponownej instalacji, dodaj konfigurację konta z Supla Cloud.\n\nNie zakładaj nowego konta. Dane historyczne i wszystkie urządzenia nie zostaną stracone."; +"lock_screen_forgotten_code_button" = "Rozumiem"; +"biometric_prompt_subtitle" = "Odblokuj używając biometrii"; +"lock_screen_pin_locked" = "Zbyt wiele błędnych prób. Pin został zablokowany na %@."; diff --git a/SUPLA/Resources/pt-PT.lproj/Localizable.strings b/SUPLA/Resources/pt-PT.lproj/Localizable.strings index 910740879..4b345ef80 100644 --- a/SUPLA/Resources/pt-PT.lproj/Localizable.strings +++ b/SUPLA/Resources/pt-PT.lproj/Localizable.strings @@ -45,17 +45,7 @@ "Garage door opening sensor" = "Sensor de abertura da garagem"; "Door opening sensor" = "Sensor de abertura da porta"; "Roller shutter opening sensor" = "Sensor de abertura dos estores"; -"Connecting..." = "Está a ligar..."; -"Unknown error" = "Erro indefinido"; -"Service temporarily unavailable" = "Serviço temporariamente indisponível"; -"Bad credentials" = "Dados de identificação errados"; -"Client limit exceeded" = "Excedeu o limite de aplicações de clientes"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "O dispositivo está desligado"; -"Access Identifier is disabled" = "O ID de acesso está desativado"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "Atualmente o registro está desativado. Para activá-lo, vá para a guia \"Smartphones\" na página \"Supla Cloud\""; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "Este dispositivo não possui um ID de acesso atribuído. Vá para a página \"Supla Cloud\" e atribua o ID apropriado na guia \"Smartphones\"."; "Incompatible server version" = "Versão do servidor incompátivel"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "O endereço do servidor não foi encontrado"; "RGB Lighting" = "Iluminação RGB"; "Dimmer and RGB lighting" = "Controlador de luz e iluminação RGB"; "Dimmer" = "Controlador de luz"; @@ -199,7 +189,6 @@ "The glass sections are exposed for more than 20 hours, which may adversely affect their life. It is recommended to cover all sections for a minimum of 4 hours in order to regenerate them." = "As secções do vidro são expostas por mais de 20 horas, o que pode afetar negativamente a sua vida útil. Recomenda-se cobrir todas as secções durante um mínimo de 4 horas para as regenerar."; "Planned regeneration is in progress." = "A regeneração planeada está em curso."; "Regeneration initiated after 20 hours of operation is in progress." = "A regeneração iniciará após 20 horas de trabalho."; -"Incorrect Email Address or Password" = "Erro de morada de emailou de palavra passe"; "Channel Id" = "ID do Canal"; "Back" = "Voltar"; "The z-wave bridge is not responding. Check if the bridge is connected to the server." = "A ponte z-wave não responde. Verifique se a ponte está conectada ao servidor."; @@ -279,7 +268,6 @@ "Closing" = "Encerramentos"; "Percent of opening" = "Percentagem de abertura"; "Percent of closing" = "Percentagem de encerramento"; -"Access Identifier inactive." = "Acesso ao identificador inativo."; "Your account" = "A sua conta"; "Your accounts" = "As suas contas"; "Accounts" = "Conta"; @@ -313,3 +301,17 @@ "roller_shutter_motor_problem" = "Problema do motor / Paragem inesperada."; "roller_shutter_calibration" = "Calibrar"; "roller_shutter_start_calibration_message" = "Tem certeza que quer começar a calibrar?"; + +/* Status */ +"status_connecting" = "Está a ligar..."; +"status_unknown_error" = "Erro indefinido"; +"status_temporarily_unavailable" = "Serviço temporariamente indisponível"; +"status_incorrect_data" = "Erro de morada de emailou de palavra passe"; +"status_bad_credentials" = "Dados de identificação errados"; +"status_client_limit_exceeded" = "Excedeu o limite de aplicações de clientes"; +"status_device_disabled" = "O dispositivo está desligado"; +"status_access_id_disabled" = "O ID de acesso está desativado"; +"status_registration_disabled" = "Atualmente o registro está desativado. Para activá-lo, vá para a guia \"Smartphones\" na página \"Supla Cloud\""; +"status_access_id_not_assigned" = "Este dispositivo não possui um ID de acesso atribuído. Vá para a página \"Supla Cloud\" e atribua o ID apropriado na guia \"Smartphones\"."; +"status_access_id_inactive" = "Acesso ao identificador inativo."; +"status_host_not_found" = "O endereço do servidor não foi encontrado"; diff --git a/SUPLA/Resources/ru.lproj/Localizable.strings b/SUPLA/Resources/ru.lproj/Localizable.strings index aed94e08b..f4b38b660 100644 --- a/SUPLA/Resources/ru.lproj/Localizable.strings +++ b/SUPLA/Resources/ru.lproj/Localizable.strings @@ -49,17 +49,7 @@ "Garage door opening sensor" = "Датчик открытия гаражных ворот"; "Door opening sensor" = "Датчик открытия двери"; "Roller shutter opening sensor" = "Датчик открытия роллет"; -"Connecting..." = "Подключение..."; -"Unknown error" = "Непонятная ошибка"; -"Service temporarily unavailable" = "Сервис временно не доступен"; -"Bad credentials" = "Неправильные логин/пароль"; -"Client limit exceeded" = "Ограничение количества пользователей"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "Устройство выключено"; -"Access Identifier is disabled" = "Ключ доступа отключен"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "Регистрация в настоящее время отключена. Для ее включения перейдите на вкладку Смартфоны на странице \"Supla Cloud\""; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "Это устройство не имеет назначенного ключа доступа. Перейдите на страницу \"Supla Cloud\" и на вкладке Смартфоны назначьте соответствующий ключ."; "Incompatible server version" = "Несовместимая версия сервера"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "Хост не найден"; "RGB Lighting" = "Контроллер освещения RGB"; "Dimmer and RGB lighting" = "Диммер и контроллер освещения RGB"; "Dimmer" = "Диммер"; @@ -203,7 +193,6 @@ "The glass sections are exposed for more than 20 hours, which may adversely affect their life. It is recommended to cover all sections for a minimum of 4 hours in order to regenerate them." = "Стеклянные секции открыты более 20 часов, что может отрицательно сказаться на их сроке действия. Рекомендуется закрыть все секции не менее чем на 4 часа для их регенерации."; "Planned regeneration is in progress." = "Идёт плановая регенерация."; "Regeneration initiated after 20 hours of operation is in progress." = "Выполняется регенерация, начатая после 20 часов работы."; -"Incorrect Email Address or Password" = "Неверный адрес email или пароль"; "Channel Id" = "Id канала"; "Back" = "Назад"; "The z-wave bridge is not responding. Check if the bridge is connected to the server." = "Мост z-wave не отвечает. Проверьте подключение моста к серверу."; @@ -283,7 +272,6 @@ "Closing" = "Закрытия"; "Percent of opening" = "Процент открытия"; "Percent of closing" = "Процент закрытия"; -"Access Identifier inactive." = "Идентификатор доступа неактивен."; "Your account" = "Ваша учётная запись"; "Your accounts" = "Ваши учётные записи"; "Accounts" = "Учётные записи"; @@ -317,3 +305,17 @@ "roller_shutter_motor_problem" = "Проблема с двигателем / Неожиданная остановка."; "roller_shutter_calibration" = "калибровка"; "roller_shutter_start_calibration_message" = "Вы уверены, что хотите начать калибровку?"; + +/* Status */ +"status_connecting" = "Подключение..."; +"status_unknown_error" = "Непонятная ошибка"; +"status_temporarily_unavailable" = "Сервис временно не доступен"; +"status_incorrect_data" = "Неверный адрес email или пароль"; +"status_bad_credentials" = "Неправильные логин/пароль"; +"status_client_limit_exceeded" = "Ограничение количества пользователей"; +"status_device_disabled" = "Устройство выключено"; +"status_access_id_disabled" = "Ключ доступа отключен"; +"status_registration_disabled" = "Регистрация в настоящее время отключена. Для ее включения перейдите на вкладку Смартфоны на странице \"Supla Cloud\"."; +"status_access_id_not_assigned" = "Это устройство не имеет назначенного ключа доступа. Перейдите на страницу \"Supla Cloud\" и на вкладке Смартфоны назначьте соответствующий ключ."; +"status_access_id_inactive" = "Идентификатор доступа неактивен."; +"status_host_not_found" = "Хост не найден"; diff --git a/SUPLA/Resources/sk-SK.lproj/Localizable.strings b/SUPLA/Resources/sk-SK.lproj/Localizable.strings index 8274d4889..9b8189315 100644 --- a/SUPLA/Resources/sk-SK.lproj/Localizable.strings +++ b/SUPLA/Resources/sk-SK.lproj/Localizable.strings @@ -45,17 +45,7 @@ "Garage door opening sensor" = "Snímač otvorenia garážovej brány"; "Door opening sensor" = "Snímač otvorenia dverí"; "Roller shutter opening sensor" = "Snímač otvorenia roliet"; -"Connecting..." = "Pripája sa..."; -"Unknown error" = "Neznáma chyba"; -"Service temporarily unavailable" = "Služba je dočasne nedostupná"; -"Bad credentials" = "Nesprávne autentifikačné údaje"; -"Client limit exceeded" = "Prekročený limit klientskych aplikácií"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "Zariadenie je vypnuté"; -"Access Identifier is disabled" = "Identifikátor prístupu je vypnutý"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "Registrácia je momentálne vypnutá. Ak ju chcete zapnúť, prejdite do záložky \"Smartfóny\" na stránke \"Supla Cloud\""; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "Toto zariadenie nemá priradený identifikátor prístupu. Prejdite na stránku \"Supla Cloud\" a v záložke \"Smartfóny\" priraďte príslušný identifikátor."; "Incompatible server version" = "Nekompatibilná verzia servera"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "Adresa servera nebola nájdená"; "RGB Lighting" = "RGB osvetlenie"; "Dimmer and RGB lighting" = "Stmievač a RGB osvetlenie"; "Dimmer" = "Stmievač"; @@ -199,7 +189,6 @@ "The glass sections are exposed for more than 20 hours, which may adversely affect their life. It is recommended to cover all sections for a minimum of 4 hours in order to regenerate them." = "Zasklenie je odhalené viac ako 20 hodín, čo môže mať negatívny vplyv na jeho životnosť. Odporúča sa všetky úseky pokryť minimálne 4 hodiny, aby sa zregenerovali."; "Planned regeneration is in progress." = "Prebieha plánovaná regenerácia."; "Regeneration initiated after 20 hours of operation is in progress." = "Prebieha regenerácia, ktorá bola spustená po 20 h prevádzky."; -"Incorrect Email Address or Password" = "Nesprávna e-mailová adresa alebo heslo"; "Channel Id" = "ID kanála"; "Back" = "Späť"; "The z-wave bridge is not responding. Check if the bridge is connected to the server." = "Most z-wave neodpovedá. Skontroluj, či je most spojený so serverom."; @@ -279,7 +268,6 @@ "Closing" = "Zatvorenia"; "Percent of opening" = "Percento otvorenia"; "Percent of closing" = "Percento zatvorenia"; -"Access Identifier inactive." = "Prístupový identifikátor nie je aktívny."; "Your account" = "Váš účet"; "Your accounts" = "Vaše účty"; "Accounts" = "Účty"; @@ -313,3 +301,17 @@ "roller_shutter_motor_problem" = "Problém s motorom / Neočakávané zastavenie."; "roller_shutter_calibration" = "Kalibrácia"; "roller_shutter_start_calibration_message" = "Naozaj chcete spustiť kalibráciu?"; + +/* Status */ +"status_connecting" = "Pripája sa..."; +"status_unknown_error" = "Neznáma chyba"; +"status_temporarily_unavailable" = "Služba je dočasne nedostupná"; +"status_incorrect_data" = "Nesprávna e-mailová adresa alebo heslo"; +"status_bad_credentials" = "Nesprávne autentifikačné údaje"; +"status_client_limit_exceeded" = "Prekročený limit klientskych aplikácií"; +"status_device_disabled" = "Zariadenie je vypnuté"; +"status_access_id_disabled" = "Identifikátor prístupu je vypnutý"; +"status_registration_disabled" = "Registrácia je momentálne vypnutá. Ak ju chcete zapnúť, prejdite do záložky \"Smartfóny\" na stránke \"Supla Cloud\""; +"status_access_id_not_assigned" = "Toto zariadenie nemá priradený identifikátor prístupu. Prejdite na stránku \"Supla Cloud\" a v záložke \"Smartfóny\" priraďte príslušný identifikátor."; +"status_access_id_inactive" = "Prístupový identifikátor nie je aktívny."; +"status_host_not_found" = "Adresa servera nebola nájdená"; diff --git a/SUPLA/Resources/sl.lproj/Localizable.strings b/SUPLA/Resources/sl.lproj/Localizable.strings index e0de4390d..b6c33ed1d 100644 --- a/SUPLA/Resources/sl.lproj/Localizable.strings +++ b/SUPLA/Resources/sl.lproj/Localizable.strings @@ -45,17 +45,7 @@ "Garage door opening sensor" = "Senzor odprtja garažnih vrat"; "Door opening sensor" = "Senzor odprtja vrat"; "Roller shutter opening sensor" = "Senzor odprtja rolet"; -"Connecting..." = "Povezovanje..."; -"Unknown error" = "Neznana napaka"; -"Service temporarily unavailable" = "Storitev trenutno ni na voljo"; -"Bad credentials" = "Napačne poverilnice"; -"Client limit exceeded" = "Omejitev odjemalskih aplikacij je presežena"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "Naprava je izklopljena"; -"Access Identifier is disabled" = "Identifikator dostopa je izklopljen"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "Registracija je trenutno izklopljena. Če jo želite omogočiti, pojdite na zavihek \"Pametni telefoni\" na strani \"Supla Cloud\""; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "Ta naprava nima dodeljenega identifikatorja dostopa. Pojdite na stran \"Supla Cloud\" in na zavihku \"Pametni telefoni\" dodelite ustrezen identifikator."; "Incompatible server version" = "Nezdružljiva različica strežnika"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "Naslova strežnika ni mogoče najti"; "RGB Lighting" = "RGB osvetlitev"; "Dimmer and RGB lighting" = "Zatemnilnik in RGB osvetlitev"; "Dimmer" = "Zatemnilnik"; @@ -199,7 +189,6 @@ "The glass sections are exposed for more than 20 hours, which may adversely affect their life. It is recommended to cover all sections for a minimum of 4 hours in order to regenerate them." = "Stekleni deli so izpostavljeni več kot 20 ur, kar lahko negativno vpliva na njihovo življenjsko dobo. Priporočljivo je, da vse dele prekrijete za najmanj 4 ure, da se obnovijo."; "Planned regeneration is in progress." = "Načrtovana obnova je v teku."; "Regeneration initiated after 20 hours of operation is in progress." = "Obnova je v teku po 20 urah dela."; -"Incorrect Email Address or Password" = "Napačen e-naslov ali geslo"; "Channel Id" = "Identifikator kanala"; "Back" = "Nazaj"; "The z-wave bridge is not responding. Check if the bridge is connected to the server." = "Most z-wave se ne odziva. Preverite, če je most povezan s strežnikom."; @@ -279,7 +268,6 @@ "Closing" = "Zapiranja"; "Percent of opening" = "Odstotek odpiranja"; "Percent of closing" = "Odstotek zapiranja"; -"Access Identifier inactive." = "Identifikator dostopa ni na voljo."; "Your account" = "Vaš račun"; "Your accounts" = "Vaši računi"; "Accounts" = "Računi"; @@ -313,3 +301,17 @@ "roller_shutter_motor_problem" = "Težava z motorjem / Nepričakovana zaustavitev."; "roller_shutter_calibration" = "Kalibracija"; "roller_shutter_start_calibration_message" = "Ali ste prepričani, da želite začeti umerjanje?"; + +/* Status */ +"status_connecting" = "Povezovanje..."; +"status_unknown_error" = "Neznana napaka"; +"status_temporarily_unavailable" = "Storitev trenutno ni na voljo"; +"status_incorrect_data" = "Napačen e-naslov ali geslo"; +"status_bad_credentials" = "Napačne poverilnice"; +"status_client_limit_exceeded" = "Omejitev odjemalskih aplikacij je presežena"; +"status_device_disabled" = "Naprava je izklopljena"; +"status_access_id_disabled" = "Identifikator dostopa je izklopljen"; +"status_registration_disabled" = "Registracija je trenutno izklopljena. Če jo želite omogočiti, pojdite na zavihek \"Pametni telefoni\" na strani \"Supla Cloud\""; +"status_access_id_not_assigned" = "Ta naprava nima dodeljenega identifikatorja dostopa. Pojdite na stran \"Supla Cloud\" in na zavihku \"Pametni telefoni\" dodelite ustrezen identifikator."; +"status_access_id_inactive" = "Identifikator dostopa ni na voljo."; +"status_host_not_found" = "Naslova strežnika ni mogoče najti"; diff --git a/SUPLA/Resources/uk.lproj/Localizable.strings b/SUPLA/Resources/uk.lproj/Localizable.strings index f0c2de625..ad313f03d 100644 --- a/SUPLA/Resources/uk.lproj/Localizable.strings +++ b/SUPLA/Resources/uk.lproj/Localizable.strings @@ -45,17 +45,7 @@ "Garage door opening sensor" = "Датчик відчинення гаражних воріт"; "Door opening sensor" = "Датчик відчинення дверей"; "Roller shutter opening sensor" = "Датчик відчинення ролет"; -"Connecting..." = "Підключення..."; -"Unknown error" = "Невідома помилка"; -"Service temporarily unavailable" = "Послуга тимчасово недоступна"; -"Bad credentials" = "Неправильні облікові дані"; -"Client limit exceeded" = "Перевищено ліміт клієнтських додатків"; -"Device disabled. Please log in to \"Supla Cloud\" and enable this device in “Smartphone” section of the website." = "Пристрій вимкнено. Увійдіть в \"Supla Cloud\" і включіть цей смартфон на вкладці \"Смартфони\"."; -"Access Identifier is disabled" = "Ідентифікатор доступу вимкнено"; -"New client registration disabled. Please log in to \"Supla Cloud\" and enable \"New client registration\" in \"Smartphone\" section of the website." = "Реєстрація в даний час відключена. Увійдіть в \"Supla Cloud\" і переключіть реєстрацію клієнтів на АКТИВНУ на вкладці \"Смартфони\"."; -"Client activation required. Please log in to \"Supla Cloud\" and assign an “Access ID” for this device in “Smartphone” section of the website." = "Цьому пристрою не присвоєно ідентифікатор доступу. Перейдіть на сторінку \"Supla Cloud\" і на вкладці \"Смартфони\" призначте відповідний ідентифікатор."; "Incompatible server version" = "Несумісна версія сервера"; -"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created." = "Адреса сервера не знайдена. Переконайтеся, що у вас є підключення до Інтернету та що створено обліковий запис із введеною адресою електронної пошти."; "RGB Lighting" = "Освітлення RGB"; "Dimmer and RGB lighting" = "Регулятор освітленості та освітлення RGB"; "Dimmer" = "Регулятор освітленості"; @@ -199,7 +189,6 @@ "The glass sections are exposed for more than 20 hours, which may adversely affect their life. It is recommended to cover all sections for a minimum of 4 hours in order to regenerate them." = "Скляні секції експонуються більше 20 годин, що може негативно вплинути на термін їх служби. Рекомендується накрити всі секції мінімум на 4 години для їх регенерації."; "Planned regeneration is in progress." = "Виконується планова регенерація."; "Regeneration initiated after 20 hours of operation is in progress." = "Триває регенерація, розпочата після 20 годин роботи."; -"Incorrect Email Address or Password" = "Неправильна адреса електронної пошти або пароль"; "Channel Id" = "Ідентифікатор каналу"; "Back" = "Назад"; "The z-wave bridge is not responding. Check if the bridge is connected to the server." = "Міст z-wave не відповідає. Перевірте, чи підключено міст до сервера."; @@ -279,7 +268,6 @@ "Closing" = "Закриття"; "Percent of opening" = "Відсоток відкриття"; "Percent of closing" = "Відсоток закриття"; -"Access Identifier inactive." = "Ідентифікатор доступу неактивний."; "Your account" = "Ваш обліковий запис"; "Your accounts" = "Ваші облікові записи"; "Accounts" = "Облікові записи"; @@ -316,3 +304,17 @@ "roller_shutter_motor_problem" = "Проблема з двигуном / Несподівана зупинка."; "roller_shutter_calibration" = "Калібрування"; "roller_shutter_start_calibration_message" = "Ви впевнені, що хочете почати калібрування?"; + +/* Status */ +"status_connecting" = "Підключення..."; +"status_unknown_error" = "Невідома помилка"; +"status_temporarily_unavailable" = "Послуга тимчасово недоступна"; +"status_incorrect_data" = "Неправильна адреса електронної пошти або пароль"; +"status_bad_credentials" = "Неправильні облікові дані"; +"status_client_limit_exceeded" = "Перевищено ліміт клієнтських додатків"; +"status_device_disabled" = "Пристрій вимкнено. Увійдіть в \"Supla Cloud\" і включіть цей смартфон на вкладці \"Смартфони\"."; +"status_access_id_disabled" = "Ідентифікатор доступу вимкнено"; +"status_registration_disabled" = "Реєстрація в даний час відключена. Увійдіть в \"Supla Cloud\" і переключіть реєстрацію клієнтів на АКТИВНУ на вкладці \"Смартфони\"."; +"status_access_id_not_assigned" = "Цьому пристрою не присвоєно ідентифікатор доступу. Перейдіть на сторінку \"Supla Cloud\" і на вкладці \"Смартфони\" призначте відповідний ідентифікатор."; +"status_access_id_inactive" = "Ідентифікатор доступу неактивний."; +"status_host_not_found" = "Адреса сервера не знайдена. Переконайтеся, що у вас є підключення до Інтернету та що створено обліковий запис із введеною адресою електронної пошти."; diff --git a/SUPLA/SADialog.m b/SUPLA/SADialog.m index ca95a3bca..592d66164 100644 --- a/SUPLA/SADialog.m +++ b/SUPLA/SADialog.m @@ -112,12 +112,12 @@ - (void)close { } + (BOOL)viewControllerIsPresented:(UIViewController*)vc { - UIViewController *rootVC = [SAApp currentNavigationCoordinator].viewController; + UIViewController *rootVC = [SuplaAppCoordinatorLegacyWrapper currentViewController]; return vc != nil && rootVC != nil && rootVC.presentedViewController == vc; } + (void)showModal:(SADialog*)dialogVC { - UIViewController *rootVC = [SAApp currentNavigationCoordinator].viewController; + UIViewController *rootVC = [SuplaAppCoordinatorLegacyWrapper currentViewController]; dialogVC.modalPresentationStyle = UIModalPresentationOverCurrentContext; if (@available(iOS 13.0, *)) { diff --git a/SUPLA/SAMenuItems.m b/SUPLA/SAMenuItems.m index 2f0a06fdf..05ccd21fa 100644 --- a/SUPLA/SAMenuItems.m +++ b/SUPLA/SAMenuItems.m @@ -37,7 +37,7 @@ @implementation SAMenuItems { - (void)menuItemsInit { _btnCount = 0; - self.backgroundColor = [UIColor primary]; + self.backgroundColor = [UIColor toolbar]; self.translatesAutoresizingMaskIntoConstraints = YES; } diff --git a/SUPLA/SAZWaveConfigurationWizardVC.m b/SUPLA/SAZWaveConfigurationWizardVC.m index 93679e97d..337e1c042 100644 --- a/SUPLA/SAZWaveConfigurationWizardVC.m +++ b/SUPLA/SAZWaveConfigurationWizardVC.m @@ -189,8 +189,7 @@ -(void)viewWillDisappear:(BOOL)animated { -(void) superuserAuthorizationSuccess { [SASuperuserAuthorizationDialog.globalInstance close]; - [(UINavigationController*)[SAApp mainNavigationCoordinator].viewController - pushViewController: self animated: YES]; + [SuplaAppCoordinatorLegacyWrapper push:self]; } -(void)show { diff --git a/SUPLA/SUPLA-Bridging-Header.h b/SUPLA/SUPLA-Bridging-Header.h index 4e9fd3d79..09aafa4f3 100644 --- a/SUPLA/SUPLA-Bridging-Header.h +++ b/SUPLA/SUPLA-Bridging-Header.h @@ -5,7 +5,6 @@ #import "SuplaApp.h" #import "SAChartHelper.h" #import "CreateAccountVC.h" -#import "StatusVC.h" #import "AddWizardVC.h" #import "AboutVC.h" #import "SAIncrementalMeterChartHelper.h" @@ -45,6 +44,5 @@ #import "SAChannelConfig+CoreDataClass.h" #import "SANotification+CoreDataClass.h" #import "SASuperuserAuthorizationResult.h" -#import "AppDelegate.h" #import "supla-client.h" diff --git a/SUPLA/StatusVC.h b/SUPLA/StatusVC.h deleted file mode 100644 index 5caf24926..000000000 --- a/SUPLA/StatusVC.h +++ /dev/null @@ -1,39 +0,0 @@ -/* - Copyright (C) AC SOFTWARE SP. Z O.O. - - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ - -#import -#import "BaseViewController.h" - -@interface SAStatusVC : BaseViewController -@property (weak, nonatomic) IBOutlet UIImageView *image; -@property (weak, nonatomic) IBOutlet UILabel *label; -@property (weak, nonatomic) IBOutlet UIButton *btnCloud; -@property (weak, nonatomic) IBOutlet UIButton *button; -@property (weak, nonatomic) IBOutlet UIButton *btnRetry; -@property (weak, nonatomic) IBOutlet UIProgressView *progress; -- (IBAction)btnTouch:(id)sender; -- (IBAction)btnRetryTouch:(id)sender; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *cintButtonCenter; -- (IBAction)btnCloudTouch:(id)sender; - - - --(void)setStatusConnectingProgress:(float)value; --(void)setStatusError:(NSString*)message; - -@end diff --git a/SUPLA/StatusVC.m b/SUPLA/StatusVC.m deleted file mode 100644 index a7f2c083a..000000000 --- a/SUPLA/StatusVC.m +++ /dev/null @@ -1,115 +0,0 @@ -/* - Copyright (C) AC SOFTWARE SP. Z O.O. - - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ - -#import "StatusVC.h" -#import "SuplaApp.h" -#import "UIColor+SUPLA.h" -#import "SUPLA-Swift.h" - -@interface SAStatusVC () - -@end - - -@implementation SAStatusVC - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil - bundle:(NSBundle *)nibBundleOrNil { - if(self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - self.modalPresentationStyle = UIModalPresentationFullScreen; - self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; - } - return self; -} - -- (void)viewDidLoad { - [super viewDidLoad]; - - self.button.layer.cornerRadius = 3; - self.button.clipsToBounds = YES; - [self.button setTitle:NSLocalizedString(@"Your account", nil) forState:UIControlStateNormal]; - - [self setNeedsStatusBarAppearanceUpdate]; - [self GreenTheme]; -} - -- (UIStatusBarStyle)preferredStatusBarStyle -{ - return UIStatusBarStyleLightContent; -} - --(void)YellowTheme { - UIColor *yellowColor = [UIColor statusYellow]; - [self.view setBackgroundColor: yellowColor]; - [self.statusBarBackgroundView setBackgroundColor: yellowColor]; - [self.progress setHidden:YES]; - [self.image setImage:nil]; - [self.label setTextColor:[UIColor blackColor]]; - [self.button setBackgroundColor:[UIColor blackColor]]; - [self.button setTitleColor:self.view.backgroundColor forState:UIControlStateNormal]; - self.btnRetry.hidden = NO; - self.cintButtonCenter.constant = -20; - self.btnCloud.hidden = NO; -} - --(void)GreenTheme { - [self.view setBackgroundColor: [UIColor primary]]; - [self.statusBarBackgroundView setBackgroundColor: [UIColor primary]]; - [self.progress setHidden:NO]; - - [self.image setImage:[UIImage imageNamed:@"logo-white"]]; - [self.label setTextColor:[UIColor whiteColor]]; - [self.button setBackgroundColor:[UIColor whiteColor]]; - [self.button setTitleColor:self.view.backgroundColor forState:UIControlStateNormal]; - self.btnRetry.hidden = YES; - self.cintButtonCenter.constant = 0; - self.btnCloud.hidden = YES; - - [self setNeedsStatusBarAppearanceUpdate]; -} - --(void)setStatusConnectingProgress:(float)value { - if(value >= 0) { - [self GreenTheme]; - [self.label setText:NSLocalizedString(@"Connecting...", NULL)]; - } else - value = 0; - [self.progress setProgress:value]; -} - -- (IBAction)btnCloudTouch:(id)sender { - [[UIApplication sharedApplication] openURL:[NSURL URLWithString: NSLocalizedString(@"https://cloud.supla.org", NULL)]]; -} - --(void)setStatusError:(NSString*)message { - [self YellowTheme]; - [self.image setImage:[UIImage imageNamed:@"error"]]; - [self.label setText:message]; -} - - -- (IBAction)btnTouch:(id)sender { - [[SAApp mainNavigationCoordinator] showProfilesView]; -} - -- (IBAction)btnRetryTouch:(id)sender { - [[SAApp SuplaClient] reconnect]; -} - - -@end diff --git a/SUPLA/StatusVC.xib b/SUPLA/StatusVC.xib deleted file mode 100644 index 131bcc094..000000000 --- a/SUPLA/StatusVC.xib +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - - - - OpenSans - - - Quicksand-Regular - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/SUPLA/SuplaClient.h b/SUPLA/SuplaClient.h index 991daa6a8..ee2226954 100644 --- a/SUPLA/SuplaClient.h +++ b/SUPLA/SuplaClient.h @@ -23,6 +23,8 @@ @protocol SuplaClientProtocol - (void) cancel; +- (BOOL) isCancelled; +- (BOOL) isFinished; - (void) reconnect; - (BOOL) executeAction: (int)actionId subjecType: (int)subjectType subjectId: (int)subjectId parameters: (void*)parameters length: (int)length; - (BOOL) timerArmFor: (int) remoteId withTurnOn: (BOOL) on withTime: (int) milis; diff --git a/SUPLA/SuplaClient.m b/SUPLA/SuplaClient.m index 988ca4674..b944d8ca3 100644 --- a/SUPLA/SuplaClient.m +++ b/SUPLA/SuplaClient.m @@ -628,6 +628,7 @@ - (void)main { usleep(2000000); } else { @try { + // TODO: Add network check if ( supla_client_connect(_sclient) == 1 ) { while ( [self isCancelled] == NO && supla_client_iterate(_sclient, 100000) == 1) { @@ -656,6 +657,7 @@ - (void)main { } [self performSelectorOnMainThread:@selector(_onTerminated) withObject:nil waitUntilDone:NO]; + [SuplaAppStateHolderProxy finish]; //NSLog(@"SuplaClient Finished"); } @@ -688,6 +690,9 @@ - (void) onVersionError:(SAVersionError*)ve { [self performSelectorOnMainThread:@selector(_onVersionError:) withObject:ve waitUntilDone:NO]; + + [self cancel]; + [SuplaAppStateHolderProxy versionError]; } - (void) _onConnected { @@ -704,6 +709,12 @@ - (void) _onConnError:(NSNumber*)code { - (void) onConnError:(int)code { [self performSelectorOnMainThread:@selector(_onConnError:) withObject:[NSNumber numberWithInt:code] waitUntilDone:NO]; + + if (code == SUPLA_RESULT_HOST_NOT_FOUND) { + [self cancel]; + } + + [SuplaAppStateHolderProxy connectionErrorWithCode: code]; } - (void) _onDisconnected { @@ -729,7 +740,11 @@ - (void) _onRegisterError:(NSNumber*)code { - (void) onRegisterError:(int)code { _regTryCounter = 0; + + [self cancel]; + [self performSelectorOnMainThread:@selector(_onRegisterError:) withObject:[NSNumber numberWithInt:code] waitUntilDone:NO]; + [SuplaAppStateHolderProxy registerErrorWithCode:code]; } - (void) _onRegistered:(SARegResult*)result { @@ -780,6 +795,8 @@ - (void) onRegistered:(SARegResult*)result { [DiContainer setOAuthUrlWithUrl: ai.serverUrlString]; [self performSelectorOnMainThread:@selector(_onRegistered:) withObject:result waitUntilDone:NO]; + + [SuplaAppStateHolderProxy connected]; } - (void) _onConnecting { @@ -788,6 +805,7 @@ - (void) _onConnecting { - (void) onConnecting { [self performSelectorOnMainThread:@selector(_onConnecting) withObject:nil waitUntilDone:NO]; + [SuplaAppStateHolderProxy connecting]; } - (void) _onDataChanged { @@ -1674,4 +1692,9 @@ - (void) registerPushNotificationClientToken:(NSData *)token forProfile: (AuthPr } } +- (void) cancel { + [super cancel]; + [SuplaAppStateHolderProxy cancel]; +} + @end diff --git a/SUPLA/Transitions/SwipeTransition.swift b/SUPLA/Transitions/SwipeTransition.swift deleted file mode 100644 index a2e25fd01..000000000 --- a/SUPLA/Transitions/SwipeTransition.swift +++ /dev/null @@ -1,68 +0,0 @@ -/* - Copyright (C) AC SOFTWARE SP. Z O.O. - - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -import UIKit - -class SwipeTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning { - - enum Direction { - case slideIn - case slideOut - } - - private let _direction: Direction - var interactionController: UIViewControllerInteractiveTransitioning? - - init(direction: Direction) { - _direction = direction - super.init() - } - - func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { - return 0.5 - } - - func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { - let duration = self.transitionDuration(using: transitionContext) - if _direction == .slideOut, let fromVC = transitionContext.viewController(forKey: .from), - let toVC = transitionContext.viewController(forKey: .to), - let newView = toVC.view, let oldView = fromVC.view { - let ff = transitionContext.finalFrame(for: toVC) - newView.frame = ff.offsetBy(dx: -ff.width, dy: 0) - transitionContext.containerView.addSubview(newView) - UIView.animate(withDuration: duration, animations: { - newView.frame = ff - oldView.frame = oldView.frame.offsetBy(dx: ff.width, dy: 0) - }, completion: { completed in - transitionContext.completeTransition(!transitionContext.transitionWasCancelled) - }) - } else if _direction == .slideIn, - let newVC = transitionContext.viewController(forKey: .to), - let oldVC = transitionContext.viewController(forKey: .from), - let newView = newVC.view, let oldView = oldVC.view { - let ff = transitionContext.finalFrame(for: newVC) - transitionContext.containerView.addSubview(newView) - newView.frame = ff.offsetBy(dx: ff.width, dy: 0) - UIView.animate(withDuration: duration) { - newView.frame = ff - oldView.frame = ff.offsetBy(dx: -ff.width, dy: 0) - } completion: { completed in - transitionContext.completeTransition(!transitionContext.transitionWasCancelled) - } - } - } -} diff --git a/SUPLA/UI/BaseViewController.h b/SUPLA/UI/BaseViewController.h index 7bdbbb0ce..60838337d 100644 --- a/SUPLA/UI/BaseViewController.h +++ b/SUPLA/UI/BaseViewController.h @@ -18,11 +18,10 @@ #import -@protocol NavigationCoordinator, NavigationCoordinatorAware; + NS_ASSUME_NONNULL_BEGIN -@interface BaseViewController : UIViewController -@property (weak,nonatomic) id navigationCoordinator; +@interface BaseViewController : UIViewController @property (readonly,nonatomic) UIView *statusBarBackgroundView; - (BOOL)adjustsStatusBarBackground; - (BOOL)hidesNavigationBar; diff --git a/SUPLA/UI/BaseViewController.m b/SUPLA/UI/BaseViewController.m index 834cf93e3..b30091828 100644 --- a/SUPLA/UI/BaseViewController.m +++ b/SUPLA/UI/BaseViewController.m @@ -39,7 +39,7 @@ - (void)viewDidLoad { CGRect sbFrame; sbFrame = [[UIApplication sharedApplication] statusBarFrame]; statusBarBg = [[UIView alloc] initWithFrame: CGRectZero]; - statusBarBg.backgroundColor = [UIColor primary]; + statusBarBg.backgroundColor = [UIColor toolbar]; statusBarBg.translatesAutoresizingMaskIntoConstraints = NO; [self.view addSubview: statusBarBg]; [statusBarBg.topAnchor constraintEqualToAnchor: self.view.topAnchor].active = YES; diff --git a/SUPLA/UseCase/App/InitializationUseCase.swift b/SUPLA/UseCase/App/InitializationUseCase.swift new file mode 100644 index 000000000..fa654feca --- /dev/null +++ b/SUPLA/UseCase/App/InitializationUseCase.swift @@ -0,0 +1,59 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +private let INITIALIZATION_MIN_TIME_S: Double = 1 + +enum InitializationUseCase { + static func invoke() { + @Singleton var dateProvider + @Singleton var profileRepository + @Singleton var stateHolder + @Singleton var settings + @Singleton var databaseProxy + @Singleton var threadHandler + + let initializationStartTime = dateProvider.currentTimestamp() + + // Migrate database if needed + databaseProxy.setup() + + // Check if there is an active profile + let profileFound = (try? profileRepository.getActiveProfile().subscribeSynchronous()?.isActive) ?? false + + // Check pin + let pinRequired = settings.lockScreenSettings.pinForAppRequired + + // Wait a moment to avoid screen blinking + let initializationEndTime = dateProvider.currentTimestamp() + let initializationTime = initializationEndTime - initializationStartTime + if (initializationTime < INITIALIZATION_MIN_TIME_S) { + threadHandler.usleepProxy(UInt32((INITIALIZATION_MIN_TIME_S - initializationTime) * 1_000_000)) + } + + // Go to next state + SALog.debug("Active profile found: \(profileFound), pin required \(pinRequired)") + if (pinRequired) { + stateHolder.handle(event: .lock) + } else if (profileFound) { + stateHolder.handle(event: .initialized) + } else { + stateHolder.handle(event: .noAccount) + } + } +} diff --git a/SUPLA/UseCase/Client/AuthorizeUseCase.swift b/SUPLA/UseCase/Client/AuthorizeUseCase.swift index 38b362947..9222ca58e 100644 --- a/SUPLA/UseCase/Client/AuthorizeUseCase.swift +++ b/SUPLA/UseCase/Client/AuthorizeUseCase.swift @@ -61,7 +61,7 @@ final class AuthorizeUseCaseImpl: AuthorizeUseCase { completable(.completed) return Disposables.create() } - completable(.error(AuthorizationError(errorMessage: result.error ?? Strings.General.unknownError))) + completable(.error(AuthorizationError(errorMessage: result.error ?? Strings.Status.errorUnknown))) return Disposables.create() } } @@ -98,9 +98,9 @@ private class AuthorizationMessageObserver { private func errorString(_ errorCode: Int32) -> String { switch (errorCode) { - case SUPLA_RESULTCODE_UNAUTHORIZED: Strings.AuthorizationDialog.unauthorized - case SUPLA_RESULTCODE_TEMPORARILY_UNAVAILABLE: Strings.AuthorizationDialog.unavailable - default: Strings.General.unknownError + case SUPLA_RESULTCODE_UNAUTHORIZED: Strings.Status.errorInvalidData + case SUPLA_RESULTCODE_TEMPORARILY_UNAVAILABLE: Strings.Status.errorUnavailable + default: Strings.Status.errorUnknown } } diff --git a/SUPLA/UseCase/Client/DisconnectUseCase.swift b/SUPLA/UseCase/Client/DisconnectUseCase.swift new file mode 100644 index 000000000..2477d39fa --- /dev/null +++ b/SUPLA/UseCase/Client/DisconnectUseCase.swift @@ -0,0 +1,62 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import RxSwift + +protocol DisconnectUseCase { + func invoke() -> Completable + func invokeSynchronous() +} + +final class DisconnectUseCaseImpl: DisconnectUseCase { + + @Singleton private var suplaClientProvider + @Singleton private var suplaAppProvider + @Singleton private var updateEventsManager + + func invoke() -> Completable { + Completable.create { completable in + self.invokeSynchronous() + + completable(.completed) + return Disposables.create() + } + } + + func invokeSynchronous() { + let suplaApp = suplaAppProvider.provide() + + if (suplaApp.isClientWorking()) { + let suplaClient = suplaClientProvider.provide() + suplaClient.cancel() + + while (!suplaClient.isFinished()) { + usleep(1000) + } + } + + suplaApp.cancelAllRestApiClientTasks() + suplaAppProvider.revokeOAuthToken() + + updateEventsManager.cleanup() + updateEventsManager.emitChannelsUpdate() + updateEventsManager.emitGroupsUpdate() + updateEventsManager.emitScenesUpdate() + } +} diff --git a/SUPLA/UseCase/Client/LoginUseCase.swift b/SUPLA/UseCase/Client/LoginUseCase.swift new file mode 100644 index 000000000..a7dfa1039 --- /dev/null +++ b/SUPLA/UseCase/Client/LoginUseCase.swift @@ -0,0 +1,126 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import RxSwift + +protocol LoginUseCase { + func invoke(userName: String, password: String) -> Completable +} + +final class LoginUseCaseImpl: LoginUseCase { + @Singleton private var notificationCenterWrapper + @Singleton private var suplaAppProvider + @Singleton private var threadHandler + + func invoke(userName: String, password: String) -> Completable { + Completable.create { completable in + var result: AuthorizationResult? = nil + + let registerObserver = RegisteredMessageObserver { result = $0 } + let registerErrorObserver = RegisterErrorMessageObserver { result = $0 } + self.register(registerObserver, registerErrorObserver) + + self.suplaAppProvider.initClientWithOneTimePassword(password) + self.waitForResponse { result != nil } + + self.notificationCenterWrapper.unregisterObserver(registerObserver) + self.notificationCenterWrapper.unregisterObserver(registerErrorObserver) + + guard let result = result else { + completable(.error(AuthorizationError(errorMessage: Strings.AuthorizationDialog.timeout))) + return Disposables.create() + } + if (result.success) { + completable(.completed) + return Disposables.create() + } + completable(.error(AuthorizationError(errorMessage: result.error ?? Strings.Status.errorUnknown))) + return Disposables.create() + } + } + + private func register(_ registeredObserver: RegisteredMessageObserver, _ registerErrorObserver: RegisterErrorMessageObserver) { + notificationCenterWrapper.registerObserver( + registeredObserver, + selector: #selector(registeredObserver.onMessageReceived(notification:)), + name: NSNotification.Name.saRegistered + ) + + notificationCenterWrapper.registerObserver( + registerErrorObserver, + selector: #selector(registerErrorObserver.onMessageReceived(notification:)), + name: NSNotification.Name.saRegisterError + ) + } + + private func waitForResponse(_ resultAvailable: () -> Bool) { + for _ in 0 ..< 10 { + if (resultAvailable()) { + return + } + threadHandler.sleep(1) + } + } + + struct AuthorizationResult { + let success: Bool + let error: String? + + init(success: Bool, error: String? = nil) { + self.success = success + self.error = error + } + } +} + +private class RegisteredMessageObserver { + @Singleton private var notificationCenterWrapper + + var resultObserver: (LoginUseCaseImpl.AuthorizationResult) -> Void = { _ in } + + init(resultObserver: @escaping (LoginUseCaseImpl.AuthorizationResult) -> Void) { + self.resultObserver = resultObserver + } + + @objc func onMessageReceived(notification: Notification) { + resultObserver(LoginUseCaseImpl.AuthorizationResult(success: true)) + notificationCenterWrapper.unregisterObserver(self) + } +} + +private class RegisterErrorMessageObserver { + @Singleton private var notificationCenterWrapper + + var resultObserver: (LoginUseCaseImpl.AuthorizationResult) -> Void = { _ in } + + init(resultObserver: @escaping (LoginUseCaseImpl.AuthorizationResult) -> Void) { + self.resultObserver = resultObserver + } + + @objc func onMessageReceived(notification: Notification) { + guard let userInfo = notification.userInfo, + let result = userInfo["code"], + let resultCode = result as? NSNumber + else { return } + + resultObserver(LoginUseCaseImpl.AuthorizationResult(success: false, error: SuplaResultCode.from(value: Int32(truncating: resultCode)).getTextMessage(authDialog: true))) + + notificationCenterWrapper.unregisterObserver(self) + } +} diff --git a/SUPLA/Features/Details/ThermometerDetail/ThermometerDetailNavigatorCoordinator.swift b/SUPLA/UseCase/Client/ReconnectUseCase.swift similarity index 55% rename from SUPLA/Features/Details/ThermometerDetail/ThermometerDetailNavigatorCoordinator.swift rename to SUPLA/UseCase/Client/ReconnectUseCase.swift index 0a941c3fc..fa884c957 100644 --- a/SUPLA/Features/Details/ThermometerDetail/ThermometerDetailNavigatorCoordinator.swift +++ b/SUPLA/UseCase/Client/ReconnectUseCase.swift @@ -1,3 +1,4 @@ +// /* Copyright (C) AC SOFTWARE SP. Z O.O. @@ -16,25 +17,26 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -import Foundation +import RxSwift -class ThermometerDetailNavigatorCoordinator: BaseNavigationCoordinator { - - private let item: ItemBundle - private let pages: [DetailPage] - - override var viewController: UIViewController { - _viewController - } - - private lazy var _viewController: ThermometerDetailVC = { - let controller = ThermometerDetailVC(navigator: self, item: item, pages: pages) - controller.navigationCoordinator = self - return controller - }() - - init(item: ItemBundle, pages: [DetailPage]) { - self.item = item - self.pages = pages +protocol ReconnectUseCase { + func invoke() -> Completable +} + +final class ReconnectUseCaseImpl: ReconnectUseCase { + @Singleton private var disconnectUseCase + @Singleton private var suplaAppProvider + + func invoke() -> Completable { + disconnectUseCase + .invoke() + .andThen( + Completable.create { completable in + self.suplaAppProvider.initSuplaClient() + + completable(.completed) + return Disposables.create() + } + ) } } diff --git a/SUPLA/UseCase/LegacyWrapper.swift b/SUPLA/UseCase/LegacyWrapper.swift index fdb97e81f..bad273a85 100644 --- a/SUPLA/UseCase/LegacyWrapper.swift +++ b/SUPLA/UseCase/LegacyWrapper.swift @@ -206,17 +206,6 @@ final class UseCaseLegacyWrapper: NSObject { } } - @objc - static func insertNotification(_ userInfo: [AnyHashable: Any]) { - @Singleton var insertNotificationUseCase - - do { - try insertNotificationUseCase.invoke(userInfo: userInfo).subscribeSynchronous() - } catch { - SALog.error("Could not insert notification: \(String(describing: error))") - } - } - @objc static func getChannelIcon(_ channel: SAChannelBase, _ iconType: IconType) -> UIImage? { @Singleton var getChannelBaseIconUseCase diff --git a/SUPLA/UseCase/Lock/CheckPinUseCase.swift b/SUPLA/UseCase/Lock/CheckPinUseCase.swift new file mode 100644 index 000000000..e5018872a --- /dev/null +++ b/SUPLA/UseCase/Lock/CheckPinUseCase.swift @@ -0,0 +1,132 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import RxSwift + +private let FIRST_STAGE_LOCK_TIME_SECS: TimeInterval = 5 +private let SECOND_STAGE_LOCK_TIME_SECS: TimeInterval = 60 +private let THIRD_STAGE_LOCK_TIME_SECS: TimeInterval = 300 +private let FOURTH_STAGE_LOCK_TIME_SECS: TimeInterval = 600 + +protocol CheckPinUseCase { + func invoke(unlockAction: LockScreenFeature.UnlockAction, pinAction: CheckPinAction) -> Single +} + +final class CheckPinUseCaseImpl: CheckPinUseCase { + @Singleton private var settings + @Singleton private var dateProvider + @Singleton private var profileRepository + @Singleton private var stateHandler + + func invoke(unlockAction: LockScreenFeature.UnlockAction, pinAction: CheckPinAction) -> Single { + Single.create { single in + var settings = self.settings + let lockScreenSettings = settings.lockScreenSettings + + if (pinAction.inAuthorized(pinSum: lockScreenSettings.pinSum)) { + settings.lockScreenSettings = lockScreenSettings.copy(failsCount: .value(0), lockTime: .value(nil)) + single(.success(.unlocked)) + } else { + single(.success(self.onWrongPin(lockScreenSettings))) + } + + return Disposables.create() + } + .flatMap { self.checkProfileExist(unlockAction, $0) } + } + + private func checkProfileExist(_ unlockAction: LockScreenFeature.UnlockAction, _ result: CheckPinResult) -> Single { + if (result == .unlocked) { + profileRepository.getActiveProfile() + .map { _ in CheckPinResult.unlocked } + .ifEmpty(switchTo: Observable.just(CheckPinResult.unlockedNoAccount)) + .map { self.performActionSpecificWork(unlockAction, result: $0) } + .asSingle() + } else { + Single.just(result) + } + } + + private func onWrongPin(_ lockScreenSettings: LockScreenSettings) -> CheckPinResult { + var settings = settings + + let lockTime = getLockTime(lockScreenSettings) + settings.lockScreenSettings = lockScreenSettings.copy( + failsCount: .value(lockScreenSettings.failsCount + 1), + lockTime: .value(lockTime) + ) + + return .failure + } + + private func getLockTime(_ lockScreenSettings: LockScreenSettings) -> TimeInterval? { + if (lockScreenSettings.failsCount == 5) { + dateProvider.currentTimestamp() + FIRST_STAGE_LOCK_TIME_SECS + } else if (lockScreenSettings.failsCount == 10) { + dateProvider.currentTimestamp() + SECOND_STAGE_LOCK_TIME_SECS + } else if (lockScreenSettings.failsCount == 15) { + dateProvider.currentTimestamp() + THIRD_STAGE_LOCK_TIME_SECS + } else if (lockScreenSettings.failsCount >= 20) { + dateProvider.currentTimestamp() + FOURTH_STAGE_LOCK_TIME_SECS + } else { + lockScreenSettings.lockTime + } + } + + private func performActionSpecificWork(_ unlockAction: LockScreenFeature.UnlockAction, result: CheckPinResult) -> CheckPinResult { + var settings = settings + + switch (unlockAction) { + case .authorizeApplication: + if (result == .unlocked) { + stateHandler.handle(event: .unlock) + } else if (result == .unlockedNoAccount) { + stateHandler.handle(event: .noAccount) + } + case .turnOffPin: + settings.lockScreenSettings = LockScreenSettings.DEFAULT + case .confirmAuthorizeApplication: + settings.lockScreenSettings = settings.lockScreenSettings.copy(scope: .value(.application)) + case .confirmAuthorizeAccounts: + settings.lockScreenSettings = settings.lockScreenSettings.copy(scope: .value(.accounts)) + case .authorizeAccountsCreate, .authorizeAccountsEdit: break + } + return result + } +} + +enum CheckPinResult { + case unlocked + case unlockedNoAccount + case failure +} + +enum CheckPinAction: Equatable { + case checkPin(pin: String) + case biometricGranted + case biometricRejected + + func inAuthorized(pinSum: String?) -> Bool { + switch (self) { + case .checkPin(let pin): pin.sha1() == pinSum + case .biometricGranted: true + case .biometricRejected: false + } + } +} diff --git a/SUPLA/UseCase/Profile/ActivateProfileUseCase.swift b/SUPLA/UseCase/Profile/ActivateProfileUseCase.swift index 68324449d..226cf39af 100644 --- a/SUPLA/UseCase/Profile/ActivateProfileUseCase.swift +++ b/SUPLA/UseCase/Profile/ActivateProfileUseCase.swift @@ -19,7 +19,7 @@ import RxSwift protocol ActivateProfileUseCase { - func invoke(profileId: ProfileID, force: Bool) -> Observable + func invoke(profileId: ProfileID, force: Bool) -> Completable } final class ActivateProfileUseCaseImpl: ActivateProfileUseCase { @@ -27,43 +27,38 @@ final class ActivateProfileUseCaseImpl: ActivateProfileUseCase { @Singleton private var profileRepository @Singleton private var runtimeConfig @Singleton private var cloudConfigHolder - @Singleton private var suplaApp - @Singleton private var suplaClientProvider + @Singleton private var reconnectUseCase - func invoke(profileId: ProfileID, force: Bool) -> Observable { + func invoke(profileId: ProfileID, force: Bool) -> Completable { profileRepository.queryItem(profileId) - .flatMap { + .flatMapCompletable { guard let profile = $0 else { - return Observable.just(false) + return Completable.complete() } - + if (profile.isActive && !force) { - return Observable.just(false) + return Completable.complete() } return self.activateProfile(profile) } } - private func activateProfile(_ profile: AuthProfileItem) -> Observable { + private func activateProfile(_ profile: AuthProfileItem) -> Completable { profileRepository.getAllProfiles() .map { profiles in profiles.forEach { $0.isActive = $0.objectID == profile.objectID } return profiles } - .flatMapFirst { profiles in + .flatMapFirst { _ in self.profileRepository.save() - }.map { + } + .flatMapCompletable { _ in var config = self.runtimeConfig config.activeProfileId = profile.objectID self.cloudConfigHolder.clean() - // reconect - self.suplaApp.cancelAllRestApiClientTasks() - self.suplaClientProvider.provide().reconnect() - - return true + return self.reconnectUseCase.invoke() } } - } diff --git a/SUPLA/UseCase/Profile/DeleteAllProfileDataUseCase.swift b/SUPLA/UseCase/Profile/DeleteAllProfileDataUseCase.swift index c8675fa55..df9cbae2f 100644 --- a/SUPLA/UseCase/Profile/DeleteAllProfileDataUseCase.swift +++ b/SUPLA/UseCase/Profile/DeleteAllProfileDataUseCase.swift @@ -39,6 +39,7 @@ final class DeleteAllProfileDataUseCaseImpl: DeleteAllProfileDataUseCase { @Singleton private var thermostatMeasurementItemRepository @Singleton private var generalPurposeMeterItemRepository @Singleton private var generalPurposeMeasurementItemRepository + @Singleton private var channelConfigRepository func invoke(profile: AuthProfileItem) -> Observable { return Observable.combineLatest([ @@ -55,7 +56,8 @@ final class DeleteAllProfileDataUseCaseImpl: DeleteAllProfileDataUseCase { self.userIconRepository.deleteAll(for: profile), self.thermostatMeasurementItemRepository.deleteAll(for: profile), self.generalPurposeMeterItemRepository.deleteAll(for: profile), - self.generalPurposeMeasurementItemRepository.deleteAll(for: profile) + self.generalPurposeMeasurementItemRepository.deleteAll(for: profile), + self.channelConfigRepository.deleteAllFor(profile: profile) ]).map { $0.first } diff --git a/SUPLA/UseCase/Profile/DeleteProfileUseCase.swift b/SUPLA/UseCase/Profile/DeleteProfileUseCase.swift index f2c93080a..8bb2b110d 100644 --- a/SUPLA/UseCase/Profile/DeleteProfileUseCase.swift +++ b/SUPLA/UseCase/Profile/DeleteProfileUseCase.swift @@ -23,14 +23,14 @@ protocol DeleteProfileUseCase { } final class DeleteProfileUseCaseImpl: DeleteProfileUseCase { - @Singleton private var profileRepository @Singleton private var singleCall @Singleton private var deleteAllProfileDataUseCase @Singleton private var activateProfileUseCase - @Singleton private var suplaApp @Singleton private var runtimeConfig @Singleton var settings + @Singleton private var disconnectUseCase + @Singleton private var suplaAppStateHolder func invoke(profileId: ProfileID) -> Observable { profileRepository.queryItem(profileId) @@ -40,14 +40,13 @@ final class DeleteProfileUseCaseImpl: DeleteProfileUseCase { } if (profile.isActive) { - self.suplaApp.terminateSuplaClient() - - return self.activateAndRemove(profile: profile) + return self.disconnectUseCase.invoke() + .andThen(self.activateAndRemove(profile: profile)) } else { let serverAddress = self.getServerAddress(profile) return self.removeLocally(profile: profile) .map { - return DeleteProfileResult( + DeleteProfileResult( restartNeeded: false, reauthNeeded: false, servertAddress: serverAddress @@ -59,24 +58,31 @@ final class DeleteProfileUseCaseImpl: DeleteProfileUseCase { private func removeLocally(profile: AuthProfileItem) -> Observable { removeToken(profile: profile) - - return profileRepository.delete(profile) - .flatMap { _ in self.deleteAllProfileDataUseCase.invoke(profile: profile) } + .andThen(deleteAllProfileDataUseCase.invoke(profile: profile)) + .flatMap { self.profileRepository.delete(profile) } + .flatMap { self.profileRepository.save() } } - private func removeToken(profile: AuthProfileItem) { - if let authInfo = profile.authInfo, - authInfo.isAuthDataComplete { - var authDetails = SingleCallWrapper.prepareAuthorizationDetails(for: profile) - var tokenDetails = SingleCallWrapper.prepareClientToken(for: nil, andProfile: profile.name) + private func removeToken(profile: AuthProfileItem) -> Completable { + Completable.create { completable in - do { - try singleCall.registerPushToken(&authDetails, Int32(authInfo.preferredProtocolVersion), &tokenDetails) - } catch { - SALog.error("Push token removal failed with error: \(error)") + if let authInfo = profile.authInfo, + authInfo.isAuthDataComplete + { + var authDetails = SingleCallWrapper.prepareAuthorizationDetails(for: profile) + var tokenDetails = SingleCallWrapper.prepareClientToken(for: nil, andProfile: profile.name) + + do { + try self.singleCall.registerPushToken(&authDetails, Int32(authInfo.preferredProtocolVersion), &tokenDetails) + } catch { + SALog.error("Push token removal failed with error: \(error)") + } + } else { + SALog.info("Push token removal skipped because of incomplete data") } - } else { - SALog.info("Push token removal skipped because of incomplete data") + + completable(.completed) + return Disposables.create() } } @@ -89,20 +95,17 @@ final class DeleteProfileUseCaseImpl: DeleteProfileUseCase { return self.activateProfileUseCase.invoke( profileId: inactiveProfile.objectID, force: true - ).flatMap { activated in - if (activated) { - return self.removeLocally(profile: profile) - .map { - DeleteProfileResult( - restartNeeded: false, - reauthNeeded: true, - servertAddress: serverAddress - ) - } - } - - return Observable.error(DeleteProfileError.otherProfileNotActivated) - } + ) + .andThen( + self.removeLocally(profile: profile) + .map { + DeleteProfileResult( + restartNeeded: false, + reauthNeeded: true, + servertAddress: serverAddress + ) + } + ) } else { // Removing last account return self.removeLocally(profile: profile) @@ -112,6 +115,7 @@ final class DeleteProfileUseCaseImpl: DeleteProfileUseCase { config.activeProfileId = nil settings.anyAccountRegistered = false + self.suplaAppStateHolder.handle(event: .noAccount) return DeleteProfileResult( restartNeeded: true, diff --git a/SUPLA/UseCase/Profile/SaveOrCreateProfileUseCase.swift b/SUPLA/UseCase/Profile/SaveOrCreateProfileUseCase.swift index 9141b8248..31a526cca 100644 --- a/SUPLA/UseCase/Profile/SaveOrCreateProfileUseCase.swift +++ b/SUPLA/UseCase/Profile/SaveOrCreateProfileUseCase.swift @@ -25,8 +25,8 @@ protocol SaveOrCreateProfileUseCase { final class SaveOrCreateProfileUseCaseImpl: SaveOrCreateProfileUseCase { @Singleton private var profileRepository - @Singleton private var suplaClientProvider @Singleton private var globalSettings + @Singleton private var stateHolder func invoke(profileId: ProfileID?, name: String, advancedMode: Bool, authInfo: AuthInfo) -> Observable { self.profileRepository.getAllProfiles() @@ -59,7 +59,7 @@ final class SaveOrCreateProfileUseCaseImpl: SaveOrCreateProfileUseCase { let needsReauth = profile.isActive && authDataChanged if (needsReauth) { - self.suplaClientProvider.provide().reconnect() + self.stateHolder.handle(event: .connecting) } return SaveOrCreateProfileResult( diff --git a/SUPLA/main.m b/SUPLA/main.m index 71570b83c..caecbf7e0 100644 --- a/SUPLA/main.m +++ b/SUPLA/main.m @@ -17,7 +17,7 @@ */ #import -#import "AppDelegate.h" +#import "SUPLA-Swift.h" int main(int argc, char * argv[]) { @autoreleasepool { diff --git a/SUPLATests/Mocks/GlobalSettingsMock.swift b/SUPLATests/Mocks/GlobalSettingsMock.swift index 8dd094ebc..66f76b8ae 100644 --- a/SUPLATests/Mocks/GlobalSettingsMock.swift +++ b/SUPLATests/Mocks/GlobalSettingsMock.swift @@ -19,6 +19,7 @@ @testable import SUPLA class GlobalSettingsMock: GlobalSettings { + var anyAccountRegisteredReturns: Bool = false var anyAccountRegisteredValues: [Bool] = [] var anyAccountRegistered: Bool { @@ -109,4 +110,11 @@ class GlobalSettingsMock: GlobalSettings { get { darkModeReturns } set { darkModeValues.append(newValue) } } + + var lockScreenSettingsReturns: LockScreenSettings = LockScreenSettings.DEFAULT + var lockScreenSettingsValues: [LockScreenSettings] = [] + var lockScreenSettings: LockScreenSettings { + get { lockScreenSettingsReturns } + set { lockScreenSettingsValues.append(newValue) } + } } diff --git a/SUPLATests/Mocks/Infrastructure/DatabaseProxyMock.swift b/SUPLATests/Mocks/Infrastructure/DatabaseProxyMock.swift new file mode 100644 index 000000000..9b9b6f737 --- /dev/null +++ b/SUPLATests/Mocks/Infrastructure/DatabaseProxyMock.swift @@ -0,0 +1,27 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +@testable import SUPLA + +final class DatabaseProxyMock: DatabaseProxy { + var setupCalls = 0 + func setup() { + setupCalls += 1 + } +} diff --git a/SUPLATests/Mocks/Infrastructure/DateProviderMock.swift b/SUPLATests/Mocks/Infrastructure/DateProviderMock.swift index e05257de5..1b232ce95 100644 --- a/SUPLATests/Mocks/Infrastructure/DateProviderMock.swift +++ b/SUPLATests/Mocks/Infrastructure/DateProviderMock.swift @@ -27,11 +27,11 @@ final class DateProviderMock: DateProvider { return currentDateReturns } - var currentTimestampReturns = 0.0 + var currentTimestampReturns: MockReturns = .empty() var currentTimestampCalls = 0 func currentTimestamp() -> TimeInterval { currentTimestampCalls += 1 - return currentTimestampReturns + return currentTimestampReturns.next() } var currentDayOfWeekCalls = 0 @@ -55,3 +55,46 @@ final class DateProviderMock: DateProvider { return currentMinuteReturns } } + +class MockReturns { + + private let values: [T] + private let empty: Bool + private var idx = 0 + + private init(values: [T]) { + self.values = values + self.empty = false + } + + private init() { + self.values = [] + self.empty = true + } + + func next() -> T { + if (empty) { + fatalError("Not mocked!") + } + + if (idx >= values.count) { + return values.last! + } + + let returns = values[idx] + idx += 1 + return returns + } + + static func single(_ value: V) -> MockReturns { + MockReturns(values: [value]) + } + + static func many(_ values: [V]) -> MockReturns { + MockReturns(values: values) + } + + static func empty() -> MockReturns { + MockReturns() + } +} diff --git a/SUPLATests/Mocks/Infrastructure/ThreadHandlerMock.swift b/SUPLATests/Mocks/Infrastructure/ThreadHandlerMock.swift index 16a169c41..c1780bc44 100644 --- a/SUPLATests/Mocks/Infrastructure/ThreadHandlerMock.swift +++ b/SUPLATests/Mocks/Infrastructure/ThreadHandlerMock.swift @@ -23,4 +23,9 @@ final class ThreadHandlerMock: ThreadHandler { func sleep(_ timeInterval: TimeInterval) { sleepParameters.append(timeInterval) } + + var usleepParameters: [UInt32] = [] + func usleepProxy(_ microseconds: UInt32) { + usleepParameters.append(microseconds) + } } diff --git a/SUPLATests/Mocks/ListsEventsManagerMock.swift b/SUPLATests/Mocks/ListsEventsManagerMock.swift index 15949eae5..f3f86a9b3 100644 --- a/SUPLATests/Mocks/ListsEventsManagerMock.swift +++ b/SUPLATests/Mocks/ListsEventsManagerMock.swift @@ -94,4 +94,9 @@ final class UpdateEventsManagerMock: UpdateEventsManager { func emitScenesUpdate() { emitSceneUpdateCounter += 1 } + + var cleanupCounter = 0 + func cleanup() { + cleanupCounter += 1 + } } diff --git a/SUPLATests/Mocks/Navigation/SuplaAppCoordinatorMock.swift b/SUPLATests/Mocks/Navigation/SuplaAppCoordinatorMock.swift new file mode 100644 index 000000000..269b8c19c --- /dev/null +++ b/SUPLATests/Mocks/Navigation/SuplaAppCoordinatorMock.swift @@ -0,0 +1,169 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + + +import XCTest +@testable import SUPLA + +final class SuplaAppCoordinatorMock: SuplaAppCoordinator { + + private let navigationControllerMock = NavigationControllerMock() + + var navigationController: UINavigationController { + get { navigationControllerMock } + set { } + } + + func attachToWindow(_ window: UIWindow) { + } + + func currentController() -> UIViewController? { UIViewController() } + + var navigateToMainMock: FunctionMock = .void() + func navigateToMain() { + navigateToMainMock.handle(()) + } + + func navigateToSettings() { + } + + func navigateToLocationOrdering() { + } + + var navigateToProfilesMock: FunctionMock = .void() + func navigateToProfiles() { + navigateToProfilesMock.handle(()) + } + + func navigateToAddWizard() { + } + + func navigateToAbout() { + } + + func navigateToNotificationsLog() { + } + + func navigateToDeviceCatalog() { + } + + var navigateToProfileMock: FunctionMock = .void() + func navigateToProfile(profileId: ProfileID?) { + navigateToProfileMock.handle(profileId) + } + + var navigateToProfileWithLockCheckMock: FunctionMock<(ProfileID?, Bool), Void> = .void() + func navigateToProfile(profileId: ProfileID?, withLockCheck: Bool) { + navigateToProfileWithLockCheckMock.handle((profileId, withLockCheck)) + } + + func navigateToCreateAccountWeb() { + } + + func navigateToRemoveAccountWeb(needsRestart: Bool, serverAddress: String?) { + } + + func navigateToLegacyDetail(_ detailType: LegacyDetailType, channelBase: SAChannelBase) { + } + + func navigateToSwitchDetail(item: ItemBundle, pages: [DetailPage]) { + } + + func navigateToThermostatDetail(item: ItemBundle, pages: [DetailPage]) { + } + + func navigateToThermometerDetail(item: ItemBundle, pages: [DetailPage]) { + } + + func navigateToGpmDetail(item: ItemBundle, pages: [DetailPage]) { + } + + func navigateToWindowDetail(item: ItemBundle, pages: [DetailPage]) { + } + + func navigateToPinSetup(lockScreenScope: LockScreenScope) { + } + + var navigateToLockScreenMock: FunctionMock = .void() + func navigateToLockScreen(unlockAction: LockScreenFeature.UnlockAction) { + navigateToLockScreenMock.handle(unlockAction) + } + + func popToStatus() { + } + + func showMenu() { + } + + func showAuthorization() { + } + + var showLoginMock: FunctionMock = .void() + func showLogin() { + showLoginMock.handle(()) + } + + func openForum() { + } + + func openCloud() { + } + + func openUrl(url: String) { + } + + func openUrl(url: URL) { + } + + func start(animated: Bool) { + } + + func verifyPopViewController(_ parameters: [Bool]) { + XCTAssertEqual(navigationControllerMock.popViewControllerParameters, parameters) + } +} + +final class NavigationControllerMock: UINavigationController { + + var popViewControllerParameters: [Bool] = [] + override func popViewController(animated: Bool) -> UIViewController? { + popViewControllerParameters.append(animated) + return nil + } +} + +class FunctionMock { + var parameters: [Parameters] = [] + var returns: MockReturns = .empty() + + func handle(_ parameters: Parameters) -> Returns { + self.parameters.append(parameters) + return returns.next() + } + + func verifyCalls(_ count: Int) { + XCTAssertEqual(parameters.count, count) + } + + static func void

() -> FunctionMock { + let mock = FunctionMock() + mock.returns = .single(()) + return mock + } +} diff --git a/SUPLATests/Mocks/Repositories/ChannelConfigRepositoryMock.swift b/SUPLATests/Mocks/Repositories/ChannelConfigRepositoryMock.swift index 2b02be5e7..483101215 100644 --- a/SUPLATests/Mocks/Repositories/ChannelConfigRepositoryMock.swift +++ b/SUPLATests/Mocks/Repositories/ChannelConfigRepositoryMock.swift @@ -28,6 +28,13 @@ final class ChannelConfigRepositoryMock: BaseRepositoryMock, Ch return deleteAllReturns } + var deleteAllForProfileParameters: [AuthProfileItem] = [] + var deleteAllForProfileReturns: Observable = .empty() + func deleteAllFor(profile: AuthProfileItem) -> Observable { + deleteAllForProfileParameters.append(profile) + return deleteAllForProfileReturns + } + var getConfigParameters: [Int32] = [] var getConfigReturns: Observable = .empty() var getConfigReturnsMap: [Int32: Observable] = [:] diff --git a/SUPLATests/Mocks/SuplaAppProviderMock.swift b/SUPLATests/Mocks/SuplaAppProviderMock.swift new file mode 100644 index 000000000..0a3d371b2 --- /dev/null +++ b/SUPLATests/Mocks/SuplaAppProviderMock.swift @@ -0,0 +1,74 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + + +@testable import SUPLA + +class SuplaAppProviderMock: SuplaAppProvider { + var suplaAppMock = SuplaAppApiMock() + + func provide() -> SuplaAppApi { suplaAppMock } + + var revokeOAuthTokenCalls = 0 + func revokeOAuthToken() { + revokeOAuthTokenCalls += 1 + } + + var initClientWithOneTimePasswordParameters: [String] = [] + func initClientWithOneTimePassword(_ password: String) { + initClientWithOneTimePasswordParameters.append(password) + } + + var initSuplaClientCalls = 0 + func initSuplaClient() { + initSuplaClientCalls += 1 + } + + +} + +class SuplaAppApiMock: NSObject, SuplaAppApi { + + var cancelAllRestApiClientTasksCalls = 0 + func cancelAllRestApiClientTasks() { + cancelAllRestApiClientTasksCalls += 1 + } + + var isClientRegisteredCalls = 0 + var isClientRegisteredReturns = false + func isClientRegistered() -> Bool { + isClientRegisteredCalls += 1 + return isClientRegisteredReturns + } + + var isClientWorkingCalls = 0 + var isClientWorkingReturns = false + func isClientWorking() -> Bool { + isClientWorkingCalls += 1 + return isClientWorkingReturns + } + + var isClientAuthorizedCalls = 0 + var isClientAuthroziedReturns = false + func isClientAuthorized() -> Bool { + isClientAuthorizedCalls += 1 + return isClientAuthroziedReturns + } + +} diff --git a/SUPLATests/Mocks/SuplaAppStateHolderMock.swift b/SUPLATests/Mocks/SuplaAppStateHolderMock.swift new file mode 100644 index 000000000..bff1d5804 --- /dev/null +++ b/SUPLATests/Mocks/SuplaAppStateHolderMock.swift @@ -0,0 +1,36 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + + +@testable import SUPLA +import RxSwift + +final class SuplaAppStateHolderMock: SuplaAppStateHolder { + var stateCounter = 0 + var stateReturns: Observable = .empty() + func state() -> Observable { + stateCounter += 1 + return stateReturns + } + + var handleParameters: [SuplaAppEvent] = [] + func handle(event: SuplaAppEvent) { + handleParameters.append(event) + } +} diff --git a/SUPLATests/Mocks/SuplaClientProviderMock.swift b/SUPLATests/Mocks/SuplaClientProviderMock.swift index c31fa7177..97c1f0e12 100644 --- a/SUPLATests/Mocks/SuplaClientProviderMock.swift +++ b/SUPLATests/Mocks/SuplaClientProviderMock.swift @@ -25,12 +25,27 @@ class SuplaClientProviderMock: SuplaClientProvider { } class SuplaClientProtocolMock: NSObject, SuplaClientProtocol { + var cancelCalls: Int = 0 func cancel() { cancelCalls += 1 } var reconnectCalls: Int = 0 func reconnect() { reconnectCalls += 1 } + var isCancelledCalls = 0 + var isCancelledReturns = false + func isCancelled() -> Bool { + isCancelledCalls += 1 + return isCancelledReturns + } + + var isFinishedCalls = 0 + var isFinishedReturns = false + func isFinished() -> Bool { + isFinishedCalls += 1 + return isFinishedReturns + } + var executeActionParameters: [(Int32, Int32, Int32, UnsafeMutableRawPointer?, Int32)] = [] var executeActionReturns = false func executeAction(_ actionId: Int32, subjecType subjectType: Int32, subjectId: Int32, parameters: UnsafeMutableRawPointer!, length: Int32) -> Bool { diff --git a/SUPLATests/Mocks/UseCase/ClientUseCasesMocks.swift b/SUPLATests/Mocks/UseCase/ClientUseCasesMocks.swift index a2561e137..f1608747c 100644 --- a/SUPLATests/Mocks/UseCase/ClientUseCasesMocks.swift +++ b/SUPLATests/Mocks/UseCase/ClientUseCasesMocks.swift @@ -81,3 +81,26 @@ final class AuthorizeUseCaseMock: AuthorizeUseCase { return returns } } + +final class DisconnectUseCaseMock: DisconnectUseCase { + var invokeCounter = 0 + var invokeReturns: Completable = .empty() + func invoke() -> Completable { + invokeCounter += 1 + return invokeReturns + } + + var invokeSynchronousCounter = 0 + func invokeSynchronous() { + invokeSynchronousCounter += 1 + } +} + +final class ReconnectUseCaseMock: ReconnectUseCase { + var invokeCounter = 0 + var returns: Completable = .empty() + func invoke() -> Completable { + invokeCounter += 1 + return returns + } +} diff --git a/SUPLATests/Mocks/UseCase/LockUseCasesMocks.swift b/SUPLATests/Mocks/UseCase/LockUseCasesMocks.swift new file mode 100644 index 000000000..0aa009aa5 --- /dev/null +++ b/SUPLATests/Mocks/UseCase/LockUseCasesMocks.swift @@ -0,0 +1,30 @@ +// +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import RxSwift +@testable import SUPLA + +final class CheckPinUseCaseMock: CheckPinUseCase { + var parameters: [(LockScreenFeature.UnlockAction, CheckPinAction)] = [] + var returns: Single = .error(GeneralError.illegalState(message: "Not mocked")) + func invoke(unlockAction: LockScreenFeature.UnlockAction, pinAction: CheckPinAction) -> Single { + parameters.append((unlockAction, pinAction)) + return returns + } +} diff --git a/SUPLATests/Mocks/UseCase/ProfileUseCasesMocks.swift b/SUPLATests/Mocks/UseCase/ProfileUseCasesMocks.swift index 30cced43d..fbc58392c 100644 --- a/SUPLATests/Mocks/UseCase/ProfileUseCasesMocks.swift +++ b/SUPLATests/Mocks/UseCase/ProfileUseCasesMocks.swift @@ -48,8 +48,8 @@ final class SaveOrCreateProfileUseCaseMock: SaveOrCreateProfileUseCase { final class ActivateProfileUseCaseMock: ActivateProfileUseCase { var parameters: [(ProfileID, Bool)] = [] - var returns: Observable = .empty() - func invoke(profileId: ProfileID, force: Bool) -> Observable { + var returns: Completable = .empty() + func invoke(profileId: ProfileID, force: Bool) -> Completable { parameters.append((profileId, force)) return returns } diff --git a/SUPLATests/Mocks/ValuesFormatterMock.swift b/SUPLATests/Mocks/ValuesFormatterMock.swift index b3910d126..49b5461d6 100644 --- a/SUPLATests/Mocks/ValuesFormatterMock.swift +++ b/SUPLATests/Mocks/ValuesFormatterMock.swift @@ -30,6 +30,8 @@ final class ValuesFormatterMock: ValuesFormatter { func minutesToString(minutes: Int) -> String { "\(minutes)" } + func secondsToString(_ time: TimeInterval) -> String { "\(time)" } + func getHourString(hour: Hour?) -> String? { guard let hour = hour else { return "nil" } return "\(hour.hour):\(hour.minute)" diff --git a/SUPLATests/Tests/Core/UI/Dialogs/SAAuthorizationDialogVMTests.swift b/SUPLATests/Tests/Core/UI/Dialogs/SAAuthorizationDialogVMTests.swift index fbd8039e5..067ed4d55 100644 --- a/SUPLATests/Tests/Core/UI/Dialogs/SAAuthorizationDialogVMTests.swift +++ b/SUPLATests/Tests/Core/UI/Dialogs/SAAuthorizationDialogVMTests.swift @@ -21,25 +21,25 @@ import RxSwift @testable import SUPLA -final class SAAuthorizationDialogVMTests: ViewModelTest { +final class SAAuthorizationDialogVMTests: ViewModelTest { private lazy var profileRepository: ProfileRepositoryMock! = ProfileRepositoryMock() - private lazy var suplaClientProvider: SuplaClientProviderMock! = SuplaClientProviderMock() private lazy var authorizeUseCase: AuthorizeUseCaseMock! = AuthorizeUseCaseMock() + private lazy var suplaAppProvider: SuplaAppProviderMock! = SuplaAppProviderMock() private lazy var schedulers: SuplaSchedulersMock! = SuplaSchedulersMock() private lazy var viewModel: SAAuthorizationDialogVM! = SAAuthorizationDialogVM() override func setUp() { DiContainer.shared.register(type: (any ProfileRepository).self, profileRepository!) - DiContainer.shared.register(type: SuplaClientProvider.self, suplaClientProvider!) DiContainer.shared.register(type: AuthorizeUseCase.self, authorizeUseCase!) + DiContainer.shared.register(type: SuplaAppProvider.self, suplaAppProvider!) DiContainer.shared.register(type: SuplaSchedulers.self, schedulers!) } override func tearDown() { profileRepository = nil - suplaClientProvider = nil authorizeUseCase = nil + suplaAppProvider = nil schedulers = nil viewModel = nil @@ -58,8 +58,8 @@ final class SAAuthorizationDialogVMTests: ViewModelTest { + private lazy var viewModel: AppSettingsVM! = AppSettingsVM() - private lazy var viewModel: AppSettingsVM! = { AppSettingsVM() }() - - private lazy var settings: GlobalSettingsMock! = { - GlobalSettingsMock() - }() - private lazy var notificationCenter: UserNotificationCenterMock! = { - UserNotificationCenterMock() - }() + private lazy var settings: GlobalSettingsMock! = GlobalSettingsMock() + + private lazy var notificationCenter: UserNotificationCenterMock! = UserNotificationCenterMock() override func setUp() { DiContainer.shared.register(type: GlobalSettings.self, settings!) @@ -53,7 +49,7 @@ class AppSettingsVMTests: ViewModelTest Int64 { 0 } override func decodeBool(forKey key: String) -> Bool { false } diff --git a/SUPLATests/Tests/Features/Details/SwitchDetail/SwitchGeneral/SwitchGeneralVMTests.swift b/SUPLATests/Tests/Features/Details/SwitchDetail/SwitchGeneral/SwitchGeneralVMTests.swift index eeca96b76..baed69e5a 100644 --- a/SUPLATests/Tests/Features/Details/SwitchDetail/SwitchGeneral/SwitchGeneralVMTests.swift +++ b/SUPLATests/Tests/Features/Details/SwitchDetail/SwitchGeneral/SwitchGeneralVMTests.swift @@ -95,6 +95,7 @@ final class SwitchGeneralVMTest: ViewModelTest { + private lazy var settings: GlobalSettingsMock! = GlobalSettingsMock() + private lazy var dateProvider: DateProviderMock! = DateProviderMock() + private lazy var profileRepository: ProfileRepositoryMock! = ProfileRepositoryMock() + private lazy var stateHolder: SuplaAppStateHolderMock! = SuplaAppStateHolderMock() + + private lazy var useCase: CheckPinUseCase! = CheckPinUseCaseImpl() + + override func setUp() { + super.setUp() + + DiContainer.shared.register(type: GlobalSettings.self, settings!) + DiContainer.shared.register(type: DateProvider.self, dateProvider!) + DiContainer.shared.register(type: (any ProfileRepository).self, profileRepository!) + DiContainer.shared.register(type: SuplaAppStateHolder.self, stateHolder!) + } + + override func tearDown() { + settings = nil + dateProvider = nil + profileRepository = nil + stateHolder = nil + + useCase = nil + + super.tearDown() + } + + func test_shouldUnlock_checkPinAction() { + // given + let password = "123" + let profile = AuthProfileItem(testContext: nil) + settings.lockScreenSettingsReturns = LockScreenSettings(scope: .application, pinSum: password.sha1(), biometricAllowed: false, failsCount: 2, lockTime: 10) + profileRepository.activeProfileObservable = .just(profile) + + // when + useCase.invoke(unlockAction: .authorizeApplication, pinAction: .checkPin(pin: password)).subscribe(observer).disposed(by: disposeBag) + + // then + let checkPinResult = try? result?.get() + XCTAssertEqual(checkPinResult, CheckPinResult.unlocked) + + XCTAssertEqual(stateHolder.handleParameters, [.unlock]) + XCTAssertEqual(settings.lockScreenSettingsValues, [LockScreenSettings(scope: .application, pinSum: password.sha1(), biometricAllowed: false)]) // Cleanups wrong pins count + XCTAssertEqual(dateProvider.currentTimestampCalls, 0) + } + + func test_shouldUnlock_biometricGranted_noProfile() { + // given + let password = "123" + settings.lockScreenSettingsReturns = LockScreenSettings(scope: .application, pinSum: password.sha1(), biometricAllowed: false, failsCount: 2, lockTime: 10) + profileRepository.activeProfileObservable = .empty() + + // when + useCase.invoke(unlockAction: .authorizeApplication, pinAction: .biometricGranted).subscribe(observer).disposed(by: disposeBag) + + // then + let checkPinResult = try? result?.get() + XCTAssertEqual(checkPinResult, CheckPinResult.unlockedNoAccount) + + XCTAssertEqual(stateHolder.handleParameters, [.noAccount]) + XCTAssertEqual(settings.lockScreenSettingsValues, [LockScreenSettings(scope: .application, pinSum: password.sha1(), biometricAllowed: false)]) // Cleanups wrong pins count + XCTAssertEqual(dateProvider.currentTimestampCalls, 0) + } + + func test_shouldUnlock_turnOffPin() { + // given + let password = "123" + let profile = AuthProfileItem(testContext: nil) + profileRepository.activeProfileObservable = .just(profile) + + let lockScreenSettings = LockScreenSettings(scope: .application, pinSum: password.sha1(), biometricAllowed: false, failsCount: 2, lockTime: 10) + settings.lockScreenSettingsReturns = lockScreenSettings + + // when + useCase.invoke(unlockAction: .turnOffPin, pinAction: .checkPin(pin: password)).subscribe(observer).disposed(by: disposeBag) + + // then + let checkPinResult = try? result?.get() + XCTAssertEqual(checkPinResult, CheckPinResult.unlocked) + + XCTAssertEqual(stateHolder.handleParameters, []) + XCTAssertEqual(settings.lockScreenSettingsValues, [LockScreenSettings(scope: .application, pinSum: password.sha1(), biometricAllowed: false), LockScreenSettings.DEFAULT]) + XCTAssertEqual(dateProvider.currentTimestampCalls, 0) + } + + func test_shouldUnlock_changeScope_Accounts() { + // given + let password = "123" + let profile = AuthProfileItem(testContext: nil) + profileRepository.activeProfileObservable = .just(profile) + + let lockScreenSettings = LockScreenSettings(scope: .application, pinSum: password.sha1(), biometricAllowed: false, failsCount: 2, lockTime: 10) + settings.lockScreenSettingsReturns = lockScreenSettings + + // when + useCase.invoke(unlockAction: .confirmAuthorizeAccounts, pinAction: .checkPin(pin: password)).subscribe(observer).disposed(by: disposeBag) + + // then + let checkPinResult = try? result?.get() + XCTAssertEqual(checkPinResult, CheckPinResult.unlocked) + + XCTAssertEqual(stateHolder.handleParameters, []) + XCTAssertEqual(settings.lockScreenSettingsValues, [lockScreenSettings.copy(failsCount: .value(0), lockTime: .value(nil)), lockScreenSettings.copy(scope: .value(.accounts))]) + XCTAssertEqual(dateProvider.currentTimestampCalls, 0) + } + + func test_shouldUnlock_changeScope_Application() { + // given + let password = "123" + let profile = AuthProfileItem(testContext: nil) + profileRepository.activeProfileObservable = .just(profile) + + let lockScreenSettings = LockScreenSettings(scope: .accounts, pinSum: password.sha1(), biometricAllowed: false, failsCount: 2, lockTime: 10) + settings.lockScreenSettingsReturns = lockScreenSettings + + // when + useCase.invoke(unlockAction: .confirmAuthorizeApplication, pinAction: .checkPin(pin: password)).subscribe(observer).disposed(by: disposeBag) + + // then + let checkPinResult = try? result?.get() + XCTAssertEqual(checkPinResult, CheckPinResult.unlocked) + + XCTAssertEqual(stateHolder.handleParameters, []) + XCTAssertEqual(settings.lockScreenSettingsValues, [lockScreenSettings.copy(failsCount: .value(0), lockTime: .value(nil)), lockScreenSettings.copy(scope: .value(.application))]) + XCTAssertEqual(dateProvider.currentTimestampCalls, 0) + } + + func test_wrongPin() { + // given + let password = "123" + let lockScreenSettings = LockScreenSettings(scope: .accounts, pinSum: password.sha1(), biometricAllowed: false, failsCount: 2, lockTime: 10) + settings.lockScreenSettingsReturns = lockScreenSettings + + // when + useCase.invoke(unlockAction: .authorizeApplication, pinAction: .checkPin(pin: "234")).subscribe(observer).disposed(by: disposeBag) + + // then + let checkPinResult = try? result?.get() + XCTAssertEqual(checkPinResult, CheckPinResult.failure) + + XCTAssertEqual(stateHolder.handleParameters, []) + XCTAssertEqual(settings.lockScreenSettingsValues, [lockScreenSettings.copy(failsCount: .value(3))]) + XCTAssertEqual(dateProvider.currentTimestampCalls, 0) + } + + func test_biometricRejected_firstLockTime() { + // given + let password = "123" + let lockScreenSettings = LockScreenSettings(scope: .accounts, pinSum: password.sha1(), biometricAllowed: false, failsCount: 5, lockTime: 10) + settings.lockScreenSettingsReturns = lockScreenSettings + dateProvider.currentTimestampReturns = .single(1000) + + // when + useCase.invoke(unlockAction: .authorizeApplication, pinAction: .biometricRejected).subscribe(observer).disposed(by: disposeBag) + + // then + let checkPinResult = try? result?.get() + XCTAssertEqual(checkPinResult, CheckPinResult.failure) + + XCTAssertEqual(stateHolder.handleParameters, []) + XCTAssertEqual(settings.lockScreenSettingsValues, [lockScreenSettings.copy(failsCount: .value(6), lockTime: .value(1005))]) + XCTAssertEqual(dateProvider.currentTimestampCalls, 1) + } + + func test_biometricRejected_secondLockTime() { + // given + let password = "123" + let lockScreenSettings = LockScreenSettings(scope: .accounts, pinSum: password.sha1(), biometricAllowed: false, failsCount: 10, lockTime: 10) + settings.lockScreenSettingsReturns = lockScreenSettings + dateProvider.currentTimestampReturns = .single(1000) + + // when + useCase.invoke(unlockAction: .authorizeApplication, pinAction: .biometricRejected).subscribe(observer).disposed(by: disposeBag) + + // then + let checkPinResult = try? result?.get() + XCTAssertEqual(checkPinResult, CheckPinResult.failure) + + XCTAssertEqual(stateHolder.handleParameters, []) + XCTAssertEqual(settings.lockScreenSettingsValues, [lockScreenSettings.copy(failsCount: .value(11), lockTime: .value(1060))]) + XCTAssertEqual(dateProvider.currentTimestampCalls, 1) + } + + func test_biometricRejected_thirdLockTime() { + // given + let password = "123" + let lockScreenSettings = LockScreenSettings(scope: .accounts, pinSum: password.sha1(), biometricAllowed: false, failsCount: 15, lockTime: 10) + settings.lockScreenSettingsReturns = lockScreenSettings + dateProvider.currentTimestampReturns = .single(1000) + + // when + useCase.invoke(unlockAction: .authorizeApplication, pinAction: .biometricRejected).subscribe(observer).disposed(by: disposeBag) + + // then + let checkPinResult = try? result?.get() + XCTAssertEqual(checkPinResult, CheckPinResult.failure) + + XCTAssertEqual(stateHolder.handleParameters, []) + XCTAssertEqual(settings.lockScreenSettingsValues, [lockScreenSettings.copy(failsCount: .value(16), lockTime: .value(1300))]) + XCTAssertEqual(dateProvider.currentTimestampCalls, 1) + } + + func test_biometricRejected_fourthLockTime() { + // given + let password = "123" + let lockScreenSettings = LockScreenSettings(scope: .accounts, pinSum: password.sha1(), biometricAllowed: false, failsCount: 20, lockTime: 10) + settings.lockScreenSettingsReturns = lockScreenSettings + dateProvider.currentTimestampReturns = .single(1000) + + // when + useCase.invoke(unlockAction: .authorizeApplication, pinAction: .biometricRejected).subscribe(observer).disposed(by: disposeBag) + + // then + let checkPinResult = try? result?.get() + XCTAssertEqual(checkPinResult, CheckPinResult.failure) + + XCTAssertEqual(stateHolder.handleParameters, []) + XCTAssertEqual(settings.lockScreenSettingsValues, [lockScreenSettings.copy(failsCount: .value(21), lockTime: .value(1600))]) + XCTAssertEqual(dateProvider.currentTimestampCalls, 1) + } +} diff --git a/SUPLATests/Tests/UseCase/Profile/ActivateProfileUseCaseTests.swift b/SUPLATests/Tests/UseCase/Profile/ActivateProfileUseCaseTests.swift index f85a66593..21808fbff 100644 --- a/SUPLATests/Tests/UseCase/Profile/ActivateProfileUseCaseTests.swift +++ b/SUPLATests/Tests/UseCase/Profile/ActivateProfileUseCaseTests.swift @@ -19,7 +19,7 @@ @testable import SUPLA import XCTest -final class ActivateProfileUseCaseTests: UseCaseTest { +final class ActivateProfileUseCaseTests: CompletableTestCase { private lazy var profileRepository: ProfileRepositoryMock! = { ProfileRepositoryMock() @@ -33,13 +33,7 @@ final class ActivateProfileUseCaseTests: UseCaseTest { SuplaCloudConfigHolderMock() }() - private lazy var suplaApp: SuplaAppWrapperMock! = { - SuplaAppWrapperMock() - }() - - private lazy var suplaClientProvider: SuplaClientProviderMock! = { - SuplaClientProviderMock() - }() + private lazy var reconnectUseCase: ReconnectUseCaseMock! = ReconnectUseCaseMock() private lazy var useCase: ActivateProfileUseCaseImpl! = { ActivateProfileUseCaseImpl() @@ -51,8 +45,7 @@ final class ActivateProfileUseCaseTests: UseCaseTest { DiContainer.shared.register(type: (any ProfileRepository).self, profileRepository!) DiContainer.shared.register(type: RuntimeConfig.self, runtimeConfig!) DiContainer.shared.register(type: SuplaCloudConfigHolder.self, cloudConfigHolder!) - DiContainer.shared.register(type: SuplaAppWrapper.self, suplaApp!) - DiContainer.shared.register(type: SuplaClientProvider.self, suplaClientProvider!) + DiContainer.shared.register(type: ReconnectUseCase.self, reconnectUseCase!) } override func tearDown() { @@ -61,8 +54,7 @@ final class ActivateProfileUseCaseTests: UseCaseTest { profileRepository = nil runtimeConfig = nil cloudConfigHolder = nil - suplaApp = nil - suplaClientProvider = nil + reconnectUseCase = nil useCase = nil } @@ -79,10 +71,7 @@ final class ActivateProfileUseCaseTests: UseCaseTest { .disposed(by: disposeBag) // then - assertEvents([ - .next(false), - .completed - ]) + assertEvents(contains: [ .completed ]) } func test_shouldReturnFalseWhenProfileActiveAndForceFalse() { @@ -98,15 +87,10 @@ final class ActivateProfileUseCaseTests: UseCaseTest { .disposed(by: disposeBag) // then - assertEvents([ - .next(false), - .completed - ]) + assertEvents(contains: [ .completed ]) XCTAssertEqual(runtimeConfig.activeProfileIdValues, []) XCTAssertEqual(cloudConfigHolder.cleanCalls, 0) - XCTAssertEqual(suplaApp.cancelAllRestApiClientTasksCalls, 0) - XCTAssertEqual(suplaClientProvider.suplaClientMock.reconnectCalls, 0) } func test_shouldActivateOtherProfile() { @@ -119,6 +103,7 @@ final class ActivateProfileUseCaseTests: UseCaseTest { profileRepository.queryItemByIdObservable = .just(notActiveProfile) profileRepository.allProfilesObservable = .just([activeProfile, notActiveProfile]) profileRepository.saveObservable = .just(()) + reconnectUseCase.returns = .complete() // when useCase.invoke(profileId: notActiveProfile.objectID, force: false) @@ -126,14 +111,9 @@ final class ActivateProfileUseCaseTests: UseCaseTest { .disposed(by: disposeBag) // then - assertEvents([ - .next(true), - .completed - ]) + assertEvents(contains: [ .completed ]) XCTAssertEqual(runtimeConfig.activeProfileIdValues, [notActiveProfile.objectID]) XCTAssertEqual(cloudConfigHolder.cleanCalls, 1) - XCTAssertEqual(suplaApp.cancelAllRestApiClientTasksCalls, 1) - XCTAssertEqual(suplaClientProvider.suplaClientMock.reconnectCalls, 1) } func test_shouldActivateActiveProfileWhenForceTrue() { @@ -144,6 +124,7 @@ final class ActivateProfileUseCaseTests: UseCaseTest { profileRepository.queryItemByIdObservable = .just(activeProfile) profileRepository.allProfilesObservable = .just([activeProfile]) profileRepository.saveObservable = .just(()) + reconnectUseCase.returns = .complete() // when useCase.invoke(profileId: activeProfile.objectID, force: true) @@ -151,13 +132,8 @@ final class ActivateProfileUseCaseTests: UseCaseTest { .disposed(by: disposeBag) // then - assertEvents([ - .next(true), - .completed - ]) + assertEvents(contains: [ .completed ]) XCTAssertEqual(runtimeConfig.activeProfileIdValues, [activeProfile.objectID]) XCTAssertEqual(cloudConfigHolder.cleanCalls, 1) - XCTAssertEqual(suplaApp.cancelAllRestApiClientTasksCalls, 1) - XCTAssertEqual(suplaClientProvider.suplaClientMock.reconnectCalls, 1) } } diff --git a/SUPLATests/Tests/UseCase/Profile/DeleteAllProfileDataUseCaseTests.swift b/SUPLATests/Tests/UseCase/Profile/DeleteAllProfileDataUseCaseTests.swift index 128f04c87..06fc41743 100644 --- a/SUPLATests/Tests/UseCase/Profile/DeleteAllProfileDataUseCaseTests.swift +++ b/SUPLATests/Tests/UseCase/Profile/DeleteAllProfileDataUseCaseTests.swift @@ -16,59 +16,45 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -import XCTest -import RxTest import RxSwift +import RxTest +import XCTest @testable import SUPLA final class DeleteAllProfileDataUseCaseTests: UseCaseTest { - - private lazy var useCase: DeleteAllProfileDataUseCase! = { DeleteAllProfileDataUseCaseImpl() }() - - private lazy var channelExtendedValueRepository: ChannelExtendedValueRepositorMock! = { - ChannelExtendedValueRepositorMock() - }() - private lazy var channelValueRepository: ChannelValueRepositoryMock! = { - ChannelValueRepositoryMock() - }() - private lazy var channelRepository: ChannelRepositoryMock! = { - ChannelRepositoryMock() - }() - private lazy var groupRepository: GroupRepositoryMock! = { - GroupRepositoryMock() - }() - private lazy var electricityMeasurementItemRepository: ElectricityMeasurementItemRepositoryMock! = { - ElectricityMeasurementItemRepositoryMock() - }() - private lazy var impulseCounterMeasurementItemRepository: ImpulseCounterMeasurementItemRepositoryMock! = { - ImpulseCounterMeasurementItemRepositoryMock() - }() - private lazy var locationRepository: LocationRepositoryMock! = { - LocationRepositoryMock() - }() - private lazy var sceneRepository: SceneRepositoryMock! = { - SceneRepositoryMock() - }() - private lazy var temperatureMeasurementItemRepository: TemperatureMeasurementItemRepositoryMock! = { - TemperatureMeasurementItemRepositoryMock() - }() - private lazy var tempHumidityMeasurementItemRepository: TempHumidityMeasurementItemRepositoryMock! = { - TempHumidityMeasurementItemRepositoryMock() - }() - private lazy var userIconRepository: UserIconRepositoryMock! = { - UserIconRepositoryMock() - }() - private lazy var thermostatMeasurementItemRepository: ThermostatMeasurementItemRepositoryMock! = { - ThermostatMeasurementItemRepositoryMock() - }() - private lazy var generalPurposeMeterItemRepository: GeneralPurposeMeterItemRepositoryMock! = { - GeneralPurposeMeterItemRepositoryMock() - }() - private lazy var generalPurposeMeasurementItemRepository: GeneralPurposeMeasurementItemRepositoryMock! = { - GeneralPurposeMeasurementItemRepositoryMock() - }() - + private lazy var useCase: DeleteAllProfileDataUseCase! = DeleteAllProfileDataUseCaseImpl() + + private lazy var channelExtendedValueRepository: ChannelExtendedValueRepositorMock! = ChannelExtendedValueRepositorMock() + + private lazy var channelValueRepository: ChannelValueRepositoryMock! = ChannelValueRepositoryMock() + + private lazy var channelRepository: ChannelRepositoryMock! = ChannelRepositoryMock() + + private lazy var groupRepository: GroupRepositoryMock! = GroupRepositoryMock() + + private lazy var electricityMeasurementItemRepository: ElectricityMeasurementItemRepositoryMock! = ElectricityMeasurementItemRepositoryMock() + + private lazy var impulseCounterMeasurementItemRepository: ImpulseCounterMeasurementItemRepositoryMock! = ImpulseCounterMeasurementItemRepositoryMock() + + private lazy var locationRepository: LocationRepositoryMock! = LocationRepositoryMock() + + private lazy var sceneRepository: SceneRepositoryMock! = SceneRepositoryMock() + + private lazy var temperatureMeasurementItemRepository: TemperatureMeasurementItemRepositoryMock! = TemperatureMeasurementItemRepositoryMock() + + private lazy var tempHumidityMeasurementItemRepository: TempHumidityMeasurementItemRepositoryMock! = TempHumidityMeasurementItemRepositoryMock() + + private lazy var userIconRepository: UserIconRepositoryMock! = UserIconRepositoryMock() + + private lazy var thermostatMeasurementItemRepository: ThermostatMeasurementItemRepositoryMock! = ThermostatMeasurementItemRepositoryMock() + + private lazy var generalPurposeMeterItemRepository: GeneralPurposeMeterItemRepositoryMock! = GeneralPurposeMeterItemRepositoryMock() + + private lazy var generalPurposeMeasurementItemRepository: GeneralPurposeMeasurementItemRepositoryMock! = GeneralPurposeMeasurementItemRepositoryMock() + + private lazy var channelConfigRepository: ChannelConfigRepositoryMock! = ChannelConfigRepositoryMock() + override func setUp() { DiContainer.shared.register(type: (any ChannelExtendedValueRepository).self, channelExtendedValueRepository!) DiContainer.shared.register(type: (any ChannelValueRepository).self, channelValueRepository!) @@ -84,11 +70,12 @@ final class DeleteAllProfileDataUseCaseTests: UseCaseTest { DiContainer.shared.register(type: (any ThermostatMeasurementItemRepository).self, thermostatMeasurementItemRepository!) DiContainer.shared.register(type: (any GeneralPurposeMeterItemRepository).self, generalPurposeMeterItemRepository!) DiContainer.shared.register(type: (any GeneralPurposeMeasurementItemRepository).self, generalPurposeMeasurementItemRepository!) + DiContainer.shared.register(type: (any ChannelConfigRepository).self, channelConfigRepository!) } - + override func tearDown() { useCase = nil - + channelExtendedValueRepository = nil channelValueRepository = nil channelRepository = nil @@ -103,10 +90,11 @@ final class DeleteAllProfileDataUseCaseTests: UseCaseTest { thermostatMeasurementItemRepository = nil generalPurposeMeterItemRepository = nil generalPurposeMeasurementItemRepository = nil - + channelConfigRepository = nil + super.tearDown() } - + func test() { // given let profile = AuthProfileItem(testContext: nil) @@ -124,10 +112,11 @@ final class DeleteAllProfileDataUseCaseTests: UseCaseTest { thermostatMeasurementItemRepository.deleteAllObservable = .just(()) generalPurposeMeterItemRepository.deleteAllForProfileReturns = .just(()) generalPurposeMeasurementItemRepository.deleteAllForProfileReturns = .just(()) - + channelConfigRepository.deleteAllForProfileReturns = .just(()) + // when useCase.invoke(profile: profile).subscribe(observer).disposed(by: disposeBag) - + // then XCTAssertEqual(observer.events.count, 2) XCTAssertEqual(channelExtendedValueRepository.deleteAllCounter, 1) @@ -144,6 +133,6 @@ final class DeleteAllProfileDataUseCaseTests: UseCaseTest { XCTAssertEqual(thermostatMeasurementItemRepository.deleteAllCounter, 1) XCTAssertEqual(generalPurposeMeterItemRepository.deleteAllForProfileParameters, [profile]) XCTAssertEqual(generalPurposeMeasurementItemRepository.deleteAllForProfileParameters, [profile]) - + XCTAssertEqual(channelConfigRepository.deleteAllForProfileParameters, [profile]) } } diff --git a/SUPLATests/Tests/UseCase/Profile/DeleteProfileUseCaseTests.swift b/SUPLATests/Tests/UseCase/Profile/DeleteProfileUseCaseTests.swift index fe960813f..4948a2cf3 100644 --- a/SUPLATests/Tests/UseCase/Profile/DeleteProfileUseCaseTests.swift +++ b/SUPLATests/Tests/UseCase/Profile/DeleteProfileUseCaseTests.swift @@ -18,6 +18,7 @@ @testable import SUPLA import XCTest +import RxSwift final class DeleteProfileUseCaseTests: UseCaseTest { @@ -37,10 +38,6 @@ final class DeleteProfileUseCaseTests: UseCaseTest { ActivateProfileUseCaseMock() }() - private lazy var suplaApp: SuplaAppWrapperMock! = { - SuplaAppWrapperMock() - }() - private lazy var runtimeConfig: RuntimeConfigMock! = { RuntimeConfigMock() }() @@ -49,6 +46,10 @@ final class DeleteProfileUseCaseTests: UseCaseTest { GlobalSettingsMock() }() + private lazy var disconnectUseCase: DisconnectUseCaseMock! = DisconnectUseCaseMock() + + private lazy var suplaAppStateHolder: SuplaAppStateHolderMock! = SuplaAppStateHolderMock() + private lazy var useCase: DeleteProfileUseCase! = { DeleteProfileUseCaseImpl() }() @@ -60,9 +61,10 @@ final class DeleteProfileUseCaseTests: UseCaseTest { DiContainer.shared.register(type: SingleCall.self, singleCall!) DiContainer.shared.register(type: DeleteAllProfileDataUseCase.self, deleteAllProfileDataUseCase!) DiContainer.shared.register(type: ActivateProfileUseCase.self, activateProfileUseCase!) - DiContainer.shared.register(type: SuplaAppWrapper.self, suplaApp!) DiContainer.shared.register(type: RuntimeConfig.self, runtimeConfig!) DiContainer.shared.register(type: GlobalSettings.self, settings!) + DiContainer.shared.register(type: DisconnectUseCase.self, disconnectUseCase!) + DiContainer.shared.register(type: SuplaAppStateHolder.self, suplaAppStateHolder!) } override func tearDown() { @@ -72,9 +74,10 @@ final class DeleteProfileUseCaseTests: UseCaseTest { singleCall = nil deleteAllProfileDataUseCase = nil activateProfileUseCase = nil - suplaApp = nil runtimeConfig = nil settings = nil + disconnectUseCase = nil + suplaAppStateHolder = nil useCase = nil } @@ -104,6 +107,7 @@ final class DeleteProfileUseCaseTests: UseCaseTest { profileRepository.queryItemByIdObservable = .just(profile) profileRepository.deleteObservable = .just(()) + profileRepository.saveObservable = .just(()) deleteAllProfileDataUseCase.returns = .just(()) // when @@ -131,7 +135,9 @@ final class DeleteProfileUseCaseTests: UseCaseTest { profileRepository.queryItemByIdObservable = .just(profile) profileRepository.allProfilesObservable = .just([profile]) profileRepository.deleteObservable = .just(()) + profileRepository.saveObservable = .just(()) deleteAllProfileDataUseCase.returns = .just(()) + disconnectUseCase.invokeReturns = .complete() // when useCase.invoke(profileId: profile.objectID) @@ -162,8 +168,10 @@ final class DeleteProfileUseCaseTests: UseCaseTest { profileRepository.queryItemByIdObservable = .just(profile) profileRepository.allProfilesObservable = .just([profile, otherProfile]) profileRepository.deleteObservable = .just(()) + profileRepository.saveObservable = .just(()) deleteAllProfileDataUseCase.returns = .just(()) - activateProfileUseCase.returns = .just(true) + activateProfileUseCase.returns = .complete() + disconnectUseCase.invokeReturns = .complete() // when useCase.invoke(profileId: profile.objectID) @@ -190,13 +198,17 @@ final class DeleteProfileUseCaseTests: UseCaseTest { profile.isActive = true profile.authInfo = AuthInfo.mock(email: "some@email.com") + var profileRemoved: Bool? = nil + var profileDataRemoved: Bool? = nil + let otherProfile = AuthProfileItem(testContext: nil) profileRepository.queryItemByIdObservable = .just(profile) profileRepository.allProfilesObservable = .just([profile, otherProfile]) - profileRepository.deleteObservable = .just(()) - deleteAllProfileDataUseCase.returns = .just(()) - activateProfileUseCase.returns = .just(false) + profileRepository.deleteObservable = .mocked { profileRemoved = true } + deleteAllProfileDataUseCase.returns = .mocked { profileDataRemoved = true } + activateProfileUseCase.returns = .error(DeleteProfileError.otherProfileNotActivated) + disconnectUseCase.invokeReturns = .complete() // when useCase.invoke(profileId: profile.objectID) @@ -210,7 +222,9 @@ final class DeleteProfileUseCaseTests: UseCaseTest { XCTAssertEqual(singleCall.registerPushTokenCalls, 0) XCTAssertEqual(profileRepository.deleteParameters, []) - XCTAssertEqual(deleteAllProfileDataUseCase.parameters, []) + XCTAssertEqual(deleteAllProfileDataUseCase.parameters, [profile]) + XCTAssertNil(profileRemoved) + XCTAssertNil(profileDataRemoved) XCTAssertEqual(runtimeConfig.activeProfileIdValues, []) XCTAssertEqual(settings.anyAccountRegisteredValues, []) XCTAssertTuples(activateProfileUseCase.parameters, [(otherProfile.objectID, true)]) @@ -221,10 +235,14 @@ final class DeleteProfileUseCaseTests: UseCaseTest { let profile = AuthProfileItem(testContext: nil) profile.authInfo = AuthInfo.mock() + var profileRemoved: Bool? = nil + var profileDataRemoved: Bool? = nil + profileRepository.queryItemByIdObservable = .just(profile) profileRepository.allProfilesObservable = .just([profile]) - profileRepository.deleteObservable = .just(()) - deleteAllProfileDataUseCase.returns = .just(()) + profileRepository.saveObservable = .just((())) + profileRepository.deleteObservable = .mocked { profileRemoved = true } + deleteAllProfileDataUseCase.returns = .mocked { profileDataRemoved = true } // when useCase.invoke(profileId: profile.objectID) @@ -240,5 +258,7 @@ final class DeleteProfileUseCaseTests: UseCaseTest { XCTAssertEqual(singleCall.registerPushTokenCalls, 0) XCTAssertEqual(profileRepository.deleteParameters, [profile]) XCTAssertEqual(deleteAllProfileDataUseCase.parameters, [profile]) + XCTAssertTrue(profileRemoved == true) + XCTAssertTrue(profileDataRemoved == true) } } diff --git a/SUPLATests/Tests/UseCase/Profile/SaveOrCreateProfileUseCaseTests.swift b/SUPLATests/Tests/UseCase/Profile/SaveOrCreateProfileUseCaseTests.swift index 820f26b5b..c7353d6bf 100644 --- a/SUPLATests/Tests/UseCase/Profile/SaveOrCreateProfileUseCaseTests.swift +++ b/SUPLATests/Tests/UseCase/Profile/SaveOrCreateProfileUseCaseTests.swift @@ -25,14 +25,12 @@ final class SaveOrCreateProfileUseCaseTests: UseCaseTest: XCTestCase { + lazy var schedulers: SuplaSchedulersMock! = SuplaSchedulersMock() + + lazy var disposeBag: DisposeBag! = DisposeBag() - private let item: ItemBundle - private let pages: [DetailPage] + lazy var observer: (Result) -> Void = { [weak self] in self?.result = $0 } - override var viewController: UIViewController { - _viewController - } + var result: Result? = nil - private lazy var _viewController: GpmDetailVC = { - let controller = GpmDetailVC(navigator: self, item: item, pages: pages) - controller.navigationCoordinator = self - return controller - }() - - init(item: ItemBundle, pages: [DetailPage]) { - self.item = item - self.pages = pages + override func setUp() { + DiContainer.shared.register(type: SuplaSchedulers.self, schedulers!) } -} + override func tearDown() { + schedulers = nil + disposeBag = nil + result = nil + } +} diff --git a/SUPLA/Features/DeviceCatalog/DeviceCatalogNavigationCoordinator.swift b/SUPLATests/Tools/Extensions/Observable+Test.swift similarity index 68% rename from SUPLA/Features/DeviceCatalog/DeviceCatalogNavigationCoordinator.swift rename to SUPLATests/Tools/Extensions/Observable+Test.swift index e637d9fdc..425b7f27a 100644 --- a/SUPLA/Features/DeviceCatalog/DeviceCatalogNavigationCoordinator.swift +++ b/SUPLATests/Tools/Extensions/Observable+Test.swift @@ -1,3 +1,4 @@ +// /* Copyright (C) AC SOFTWARE SP. Z O.O. @@ -15,13 +16,16 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ + -class DeviceCatalogNavigationCoordinator: BaseNavigationCoordinator { - override var viewController: UIViewController { _viewController } +import RxSwift - private lazy var _viewController: DeviceCatalogVC = .init(navigationCoordinator: self) - - func navigateToWeb(url: URL) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) +extension Observable { + static func mocked(callback: @escaping () -> V) -> Observable { + Observable.create { observable in + observable.onNext(callback()) + observable.onCompleted() + return Disposables.create() + } } } diff --git a/SuplaApp.h b/SuplaApp.h index 18465eda4..bcd61b083 100644 --- a/SuplaApp.h +++ b/SuplaApp.h @@ -28,20 +28,24 @@ @class SADatabase; @class SASettingsVC; -@class SAStatusVC; @class SACreateAccountVC; -@class MainNavigationCoordinator; @protocol ProfileManager; -@protocol NavigationCoordinator; + +@protocol SuplaAppApi + +-(void) cancelAllRestApiClientTasks; +-(BOOL) isClientRegistered; +-(BOOL) isClientWorking; +-(BOOL) isClientAuthorized; + +@end NS_ASSUME_NONNULL_BEGIN -@interface SAApp : NSObject +@interface SAApp : NSObject +(SAApp*)instance; -+(nullable id)currentNavigationCoordinator; -+(nullable MainNavigationCoordinator*)mainNavigationCoordinator; -+(BOOL) getClientGUID:(char[SUPLA_GUID_SIZE])guid DEPRECATED_ATTRIBUTE; -+(BOOL) getAuthKey:(char[SUPLA_AUTHKEY_SIZE])auth_key DEPRECATED_ATTRIBUTE; ++(BOOL) getClientGUID:(char[_Nullable SUPLA_GUID_SIZE])guid DEPRECATED_ATTRIBUTE; ++(BOOL) getAuthKey:(char[_Nullable SUPLA_AUTHKEY_SIZE])auth_key DEPRECATED_ATTRIBUTE; +(void) abstractMethodException:(NSString *)methodName; +(NSURL *)applicationDocumentsDirectory; +(void) setBrightnessPickerTypeToSlider:(BOOL)slider; @@ -64,6 +68,7 @@ NS_ASSUME_NONNULL_BEGIN -(SAOAuthToken*) registerRestApiClientTask:(SARestApiClientTask *)client; -(void) unregisterRestApiClientTask:(SARestApiClientTask *)task; -(void) cancelAllRestApiClientTasks; +-(bool) isClientWorking; @end extern NSString *kSADataChangedNotification; diff --git a/SuplaApp.m b/SuplaApp.m index 22b7fee4b..663c99632 100644 --- a/SuplaApp.m +++ b/SuplaApp.m @@ -24,7 +24,6 @@ #import "SASuperuserAuthorizationDialog.h" #import "NSNumber+SUPLA.h" #import "SUPLA-Swift.h" -#import "AppDelegate.h" #import "AuthProfileItem+CoreDataClass.h" static SAApp* _Globals = nil; @@ -87,38 +86,6 @@ -(id)init { _RestApiClientTasks = [[NSMutableArray alloc] init]; - [[NSNotificationCenter defaultCenter] - addObserver:self selector:@selector(onDisconnected) - name:kSADisconnectedNotification object:nil]; - - [[NSNotificationCenter defaultCenter] - addObserver:self selector:@selector(onConnecting) - name:kSAConnectingNotification object:nil]; - - [[NSNotificationCenter defaultCenter] - addObserver:self selector:@selector(onConnected) - name:kSAConnectedNotification object:nil]; - - [[NSNotificationCenter defaultCenter] - addObserver:self selector:@selector(onConnError:) - name:kSAConnErrorNotification object:nil]; - - [[NSNotificationCenter defaultCenter] - addObserver:self selector:@selector(onRegistering) - name:kSARegisteringNotification object:nil]; - - [[NSNotificationCenter defaultCenter] - addObserver:self selector:@selector(onRegistered:) - name:kSARegisteredNotification object:nil]; - - [[NSNotificationCenter defaultCenter] - addObserver:self selector:@selector(onRegisterError:) - name:kSARegisterErrorNotification object:nil]; - - [[NSNotificationCenter defaultCenter] - addObserver:self selector:@selector(onVersionError:) - name:kSAVersionErrorNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onOAuthTokenRequestResult:) name:kSAOAuthTokenRequestResult object:nil]; @@ -137,20 +104,6 @@ +(SAApp*)instance { return _Globals; } -+(id)currentNavigationCoordinator { - return [[((AppDelegate *)[UIApplication sharedApplication].delegate) navigation] - currentCoordinator]; -} - -+(MainNavigationCoordinator*)mainNavigationCoordinator { - id coordinator = [((AppDelegate *)[UIApplication sharedApplication].delegate) navigation]; - if([coordinator isKindOfClass:[MainNavigationCoordinator class]]) { - return (MainNavigationCoordinator*)coordinator; - } else { - return nil; - } -} - -(BOOL) getRandom:(char*)key size:(int)size forPrefKey:(NSString*)pref_key { @synchronized(self) { @@ -333,6 +286,29 @@ -(BOOL) isClientRegistered { } +-(BOOL) isClientWorking { + BOOL result = NO; + + @synchronized(self) { + if ( _SuplaClient != nil) { + result = ![_SuplaClient isCancelled]; + } + } + + return result; +} + +-(BOOL) isClientAuthorized { + BOOL result = NO; + @synchronized(self) { + if ( _SuplaClient != nil) { + result = [_SuplaClient isRegistered] && [_SuplaClient isSuperuserAuthorized]; + } + } + + return result; +} + +(SASuplaClient*) SuplaClient { return [[self instance] SuplaClientWithOneTimePassword:nil]; } @@ -374,8 +350,9 @@ +(void) SuplaClientTerminate { +(void) SuplaClientWaitForTerminate { NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow: 3]; + BOOL working = [[self instance] SuplaClientTerminate]; - while([[self instance] SuplaClientTerminate]) { + while(working && [[self instance] isClientWorking]) { @autoreleasepool { NSDate *cDate = [NSDate date]; if([cDate earlierDate: deadline] == deadline) @@ -429,107 +406,6 @@ -(NSString*)getMsgHostName { } } --(BOOL)canChangeView { - NSObject *nav = [SAApp currentNavigationCoordinator]; - if(nav == [SAApp mainNavigationCoordinator] || - ([nav isKindOfClass: [PresentationNavigationCoordinator class]] && - [nav.viewController isKindOfClass: [SAStatusVC class]])) - return YES; - else - return NO; -// return [self.UI addWizardIsVisible] != YES && [self.UI createAccountVCisVisible] != YES && ![self.UI settingsVCisVisible]; - -} - --(void)onDisconnected { - if ( ![self canChangeView] ) { - return; - } - - [[SAApp mainNavigationCoordinator] showStatusViewWithProgress: @-1]; -} - --(void)onConnecting { - if ( ![self canChangeView] ) { - return; - } - [[SAApp mainNavigationCoordinator] showStatusViewWithProgress:@0.25]; -} - --(void)onConnected { - if ( ![self canChangeView] ) { - return; - } - [[SAApp mainNavigationCoordinator] showStatusViewWithProgress:@0.5]; -} - --(void)onConnError:(NSNotification *)notification { - if ( ![self canChangeView] ) { - return; - } - - NSNumber *code = [NSNumber codeNotificationToNumber:notification]; - - if ( code && [code intValue] == SUPLA_RESULT_HOST_NOT_FOUND ) { - - [self SuplaClientTerminate]; - [[SAApp mainNavigationCoordinator] showStatusViewWithError:NSLocalizedString(@"Host not found. Make sure you are connected to the internet and that an account with the entered email address has been created.", nil) completion: nil]; - } -} - --(void)onRegistering { - if ( ![self canChangeView] ) { - return; - } - - [[SAApp mainNavigationCoordinator] showStatusViewWithProgress:@0.75]; -} - --(void)onRegistered:(NSNotification *)notification { - if ( ![self canChangeView] ) { - return; - } - - [[SAApp mainNavigationCoordinator] showStatusViewWithProgress:@1]; -} - --(void)onRegisterError:(NSNotification *)notification { - - if ( ![self canChangeView] ) { - return; - } - - [self SuplaClientTerminate]; - - NSNumber *code = [NSNumber codeNotificationToNumber:notification]; - [[SAApp mainNavigationCoordinator] showStatusViewWithError:[SASuplaClient codeToString:code] - completion: ^{ - int cint = [code intValue]; - - AuthProfileItem *profile = [SAApp.profileManager getCurrentProfile]; - if ((cint == SUPLA_RESULTCODE_REGISTRATION_DISABLED - || cint == SUPLA_RESULTCODE_ACCESSID_NOT_ASSIGNED) - && profile.authInfo.isAuthDataComplete - && profile.authInfo.emailAuth - && ![SASuperuserAuthorizationDialog.globalInstance isVisible]) { - [SASuperuserAuthorizationDialog.globalInstance authorizeWithDelegate:nil]; - } - }]; - -} - --(void)onVersionError:(NSNotification *)notification { - - if ( ![self canChangeView] ) { - return; - } - - [self SuplaClientTerminate]; - - [[SAApp mainNavigationCoordinator] showStatusViewWithError: NSLocalizedString(@"Incompatible server version", nil) - completion: nil]; -} - -(void)onOAuthTokenRequestResult:(NSNotification *)notification { SAOAuthToken *token = [SAOAuthToken notificationToToken:notification];