Merge pull request #121 from twofas/feature/TF-1571

[TF-1571] Apple Watch settings
This commit is contained in:
gmachnio 2024-07-30 19:32:26 +02:00 committed by GitHub
commit 26a22e75ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 419 additions and 26 deletions

View File

@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.137",
"green" : "0.110",
"red" : "0.929"
"blue" : "0x22",
"green" : "0x1C",
"red" : "0xEC"
}
},
"idiom" : "universal"

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.480",
"green" : "0.450",
"red" : "1.000"
"blue" : "0x7A",
"green" : "0x72",
"red" : "0xFF"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.140",
"green" : "0.110",
"red" : "0.744"
"blue" : "0x23",
"green" : "0x1C",
"red" : "0xBD"
}
},
"idiom" : "universal"

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.137",
"green" : "0.110",
"red" : "0.929"
"blue" : "0x22",
"green" : "0x1C",
"red" : "0xEC"
}
},
"idiom" : "universal"

View File

@ -8,11 +8,15 @@
/* Begin PBXBuildFile section */
8F00E46A2C23953B001F15AD /* SortTokensView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F00E4692C23953B001F15AD /* SortTokensView.swift */; };
8F872DE92C24EE2600160D14 /* SortTokensPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F872DE82C24EE2600160D14 /* SortTokensPresenter.swift */; };
8F872DEB2C24EE5500160D14 /* SortTokensInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F872DEA2C24EE5500160D14 /* SortTokensInteractor.swift */; };
8F43E7002C4C50DD0006D380 /* AppleWatchPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F43E6FF2C4C50DD0006D380 /* AppleWatchPresenter.swift */; };
8F43E7032C4C511A0006D380 /* AppleWatchFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F43E7022C4C511A0006D380 /* AppleWatchFlowController.swift */; };
8F594C422C3027160066562F /* AppleWatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F594C412C3027160066562F /* AppleWatchView.swift */; };
8F594C442C3054F90066562F /* AppleWatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F594C432C3054F90066562F /* AppleWatchViewController.swift */; };
8F5C0C6C2BFFAC8A00D73ADE /* TwoFAS.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 8F5C0C6A2BFFAC8900D73ADE /* TwoFAS.xcdatamodeld */; };
8F5C0C6F2BFFACA900D73ADE /* Sync.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 8F5C0C6D2BFFACA900D73ADE /* Sync.xcdatamodeld */; };
8F7952532C01D1940053F776 /* KeyboardButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F7952522C01D1940053F776 /* KeyboardButtonStyle.swift */; };
8F872DE92C24EE2600160D14 /* SortTokensPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F872DE82C24EE2600160D14 /* SortTokensPresenter.swift */; };
8F872DEB2C24EE5500160D14 /* SortTokensInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F872DEA2C24EE5500160D14 /* SortTokensInteractor.swift */; };
C200E49E1FB3911B00D7C748 /* Storage.h in Headers */ = {isa = PBXBuildFile; fileRef = C200E49C1FB3911B00D7C748 /* Storage.h */; settings = {ATTRIBUTES = (Public, ); }; };
C200E4A11FB3911B00D7C748 /* Storage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C200E49A1FB3911B00D7C748 /* Storage.framework */; };
C200E4A21FB3911B00D7C748 /* Storage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C200E49A1FB3911B00D7C748 /* Storage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@ -1880,12 +1884,15 @@
/* Begin PBXFileReference section */
8F00E4692C23953B001F15AD /* SortTokensView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortTokensView.swift; sourceTree = "<group>"; };
8F872DE82C24EE2600160D14 /* SortTokensPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortTokensPresenter.swift; sourceTree = "<group>"; };
8F872DEA2C24EE5500160D14 /* SortTokensInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortTokensInteractor.swift; sourceTree = "<group>"; };
8F43E6FF2C4C50DD0006D380 /* AppleWatchPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleWatchPresenter.swift; sourceTree = "<group>"; };
8F43E7022C4C511A0006D380 /* AppleWatchFlowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleWatchFlowController.swift; sourceTree = "<group>"; };
8F594C412C3027160066562F /* AppleWatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleWatchView.swift; sourceTree = "<group>"; };
8F594C432C3054F90066562F /* AppleWatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleWatchViewController.swift; sourceTree = "<group>"; };
8F5C0C6B2BFFAC8900D73ADE /* TwoFAS.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TwoFAS.xcdatamodel; sourceTree = "<group>"; };
8F5C0C6E2BFFACA900D73ADE /* Sync.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Sync.xcdatamodel; sourceTree = "<group>"; };
8F7952522C01D1940053F776 /* KeyboardButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardButtonStyle.swift; sourceTree = "<group>"; };
8FD3F0852C37324B0042092E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/LaunchScreen.strings; sourceTree = "<group>"; };
8F872DE82C24EE2600160D14 /* SortTokensPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortTokensPresenter.swift; sourceTree = "<group>"; };
8F872DEA2C24EE5500160D14 /* SortTokensInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortTokensInteractor.swift; sourceTree = "<group>"; };
8FD3F0862C37324B0042092E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/TwoFASWidget.strings; sourceTree = "<group>"; };
8FD3F0872C37324B0042092E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = "<group>"; };
8FD3F0882C37324B0042092E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@ -3326,8 +3333,43 @@
8F872DEA2C24EE5500160D14 /* SortTokensInteractor.swift */,
);
path = SortTokens;
sourceTree = "<group>";
};
sourceTree = "<group>";
};
8F43E6FD2C4C50A60006D380 /* View */ = {
isa = PBXGroup;
children = (
8F594C412C3027160066562F /* AppleWatchView.swift */,
8F594C432C3054F90066562F /* AppleWatchViewController.swift */,
);
path = View;
sourceTree = "<group>";
};
8F43E6FE2C4C50C70006D380 /* Presenter */ = {
isa = PBXGroup;
children = (
8F43E6FF2C4C50DD0006D380 /* AppleWatchPresenter.swift */,
);
path = Presenter;
sourceTree = "<group>";
};
8F43E7012C4C51060006D380 /* Flow */ = {
isa = PBXGroup;
children = (
8F43E7022C4C511A0006D380 /* AppleWatchFlowController.swift */,
);
path = Flow;
sourceTree = "<group>";
};
8F594C402C3024E60066562F /* AppleWatch */ = {
isa = PBXGroup;
children = (
8F43E7012C4C51060006D380 /* Flow */,
8F43E6FE2C4C50C70006D380 /* Presenter */,
8F43E6FD2C4C50A60006D380 /* View */,
);
path = AppleWatch;
sourceTree = "<group>";
};
8F7952502C01D1770053F776 /* CommonSwiftUI */ = {
isa = PBXGroup;
children = (
@ -3735,6 +3777,7 @@
C21111E1274AB8C100CA7C78 /* Settings */ = {
isa = PBXGroup;
children = (
8F594C402C3024E60066562F /* AppleWatch */,
C2B1208529D767150020281E /* Appearance */,
C2CC0A4129D08D7700677A7B /* ExternalImportInstructions */,
C2CC0A3429D056CF00677A7B /* ExternalImport */,
@ -9811,6 +9854,7 @@
C21B1B312A882A920021CAF1 /* AddingServiceTOTPTimerView.swift in Sources */,
C27BE517275D6ACC0090CA12 /* BackupMenuModuleInteractor.swift in Sources */,
C2B39E5B29F5D0AA00EC31F6 /* TokensServiceName.swift in Sources */,
8F594C422C3027160066562F /* AppleWatchView.swift in Sources */,
C27D543F275D393D001E9ABF /* BackupMenuPresenter.swift in Sources */,
C25C91CF2B08E84800E58729 /* AddingServiceManuallyNavigationFlowController.swift in Sources */,
C23FD51228146E2E00E4E9C5 /* ComposeServiceWebExtensionPresenter.swift in Sources */,
@ -9833,6 +9877,7 @@
C2035AED245DCAAD00C482D2 /* MainContainerBottomNavigationGenerator.swift in Sources */,
C2B2ED2427C44BF300332255 /* AdvancedAlertViewController.swift in Sources */,
C25B221927C199B80052AAC4 /* ComposeServicePrivateKeyCell.swift in Sources */,
8F43E7002C4C50DD0006D380 /* AppleWatchPresenter.swift in Sources */,
C2ADD6122752DC1E0010F5E7 /* SettingsMenuFlowController.swift in Sources */,
C2C1DDDF27CAA7F2002C7867 /* ComposeServiceAdvancedEditFlowController.swift in Sources */,
C263F45A29900DED009B0837 /* MainMenuModels.swift in Sources */,
@ -10039,6 +10084,7 @@
C20524C32807620500B2B20C /* NavigationBarHiddenHostingController.swift in Sources */,
C2FDEE57279C8FF8002CAB9E /* SwiftUIButtons.swift in Sources */,
C22E6FA121E2666B00A2E66B /* CircularShape.swift in Sources */,
8F594C442C3054F90066562F /* AppleWatchViewController.swift in Sources */,
C2F7FE092A8ABA25004B2C95 /* AddingServiceManuallyView.swift in Sources */,
C2AC221327661BA300271402 /* ExporterPasswordProtectionViewController.swift in Sources */,
C2B2ED1E27C4349100332255 /* AdvancedAlert.swift in Sources */,
@ -10082,6 +10128,7 @@
C296D4D42A8979BF005F05E2 /* BrowserExtensionPairingNavigationFlowController.swift in Sources */,
C2C66F951FE068CC00AD5A1C /* TokensViewController.swift in Sources */,
C2EBDFFC2AC44750008FD744 /* GuideMenuViewController.swift in Sources */,
8F43E7032C4C511A0006D380 /* AppleWatchFlowController.swift in Sources */,
C240487C27656DB40076376E /* ImporterFileErrorFlowController.swift in Sources */,
C2A74FD72610D3A8007E5AB3 /* TintColor.swift in Sources */,
C2CC0A3A29D0580B00677A7B /* ExternalImportViewController.swift in Sources */,
@ -11198,7 +11245,6 @@
C2BBD24C2B813162009A91FB /* uk */,
C2BBD24D2B813162009A91FB /* de */,
C2BBD24E2B813162009A91FB /* pl */,
8FD3F08A2C3732E20042092E /* el */,
);
name = Localizable.strings;
sourceTree = "<group>";

View File

@ -148,6 +148,7 @@ enum Theme {
static let background = ThemeColor.background
static let backgroundLight = ThemeColor.backgroundLight
static let placeholder = ThemeColor.divider
static let tertiary = ThemeColor.tertiary
}
}
@ -211,6 +212,8 @@ enum Theme {
static let warning = UIFont.systemFont(ofSize: 13, weight: .medium)
static let iconLabel = UIFont.systemFont(ofSize: 13, weight: .bold)
static let iconLabelSmall = UIFont.systemFont(ofSize: 12, weight: .bold)
static let iconLabelLarge = UIFont.systemFont(ofSize: 15, weight: .bold)
static let iconLabelExtraLarge = UIFont.systemFont(ofSize: 15, weight: .bold)
static let sectionHeader = UIFont.systemFont(ofSize: 13, weight: .regular)
static let iconLabelInputTitle = UIFont.systemFont(ofSize: 22, weight: .light)
static let tabBar = UIFont.systemFont(ofSize: 10, weight: .semibold)
@ -258,7 +261,9 @@ enum Theme {
static let mediumMargin: CGFloat = (3 * ThemeMetrics.margin) / 2
/// 16
static let doubleMargin: CGFloat = 2 * ThemeMetrics.margin
/// 32
static let quadrupleMargin: CGFloat = 4 * ThemeMetrics.margin
static let lineWidth: CGFloat = ThemeMetrics.lineWidth
static let separatorHeight: CGFloat = 0.5
/// 6

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"filename" : "appleWatch.pdf",
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "appleWatchDark.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "SettingsWatch.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -29,6 +29,7 @@ internal enum Asset {
internal static let imageIcon = ImageAsset(name: "imageIcon")
internal static let keybordIcon = ImageAsset(name: "keybordIcon")
internal static let alertIcon = ImageAsset(name: "AlertIcon")
internal static let appleWatch = ImageAsset(name: "AppleWatch")
internal static let authRequestQuestion = ImageAsset(name: "AuthRequestQuestion")
internal static let deleteSettingsIcon = ImageAsset(name: "DeleteSettingsIcon")
internal static let backupDeleted = ImageAsset(name: "backupDeleted")
@ -156,6 +157,7 @@ internal enum Asset {
internal static let settingsTouchID = ImageAsset(name: "SettingsTouchID")
internal static let settingsTrash = ImageAsset(name: "SettingsTrash")
internal static let settingsValut = ImageAsset(name: "SettingsValut")
internal static let settingsWatch = ImageAsset(name: "SettingsWatch")
internal static let settingsWidget = ImageAsset(name: "SettingsWidget")
internal static let settingsWriteReview = ImageAsset(name: "SettingsWriteReview")
internal static let trashEmptyIcon = ImageAsset(name: "TrashEmptyIcon")

View File

@ -289,6 +289,18 @@ internal enum T {
/// Active search
internal static let toggleActiveSearch = T.tr("Localizable", "appearance__toggle_active_search", fallback: "Active search")
}
internal enum AppleWatch {
/// Install 2FAS Auth via Watch app
internal static let installationFirstStep = T.tr("Localizable", "appleWatch__installation_first_step", fallback: "Install 2FAS Auth via Watch app")
/// Open Watch app
internal static let installationFirstStepLink = T.tr("Localizable", "appleWatch__installation_first_step_link", fallback: "Open Watch app")
/// 2FAS Apple Watch app installation
internal static let installationInfoTitle = T.tr("Localizable", "appleWatch__installation_info_title", fallback: "2FAS Apple Watch app installation")
/// Ensure your iCloud Sync is enabled
internal static let installationSecondStep = T.tr("Localizable", "appleWatch__installation_second_step", fallback: "Ensure your iCloud Sync is enabled")
/// Go to 2FAS Backup settings
internal static let installationSecondStepLink = T.tr("Localizable", "appleWatch__installation_second_step_link", fallback: "Go to 2FAS Backup settings")
}
internal enum Backup {
/// 2FAS Backup
internal static let _2fasBackup = T.tr("Localizable", "backup__2fas_backup", fallback: "2FAS Backup")
@ -378,9 +390,9 @@ internal enum T {
internal static let incorrectCharacterError = T.tr("Localizable", "backup__incorrect_character_error", fallback: "Incorrect character. Use only letter A-Z, a-z, digits and special characters: -_/!#$%&+*~@?=^.,'(){}[]:;<>|")
/// Incorrect Password
internal static let incorrectPassword = T.tr("Localizable", "backup__incorrect_password", fallback: "Incorrect Password")
/// Couldn't backup tokens because "%@" secret contains illegal characters. Remove it from list and try again
/// Couldn't backup tokens because "%@" secret contains invalid characters. Remove it from list and try again
internal static func incorrectSecret(_ p1: Any) -> String {
return T.tr("Localizable", "backup__incorrect_secret", String(describing: p1), fallback: "Couldn't backup tokens because \"%@\" secret contains illegal characters. Remove it from list and try again")
return T.tr("Localizable", "backup__incorrect_secret", String(describing: p1), fallback: "Couldn't backup tokens because \"%@\" secret contains invalid characters. Remove it from list and try again")
}
/// Local file
internal static let localFileTitle = T.tr("Localizable", "backup__local_file_title", fallback: "Local file")
@ -1020,6 +1032,8 @@ internal enum T {
internal static let appSecurity = T.tr("Localizable", "settings__app_security", fallback: "App security")
/// Appearance
internal static let appearance = T.tr("Localizable", "settings__appearance", fallback: "Appearance")
/// Apple Watch
internal static let appleWatch = T.tr("Localizable", "settings__apple_watch", fallback: "Apple Watch")
/// Backup and Synchronization
internal static let backupAndSynchronization = T.tr("Localizable", "settings__backup_and_synchronization", fallback: "Backup and Synchronization")
/// Biometric Authentication

View File

@ -0,0 +1,60 @@
//
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
// Copyright © 2024 Two Factor Authentication Service, Inc.
// Contributed by Grzegorz Machnio. All rights reserved.
//
// 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 3 of the License, or
// 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, see <https://www.gnu.org/licenses/>
//
import UIKit
protocol AppleWatchFlowControllerParent: AnyObject {
func toBackup()
}
protocol AppleWatchFlowControlling: AnyObject {
func toSystemWatchApp()
func toBackup()
}
final class AppleWatchFlowController: FlowController {
private weak var parent: AppleWatchFlowControllerParent?
private weak var navigationController: UINavigationController?
static func push(
in navigationController: UINavigationController,
parent: AppleWatchFlowControllerParent
) {
let viewController = AppleWatchViewController()
let flowController = AppleWatchFlowController(viewController: viewController)
flowController.parent = parent
flowController.navigationController = navigationController
let presenter = AppleWatchPresenter(flowController: flowController)
viewController.presenter = presenter
navigationController.pushRootViewController(viewController, animated: true)
}
}
extension AppleWatchFlowController: AppleWatchFlowControlling {
func toSystemWatchApp() {
if let url = URL(string: "itms-watchs://") {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
func toBackup() {
parent?.toBackup()
}
}

View File

@ -0,0 +1,49 @@
//
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
// Copyright © 2024 Two Factor Authentication Service, Inc.
// Contributed by Grzegorz Machnio. All rights reserved.
//
// 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 3 of the License, or
// 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, see <https://www.gnu.org/licenses/>
//
import Foundation
protocol AppleWatchPresenting {
var appleWatchInstallationSteps: [AppleWatchInstallationStep] { get }
func handleInstallationStep(number: Int)
}
final class AppleWatchPresenter: AppleWatchPresenting {
let appleWatchInstallationSteps: [AppleWatchInstallationStep] = [
.init(description: T.AppleWatch.installationFirstStep,
actionTitle: T.AppleWatch.installationFirstStepLink),
.init(description: T.AppleWatch.installationSecondStep,
actionTitle: T.AppleWatch.installationSecondStepLink)
]
private let flowController: AppleWatchFlowControlling
init(flowController: AppleWatchFlowControlling) {
self.flowController = flowController
}
func handleInstallationStep(number: Int) {
if number == 1 {
flowController.toSystemWatchApp()
} else if number == 2 {
flowController.toBackup()
}
}
}

View File

@ -0,0 +1,114 @@
//
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
// Copyright © 2024 Two Factor Authentication Service, Inc.
// Contributed by Grzegorz Machnio. All rights reserved.
//
// 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 3 of the License, or
// 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, see <https://www.gnu.org/licenses/>
//
import SwiftUI
import Common
struct AppleWatchInstallationStep: Hashable, Identifiable {
var id = UUID()
let description: String
let actionTitle: String
}
struct AppleWatchView<Presenter: AppleWatchPresenting>: View {
private let spacing: CGFloat = Theme.Metrics.doubleSpacing
private let presenter: Presenter
init(presenter: Presenter) {
self.presenter = presenter
}
var body: some View {
VStack(spacing: spacing) {
Image(uiImage: Asset.appleWatch.image)
.renderingMode(.original)
.padding(.top, Theme.Metrics.standardSpacing)
Text(T.AppleWatch.installationInfoTitle)
.font(Font(Theme.Fonts.Text.title))
.multilineTextAlignment(.center)
.minimumScaleFactor(0.7)
VStack(alignment: .leading) {
ForEach(Array(presenter.appleWatchInstallationSteps.enumerated()), id: \.element) { index, step in
let stepNumber = index + 1
stepView(
for: step,
stepNumber: stepNumber,
isDividerVisible: stepNumber != presenter.appleWatchInstallationSteps.count
)
}
}
.padding(.horizontal, Theme.Metrics.quadrupleMargin)
}
}
private func stepView(
for step: AppleWatchInstallationStep,
stepNumber: Int,
isDividerVisible: Bool
) -> some View {
HStack(alignment: .top) {
Text("\(stepNumber)")
.font(Font(Theme.Fonts.iconLabelLarge))
.frame(width: 28, height: 28)
.background(Color(Theme.Colors.Fill.tertiary))
.clipShape(Circle())
VStack(alignment: .leading, spacing: Theme.Metrics.halfSpacing) {
Text(step.description)
.font(Font(Theme.Fonts.Text.content))
Button {
presenter.handleInstallationStep(number: stepNumber)
} label: {
HStack(spacing: Theme.Metrics.halfSpacing) {
Text(step.actionTitle)
.font(Font(Theme.Fonts.Text.boldContent))
Image(systemName: "arrow.up.right")
.font(Font(Theme.Fonts.iconLabelExtraLarge))
}
.foregroundColor(Color(Theme.Colors.Text.theme))
}
if isDividerVisible {
Divider()
.frame(height: Theme.Metrics.separatorHeight)
.padding(.vertical, Theme.Metrics.standardSpacing)
}
}
}
}
}
#Preview {
final class AppleWatchPresenterMock: AppleWatchPresenting {
let appleWatchInstallationSteps: [AppleWatchInstallationStep] = [
.init(description: T.AppleWatch.installationFirstStep,
actionTitle: T.AppleWatch.installationFirstStepLink),
.init(description: T.AppleWatch.installationSecondStep,
actionTitle: T.AppleWatch.installationSecondStepLink)
]
func handleInstallationStep(number: Int) {}
}
return AppleWatchView(presenter: AppleWatchPresenterMock())
}

View File

@ -0,0 +1,38 @@
//
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
// Copyright © 2024 Two Factor Authentication Service, Inc.
// Contributed by Grzegorz Machnio. All rights reserved.
//
// 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 3 of the License, or
// 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, see <https://www.gnu.org/licenses/>
//
import UIKit
import SwiftUI
final class AppleWatchViewController: UIViewController {
var presenter: AppleWatchPresenter!
override func viewDidLoad() {
title = T.Settings.appleWatch
let appleWatchView = AppleWatchView(presenter: presenter)
let hostingController = UIHostingController(rootView: appleWatchView)
hostingController.willMove(toParent: self)
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.view.pinToParent()
hostingController.didMove(toParent: self)
}
}

View File

@ -45,6 +45,7 @@ protocol SettingsMenuFlowControllerParent: AnyObject {
func toUpdateCurrentPosition(_ viewPath: ViewPath.Settings?)
func toExternalImport()
func toAppearance()
func toAppleWatch()
}
protocol SettingsMenuFlowControlling: AnyObject {
@ -60,6 +61,7 @@ protocol SettingsMenuFlowControlling: AnyObject {
func toUpdateCurrentPosition(_ viewPath: ViewPath.Settings?)
func toExternalImport()
func toAppearance()
func toAppleWatch()
}
final class SettingsMenuFlowController: FlowController {
@ -109,6 +111,7 @@ extension SettingsMenuFlowController: SettingsMenuFlowControlling {
func toUpdateCurrentPosition(_ viewPath: ViewPath.Settings?) { parent?.toUpdateCurrentPosition(viewPath) }
func toExternalImport() { parent?.toExternalImport() }
func toAppearance() { parent?.toAppearance() }
func toAppleWatch() { parent?.toAppleWatch() }
}
extension SettingsMenuFlowController: SettingsMenuFlowControllerChild {

View File

@ -78,6 +78,7 @@ enum SettingsNavigationModule: Hashable {
case donate
case externalImport
case appearance
case appleWatch
}
enum SettingsNavigationToggle: Hashable {

View File

@ -64,7 +64,7 @@ extension SettingsMenuPresenter {
return T.Commons.off
}()
let browerExtension = SettingsMenuSection(
title: T.Browser.browserExtension,
title: T.Settings.advanced,
cells: [
.init(
icon: Asset.settingsBrowserExtension.image,
@ -72,6 +72,12 @@ extension SettingsMenuPresenter {
info: browerExtensionDescription,
accessory: .arrow,
action: .navigation(navigatesTo: .browserExtension)
),
.init(
icon: Asset.settingsWatch.image,
title: T.Settings.appleWatch,
accessory: .arrow,
action: .navigation(navigatesTo: .appleWatch)
)
]
)

View File

@ -201,6 +201,8 @@ private extension SettingsMenuPresenter {
flowController.toExternalImport()
case .appearance:
flowController.toAppearance()
case .appleWatch:
flowController.toAppleWatch()
}
}

View File

@ -214,6 +214,10 @@ extension SettingsFlowController: SettingsMenuFlowControllerParent {
AppearanceFlowController.showAsRoot(in: viewController.contentNavi, parent: self)
}
}
func toAppleWatch() {
AppleWatchFlowController.push(in: viewController.navigationNavi, parent: self)
}
}
extension SettingsFlowController: BackupMenuFlowControllerParent {
func showFAQ() {
@ -239,3 +243,4 @@ extension SettingsFlowController: BrowserExtensionMainFlowControllerParent {}
extension SettingsFlowController: AboutFlowControllerParent {}
extension SettingsFlowController: ExternalImportFlowControllerParent {}
extension SettingsFlowController: AppearanceFlowControllerParent {}
extension SettingsFlowController: AppleWatchFlowControllerParent {}

View File

@ -289,6 +289,18 @@ internal enum T {
/// Active search
internal static let toggleActiveSearch = T.tr("Localizable", "appearance__toggle_active_search", fallback: "Active search")
}
internal enum AppleWatch {
/// Install 2FAS Auth via Watch app
internal static let installationFirstStep = T.tr("Localizable", "appleWatch__installation_first_step", fallback: "Install 2FAS Auth via Watch app")
/// Open Watch app
internal static let installationFirstStepLink = T.tr("Localizable", "appleWatch__installation_first_step_link", fallback: "Open Watch app")
/// 2FAS Apple Watch app installation
internal static let installationInfoTitle = T.tr("Localizable", "appleWatch__installation_info_title", fallback: "2FAS Apple Watch app installation")
/// Ensure your iCloud Sync is enabled
internal static let installationSecondStep = T.tr("Localizable", "appleWatch__installation_second_step", fallback: "Ensure your iCloud Sync is enabled")
/// Go to 2FAS Backup settings
internal static let installationSecondStepLink = T.tr("Localizable", "appleWatch__installation_second_step_link", fallback: "Go to 2FAS Backup settings")
}
internal enum Backup {
/// 2FAS Backup
internal static let _2fasBackup = T.tr("Localizable", "backup__2fas_backup", fallback: "2FAS Backup")
@ -378,9 +390,9 @@ internal enum T {
internal static let incorrectCharacterError = T.tr("Localizable", "backup__incorrect_character_error", fallback: "Incorrect character. Use only letter A-Z, a-z, digits and special characters: -_/!#$%&+*~@?=^.,'(){}[]:;<>|")
/// Incorrect Password
internal static let incorrectPassword = T.tr("Localizable", "backup__incorrect_password", fallback: "Incorrect Password")
/// Couldn't backup tokens because "%@" secret contains illegal characters. Remove it from list and try again
/// Couldn't backup tokens because "%@" secret contains invalid characters. Remove it from list and try again
internal static func incorrectSecret(_ p1: Any) -> String {
return T.tr("Localizable", "backup__incorrect_secret", String(describing: p1), fallback: "Couldn't backup tokens because \"%@\" secret contains illegal characters. Remove it from list and try again")
return T.tr("Localizable", "backup__incorrect_secret", String(describing: p1), fallback: "Couldn't backup tokens because \"%@\" secret contains invalid characters. Remove it from list and try again")
}
/// Local file
internal static let localFileTitle = T.tr("Localizable", "backup__local_file_title", fallback: "Local file")
@ -1020,6 +1032,8 @@ internal enum T {
internal static let appSecurity = T.tr("Localizable", "settings__app_security", fallback: "App security")
/// Appearance
internal static let appearance = T.tr("Localizable", "settings__appearance", fallback: "Appearance")
/// Apple Watch
internal static let appleWatch = T.tr("Localizable", "settings__apple_watch", fallback: "Apple Watch")
/// Backup and Synchronization
internal static let backupAndSynchronization = T.tr("Localizable", "settings__backup_and_synchronization", fallback: "Backup and Synchronization")
/// Biometric Authentication