Merge tag 'vv5.3.5' into develop

v5.3.5
This commit is contained in:
Zbigniew Cisiński 2024-03-26 18:29:15 +01:00
commit d223770737
121 changed files with 2018 additions and 181 deletions

View File

@ -81,7 +81,9 @@ public final class Camera {
extension Camera: CameraControllerDelegate {
func cameraDidInitialize() {}
func cameraFailedToInitilize(with error: CameraController.CameraError) {}
func cameraFailedToInitilize(with error: CameraController.CameraError) {
Log("Camera - can't start: \(error)", module: .camera)
}
func cameraStartedPreview() {
delegate?.didStartScanning()
}

View File

@ -22,6 +22,9 @@ import Foundation
public extension Notification.Name {
static let servicesWereUpdated = Notification.Name("servicesWereUpdatedNotification")
static let sectionsWereUpdated = Notification.Name("sectionsWereUpdatedNotification")
static let syncCompletedSuccessfuly = Notification.Name("syncCompletedSuccessfuly")
static let clearSyncCompletedSuccessfuly = Notification.Name("clearSyncCompletedSuccessfuly")
static let localNotificationsHandled = Notification.Name("localNotificationsHandled")
}
public extension Notification {

View File

@ -25,6 +25,11 @@ public struct ListNewsEntry: Hashable {
case news
case features
case youtube
case tips
}
public enum InternalLink {
case backup
}
public let newsID: String
@ -34,6 +39,8 @@ public struct ListNewsEntry: Hashable {
public let publishedAt: Date
public let createdAt: Date?
public let wasRead: Bool
public let internalLink: InternalLink?
public let localNotificationType: String?
public init(
newsID: String,
@ -42,7 +49,9 @@ public struct ListNewsEntry: Hashable {
message: String?,
publishedAt: Date,
createdAt: Date?,
wasRead: Bool
wasRead: Bool,
internalLink: InternalLink?,
localNotificationType: String? = nil
) {
self.newsID = newsID
self.icon = icon
@ -51,5 +60,7 @@ public struct ListNewsEntry: Hashable {
self.publishedAt = publishedAt
self.createdAt = createdAt
self.wasRead = wasRead
self.internalLink = internalLink
self.localNotificationType = localNotificationType
}
}

Binary file not shown.

View File

@ -32,6 +32,7 @@ public enum AppEvent {
case missingIssuer(String)
case supportedCodeAdded(String)
case articleRead(String)
case localNotificationRead(String)
case codeDetailsTypeAdded(String)
case codeDetailsAlgorithmChosen(String)
case codeDetailsRefreshTimeChosen(String)
@ -111,6 +112,7 @@ private extension AppEvent {
case .orderIconAsCompany: return "request_icon_as_company_click"
case .orderIconDiscord: return "request_icon_discord_click"
case .orderIconShare: return "request_icon_share_click"
case .localNotificationRead: return "local_notification_read"
}
}
@ -126,6 +128,7 @@ private extension AppEvent {
case .codeDetailsRefreshTimeChosen(let string): return [AppEventController.KeyValue: string]
case .codeDetailsNumberOfDigitsChosen(let string): return [AppEventController.KeyValue: string]
case .codeDetailsInitialCounterChosen(let string): return [AppEventController.KeyValue: string]
case .localNotificationRead(let string): return [AppEventController.KeyValue: string]
default: return nil
}
}

View File

@ -42,6 +42,10 @@ public protocol CloudBackupStateInteracting: AnyObject {
func clearBackup()
func synchronizeBackup()
var successSyncDate: Date? { get }
func saveSuccessSyncDate()
func clearSaveSuccessSync()
}
/// Use one instance per use case
@ -80,6 +84,10 @@ extension CloudBackupStateInteractor: CloudBackupStateInteracting {
var isBackupEnabled: Bool { isEnabled }
var isBackupAvailable: Bool { isAvailable }
var successSyncDate: Date? {
mainRepository.successSyncDate
}
func startMonitoring() {
Log("CloudBackupStateInteractor - start monitoring, listenerID: \(listenerID)", module: .interactor)
saveStates()
@ -138,6 +146,16 @@ extension CloudBackupStateInteractor: CloudBackupStateInteracting {
Log("CloudBackupStateInteractor - synchronizeBackup", module: .interactor)
mainRepository.synchronizeBackup()
}
func saveSuccessSyncDate() {
Log("CloudBackupStateInteractor - saveSuccessSync", module: .interactor)
mainRepository.saveSuccessSyncDate(Date())
}
func clearSaveSuccessSync() {
Log("CloudBackupStateInteractor - clearSavesuccessSync", module: .interactor)
mainRepository.saveSuccessSyncDate(nil)
}
}
private extension CloudBackupStateInteractor {

View File

@ -246,4 +246,28 @@ public final class InteractorFactory {
public func appStateInteractor() -> AppStateInteracting {
AppStateInteractor(mainRepository: MainRepositoryImpl.shared)
}
public func mdmInteractor() -> MDMInteracting {
MDMInteractor(
mainRepository: MainRepositoryImpl.shared,
pairingInteractor: pairingWebExtensionInteractor(),
cloudBackupStateInteractor: cloudBackupStateInteractor(listenerID: "MDMInteractor")
)
}
public func localNotificationStateInteractor() -> LocalNotificationStateInteracting {
LocalNotificationStateInteractor(
mainRepository: MainRepositoryImpl.shared,
serviceListingInteractor: serviceListingInteractor(),
cloudBackup: cloudBackupStateInteractor(listenerID: "localNotificationStateInteractor"),
pairingDeviceInteractor: pairingWebExtensionInteractor(),
mdmInteractor: mdmInteractor()
)
}
public func localNotificationFetchInteractor() -> LocalNotificationFetchInteracting {
LocalNotificationFetchInteractor(
mainRepository: MainRepositoryImpl.shared
)
}
}

View File

@ -75,7 +75,7 @@ extension ListNewsNetworkInteractor: ListNewsNetworkInteracting {
private extension ListNewsNetworkInteractor {
func parsedList(_ list: [ListNews.NewsEntry]) -> [ListNewsEntry] {
list.compactMap { entry in
list.compactMap { entry -> ListNewsEntry? in
guard let icon = ListNewsEntry.Icon(rawValue: entry.icon),
let publishedAt = dateFormatter.date(from: entry.publishedAt)
else { return nil }
@ -92,7 +92,8 @@ private extension ListNewsNetworkInteractor {
guard let createdAt = entry.createdAt else { return nil }
return dateFormatter.date(from: createdAt)
}(),
wasRead: false
wasRead: false,
internalLink: nil
)
}
.sorted(by: { $0.publishedAt > $1.publishedAt })

View File

@ -0,0 +1,135 @@
//
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
// Copyright © 2024 Two Factor Authentication Service, Inc.
// Contributed by Zbigniew Cisiński. 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
import Common
public struct LocalNotification: Hashable {
public enum NotificationKind: String, Decodable, Hashable {
case tipsNTricks
case backup
case browserExtension
case donation
static func kindFromCycle(_ cycle: Int) -> NotificationKind {
switch cycle {
case -1: .tipsNTricks
case 0: .backup
case 1: .browserExtension
default: .donation
}
}
}
public let id: String
public let kind: NotificationKind
public let publishedAt: Date
public let wasRead: Bool
}
public protocol LocalNotificationFetchInteracting: AnyObject {
func getNotification(completion: @escaping (LocalNotification?) -> Void)
func markNotificationAsRead()
}
final class LocalNotificationFetchInteractor {
private let mainRepository: MainRepository
private let notificationCenter = NotificationCenter.default
private var fetched = false
private var notificationCallback: ((LocalNotification?) -> Void)?
init(mainRepository: MainRepository) {
self.mainRepository = mainRepository
notificationCenter.addObserver(
self,
selector: #selector(notificationsHandled),
name: .localNotificationsHandled,
object: nil
)
}
}
extension LocalNotificationFetchInteractor: LocalNotificationFetchInteracting {
func getNotification(completion: @escaping (LocalNotification?) -> Void) {
Log("Local Notification Fetch - fetching", module: .interactor)
if mainRepository.localNotificationsHandled {
Log("Local Notification Fetch - handled, ready", module: .interactor)
fetched = true
completion(currentNotification())
} else {
Log("Local Notification Fetch - awaiting", module: .interactor)
notificationCallback = completion
}
}
func markNotificationAsRead() {
Log("Local Notification Fetch - mark as read", module: .interactor)
setWasRead(true)
}
}
private extension LocalNotificationFetchInteractor {
@objc
private func notificationsHandled() {
guard !fetched else { return }
fetched = true
Log("Local Notification Fetch - handled after awaiting", module: .interactor)
notificationCallback?(currentNotification())
}
var wasRead: Bool {
mainRepository.localNotificationWasRead
}
func setWasRead(_ wasRead: Bool) {
mainRepository.saveLocalNotificationWasRead(wasRead)
}
var publishedID: String? {
mainRepository.localNotificationPublicationID
}
var cycle: Int {
mainRepository.localNotificationCycle
}
func currentNotification() -> LocalNotification? {
guard let publishedID, let notificationPublishedDate else {
Log("Local Notification Fetch - no notification", module: .interactor)
return nil
}
let kind = LocalNotification.NotificationKind.kindFromCycle(cycle)
Log("Local Notification Fetch - we have notification of kind: \(kind)", module: .interactor)
return LocalNotification(
id: publishedID,
kind: kind,
publishedAt: notificationPublishedDate,
wasRead: wasRead
)
}
var isPublished: Bool {
mainRepository.localNotificationPublicationID != nil
}
var notificationPublishedDate: Date? {
mainRepository.localNotificationPublicationDate
}
}

View File

@ -0,0 +1,236 @@
//
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
// Copyright © 2024 Two Factor Authentication Service, Inc.
// Contributed by Zbigniew Cisiński. 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
import Common
public protocol LocalNotificationStateInteracting: AnyObject {
func activate()
}
final class LocalNotificationStateInteractor {
private let mainRepository: MainRepository
private let serviceListingInteractor: ServiceListingInteracting
private let cloudBackup: CloudBackupStateInteracting
private let pairingDeviceInteractor: PairingWebExtensionInteracting
private let mdmInteractor: MDMInteracting
private let notificationCenter = NotificationCenter.default
private let cycleDays: Int = 30
private let firstNotificationDays: Int = 2
private var backupStateKnown = false
private var awaitsBackupStateChange: Callback?
init(
mainRepository: MainRepository,
serviceListingInteractor: ServiceListingInteracting,
cloudBackup: CloudBackupStateInteracting,
pairingDeviceInteractor: PairingWebExtensionInteracting,
mdmInteractor: MDMInteracting
) {
self.mainRepository = mainRepository
self.serviceListingInteractor = serviceListingInteractor
self.cloudBackup = cloudBackup
self.pairingDeviceInteractor = pairingDeviceInteractor
self.mdmInteractor = mdmInteractor
cloudBackup.stateChanged = { [weak self] in
guard self?.backupStateKnown == false else { return }
self?.backupStateKnown = true
self?.awaitsBackupStateChange?()
self?.awaitsBackupStateChange = nil
}
cloudBackup.startMonitoring()
}
}
extension LocalNotificationStateInteractor: LocalNotificationStateInteracting {
func activate() {
if runCount == 0 { // First run
saveCycle(-2)
}
increaseRunCount()
if runCount >= 2 && cycle == -2 {
startNotification(-1)
markLocalNotificationsAsHandled()
return
}
guard isTimeForNext else {
markLocalNotificationsAsHandled()
return
}
clearNotification()
let next = nextCycle()
Log("Local Notification State - next cycle: \(next)", module: .interactor)
switch next {
case 0: canDisplayBackup { [weak self] value in
if value {
self?.startNotification(next)
} else {
self?.setInactiveNotification(next)
}
self?.markLocalNotificationsAsHandled()
}
case 1: if canDisplayBrowserExtension() {
startNotification(next)
} else {
setInactiveNotification(next)
}
markLocalNotificationsAsHandled()
case 2: if canDisplayDonation() {
startNotification(3)
} else {
setInactiveNotification(next)
}
markLocalNotificationsAsHandled()
default:
markLocalNotificationsAsHandled()
}
}
}
private extension LocalNotificationStateInteractor {
func markLocalNotificationsAsHandled() {
Log("Local Notification State - notification handled", module: .interactor)
mainRepository.markLocalNotificationsAsHandled()
notificationCenter.post(name: .localNotificationsHandled, object: nil)
}
var isTimeForNext: Bool {
guard let publicationDate = mainRepository.localNotificationPublicationDate else {
return true
}
let days = publicationDate.days(from: .now)
return days >= cycleDays
}
func saveNotificationPublicationDate() {
mainRepository.saveLocalNotificationPublicationDate(.now)
}
func clearNotificationPublicationDate() {
mainRepository.saveLocalNotificationPublicationDate(nil)
}
func saveCycle(_ cycle: Int) {
Log("Local Notification State - setting cycle: \(cycle)", module: .interactor)
mainRepository.saveLocalNotificationCycle(cycle)
}
func increaseRunCount() {
Log("Local Notification State - increaseRunCount", module: .interactor)
mainRepository.saveRunCount(runCount + 1)
}
var runCount: Int {
mainRepository.runCount
}
var cycle: Int {
mainRepository.localNotificationCycle
}
func startNotification(_ cycle: Int) {
Log("Local Notification State - start notification for cycle: \(cycle)", module: .interactor)
saveNotificationPublicationDate()
saveCycle(cycle)
setIsPublished(true)
}
func setInactiveNotification(_ cycle: Int) {
Log("Local Notification State - set inactive notification for cycle: \(cycle)", module: .interactor)
saveNotificationPublicationDate()
saveCycle(cycle)
setIsPublished(false)
}
func increaseCycle() {
saveCycle(nextCycle())
}
func clearNotification() {
Log("Local Notification State - clear notifications", module: .interactor)
setIsPublished(false)
clearNotificationPublicationDate()
setWasRead(false)
}
func setWasRead(_ wasRead: Bool) {
mainRepository.saveLocalNotificationWasRead(wasRead)
}
var hasServices: Bool {
serviceListingInteractor.hasServices
}
var isBrowserExtensionActive: Bool {
pairingDeviceInteractor.hasActiveBrowserExtension
}
func setIsPublished(_ isPublished: Bool) {
if isPublished {
mainRepository.saveLocalNotificationPublicationID(UUID().uuidString)
} else {
mainRepository.saveLocalNotificationPublicationID(nil)
}
}
func nextCycle() -> Int {
switch cycle {
case -2: -1
case -1: 0
case 0: 1
case 1: 2
case 2: 0
default: 0
}
}
func canDisplayBackup(completion: @escaping (Bool) -> Void) {
if backupStateKnown {
completion(isBackupPossible())
return
}
awaitsBackupStateChange = { [weak self] in
guard let self else { return }
completion(isBackupPossible())
}
}
func isBackupPossible() -> Bool {
!cloudBackup.isBackupEnabled && hasServices && !mdmInteractor.isBackupBlocked
}
func canDisplayBrowserExtension() -> Bool {
!isBrowserExtensionActive && hasServices && !mdmInteractor.isBrowserExtensionBlocked
}
func canDisplayDonation() -> Bool {
hasServices
}
}

View File

@ -0,0 +1,136 @@
//
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
// Copyright © 2024 Two Factor Authentication Service, Inc.
// Contributed by Zbigniew Cisiński. 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
import Common
public protocol MDMInteracting: AnyObject {
var isBackupBlocked: Bool { get }
var isBiometryBlocked: Bool { get }
var isBrowserExtensionBlocked: Bool { get }
var isLockoutAttemptsChangeBlocked: Bool { get }
var isLockoutBlockTimeChangeBlocked: Bool { get }
var isPasscodeRequried: Bool { get }
var shouldSetPasscode: Bool { get }
func apply()
}
final class MDMInteractor {
private let mainRepository: MainRepository
private let pairingInteractor: PairingWebExtensionInteracting
private let cloudBackupStateInteractor: CloudBackupStateInteracting
private var syncDetermined = false
private var syncDisabled = false
init(
mainRepository: MainRepository,
pairingInteractor: PairingWebExtensionInteracting,
cloudBackupStateInteractor: CloudBackupStateInteracting
) {
self.mainRepository = mainRepository
self.pairingInteractor = pairingInteractor
self.cloudBackupStateInteractor = cloudBackupStateInteractor
cloudBackupStateInteractor.stateChanged = { [weak self] in self?.syncStateDetermined() }
cloudBackupStateInteractor.startMonitoring()
if cloudBackupStateInteractor.isBackupEnabled {
syncStateDetermined()
}
}
}
extension MDMInteractor: MDMInteracting {
var isBackupBlocked: Bool {
mainRepository.mdmIsBackupBlocked
}
var isBiometryBlocked: Bool {
mainRepository.mdmIsBiometryBlocked
}
var isBrowserExtensionBlocked: Bool {
mainRepository.mdmIsBrowserExtensionBlocked
}
var isLockoutAttemptsChangeBlocked: Bool {
mainRepository.mdmLockoutAttempts != nil
}
var isLockoutBlockTimeChangeBlocked: Bool {
mainRepository.mdmLockoutBlockTime != nil
}
var isPasscodeRequried: Bool {
mainRepository.mdmIsPasscodeRequried
}
var shouldSetPasscode: Bool {
isPasscodeRequried && !mainRepository.isPINSet
}
func apply() {
if syncDetermined {
disableSyncIfNecessary()
}
if isBiometryBlocked && mainRepository.isBiometryEnabled {
Log("MDMInteractor - disabling Biometry", module: .interactor)
mainRepository.disableBiometry()
}
if isBrowserExtensionBlocked && pairingInteractor.hasActiveBrowserExtension {
Log("MDMInteractor - disabling Browser Extension", module: .interactor)
pairingInteractor.disableExtension(completion: { _ in })
}
if let lockoutAttempts = mainRepository.mdmLockoutAttempts {
Log("MDMInteractor - setting Lockout Attemtps", module: .interactor)
mainRepository.setAppLockAttempts(lockoutAttempts)
}
if let blockTime = mainRepository.mdmLockoutBlockTime {
Log("MDMInteractor - setting Lockout Block Time", module: .interactor)
mainRepository.setAppLockBlockTime(blockTime)
}
}
}
private extension MDMInteractor {
func syncStateDetermined() {
guard !syncDetermined else { return }
Log("MDMInteractor - syncStateDetermined", module: .interactor)
syncDetermined = true
cloudBackupStateInteractor.stopMonitoring()
disableSyncIfNecessary()
}
func disableSyncIfNecessary() {
Log(
"MDMInteractor - disableSyncIfNecessary: Backup enabled: \(cloudBackupStateInteractor.isBackupEnabled)",
module: .interactor
)
if isBackupBlocked && cloudBackupStateInteractor.isBackupEnabled && !syncDisabled {
Log("MDMInteractor - disableSyncIfNecessary - Clearing", module: .interactor)
syncDisabled = true
mainRepository.clearBackup()
}
}
}

View File

@ -72,6 +72,7 @@ extension NewsInteractor: NewsInteracting {
network.fetchNews { [weak self] result in
switch result {
case .success(let newList):
Log("NewsInteractor: News list fetched, items count: \(newList.count)", module: .moduleInteractor)
self?.mainRepository.saveLastNewsFetch(Date())
self?.handleFetchedList(newList)
case .failure:

View File

@ -27,6 +27,7 @@ public enum PairingWebExtensionError: Error {
case noPublicKey
case serverError
case noInternet
case blocked
}
public enum UnparingWebExtensionError: Error {
@ -43,6 +44,7 @@ public enum FetchingListError: Error {
}
public protocol PairingWebExtensionInteracting: AnyObject {
var hasActiveBrowserExtension: Bool { get }
func extensionData(for extensionID: ExtensionID) -> PairedWebExtension?
func pair(with extensionID: ExtensionID, completion: @escaping (Result<Void, PairingWebExtensionError>) -> Void)
func listAll() -> [PairedWebExtension]
@ -51,6 +53,7 @@ public protocol PairingWebExtensionInteracting: AnyObject {
completion: @escaping (Result<Void, UnparingWebExtensionError>) -> Void
)
func fetchList(completion: @escaping (Result<[PairedWebExtension], FetchingListError>) -> Void)
func disableExtension(completion: @escaping (Result<Void, UnparingWebExtensionError>) -> Void)
}
final class PairingWebExtensionInteractor {
@ -72,12 +75,19 @@ final class PairingWebExtensionInteractor {
}
extension PairingWebExtensionInteractor: PairingWebExtensionInteracting {
var hasActiveBrowserExtension: Bool { !listAll().isEmpty }
func extensionData(for extensionID: ExtensionID) -> PairedWebExtension? {
listAll().first { $0.extensionID == extensionID }
}
func pair(with extensionID: ExtensionID, completion: @escaping (Result<Void, PairingWebExtensionError>) -> Void) {
Log("PairingWebExtensionInteractor - pair. extensionID: \(extensionID)", module: .interactor)
guard !mainRepository.mdmIsBrowserExtensionBlocked else {
Log("PairingWebExtensionInteractor - pair. Error: blocked!", module: .interactor)
completion(.failure(.blocked))
return
}
guard !mainRepository.listAllPairedExtensions().map({ $0.extensionID }).contains(extensionID) else {
Log("PairingWebExtensionInteractor - failure. Already paired", module: .interactor)
completion(.failure(.alreadyPaired))
@ -193,6 +203,51 @@ extension PairingWebExtensionInteractor: PairingWebExtensionInteracting {
}
}
}
func disableExtension(completion: @escaping (Result<Void, UnparingWebExtensionError>) -> Void) {
Log("PairingWebExtensionInteractor - disableExtension", module: .interactor)
guard let deviceID = mainRepository.deviceID else {
Log("PairingWebExtensionInteractor - disableExtension. Failure: not registered", module: .interactor)
completion(.failure(.notRegistered))
return
}
let extensionIDs = mainRepository.listAllPairedExtensions().map({ $0.extensionID })
guard !extensionIDs.isEmpty else {
Log("PairingWebExtensionInteractor - disableExtension. Failure: not paired", module: .interactor)
completion(.failure(.notPaired))
return
}
var count = extensionIDs.count
var errored = false
for extensionID in extensionIDs {
mainRepository.deletePairing(for: deviceID, extensionID: extensionID) { [weak self] result in
switch result {
case .success:
Log("PairingWebExtensionInteractor - disableExtension. Success", module: .interactor)
self?.mainRepository.deletePairedExtension(with: extensionID)
self?.mainRepository.removeAuthRequest(for: extensionID)
count -= 1
if count == 0 && !errored {
completion(.success(Void()))
}
case .failure(let error):
guard !errored else { return }
Log("PairingWebExtensionInteractor - disableExtension. Error: \(error)", module: .interactor)
errored = true
switch error {
case .noInternet: completion(.failure(.noInternet))
case .connection: completion(.failure(.serverError))
}
}
}
}
}
}
private extension PairingWebExtensionInteractor {

View File

@ -81,7 +81,9 @@ extension RootInteractor: RootInteracting {
func markIntroAsShown() {
mainRepository.setIntroductionAsShown()
mainRepository.enableCloudBackup()
if !mainRepository.mdmIsBackupBlocked {
mainRepository.enableCloudBackup()
}
}
func lockApplicationIfNeeded(presentLoginImmediately: @escaping () -> Void) {

View File

@ -0,0 +1,29 @@
//
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
// Copyright © 2024 Two Factor Authentication Service, Inc.
// Contributed by Zbigniew Cisiński. 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 MDMRepository: AnyObject {
var isBackupBlocked: Bool { get }
var isBiometryBlocked: Bool { get }
var isBrowserExtensionBlocked: Bool { get }
var lockoutAttepts: AppLockAttempts? { get }
var lockoutBlockTime: AppLockBlockTime? { get }
var isPasscodeRequried: Bool { get }
}

View File

@ -0,0 +1,104 @@
//
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
// Copyright © 2024 Two Factor Authentication Service, Inc.
// Contributed by Zbigniew Cisiński. 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
final class MDMRepositoryImpl {
private let mdmKey = "com.apple.configuration.managed"
private let userDefaults = UserDefaults.standard
private let notificationCenter = NotificationCenter.default
private var settings: [String: Any] = [:]
private enum SettingsKeys: String {
case isBackupBlocked = "blockBackup"
case isBiometryBlocked = "blockBiometry"
case isBrowserExtensionBlocked = "blockBrowserExtension"
case lockoutAttempts = "lockoutAttempts"
case lockoutBlockTime = "lockoutBlockTime"
case isPasscodeRequried = "requirePasscode"
}
private let isBackupBlockedDefaultValue = false
private let isBiometryBlockedDefaultValue = false
private let isBrowserExtensionBlockedDefaultValue = false
private let isPasscodeRequriedDefaultValue = false
init() {
reload()
}
private func reload() {
settings = userDefaults.dictionary(forKey: mdmKey) ?? [:]
}
}
extension MDMRepositoryImpl: MDMRepository {
var isBackupBlocked: Bool {
guard let value = settings[SettingsKeys.isBackupBlocked.rawValue] as? Bool else {
return isBackupBlockedDefaultValue
}
return value
}
var isBiometryBlocked: Bool {
guard let value = settings[SettingsKeys.isBiometryBlocked.rawValue] as? Bool else {
return isBiometryBlockedDefaultValue
}
return value
}
var isBrowserExtensionBlocked: Bool {
guard let value = settings[SettingsKeys.isBrowserExtensionBlocked.rawValue] as? Bool else {
return isBrowserExtensionBlockedDefaultValue
}
return value
}
var lockoutAttepts: AppLockAttempts? {
guard let value = settings[SettingsKeys.lockoutAttempts.rawValue] as? Int else {
return nil
}
switch value {
case 0: return .noLimit
case 3: return .try3
case 5: return .try5
case 10: return .try10
default: return nil
}
}
var lockoutBlockTime: AppLockBlockTime? {
guard let value = settings[SettingsKeys.lockoutBlockTime.rawValue] as? Int else {
return nil
}
switch value {
case 3: return .min3
case 5: return .min5
case 10: return .min10
default: return nil
}
}
var isPasscodeRequried: Bool {
guard let value = settings[SettingsKeys.isPasscodeRequried.rawValue] as? Bool else {
return isPasscodeRequriedDefaultValue
}
return value
}
}

View File

@ -122,6 +122,7 @@ protocol MainRepository: AnyObject {
// MARK: - Cloud
var secretSyncError: ((String) -> Void)? { get set }
var isCloudBackupConnected: Bool { get }
var successSyncDate: Date? { get }
var cloudCurrentState: CloudState { get }
func registerForCloudStateChanges(_ listener: @escaping CloudStateListener, id: CloudStateListenerID)
func unregisterForCloudStageChanges(with id: CloudStateListenerID)
@ -133,6 +134,7 @@ protocol MainRepository: AnyObject {
userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
)
func saveSuccessSyncDate(_ date: Date?)
// MARK: - Import
var fileURL: URL? { get set }
@ -471,4 +473,31 @@ protocol MainRepository: AnyObject {
// MARK: - Time Verification
func timeVerificationStart()
// MARK: - MDM options
var mdmIsBackupBlocked: Bool { get }
var mdmIsBiometryBlocked: Bool { get }
var mdmIsBrowserExtensionBlocked: Bool { get }
var mdmLockoutAttempts: AppLockAttempts? { get }
var mdmLockoutBlockTime: AppLockBlockTime? { get }
var mdmIsPasscodeRequried: Bool { get }
// MARK: - Local Notifications
var localNotificationPublicationDate: Date? { get }
func saveLocalNotificationPublicationDate(_ date: Date?)
var localNotificationPublicationID: String? { get }
func saveLocalNotificationPublicationID(_ ID: String?)
var localNotificationWasRead: Bool { get }
func saveLocalNotificationWasRead(_ wasRead: Bool)
var localNotificationCycle: Int { get }
func saveLocalNotificationCycle(_ cycle: Int)
var runCount: Int { get }
func saveRunCount(_ count: Int)
var localNotificationsHandled: Bool { get }
func markLocalNotificationsAsHandled()
}

View File

@ -44,6 +44,10 @@ public enum CloudState: Equatable {
}
extension MainRepositoryImpl {
var successSyncDate: Date? {
userDefaultsRepository.successSyncDate
}
var secretSyncError: ((String) -> Void)? {
get {
cloudHandler.secretSyncError
@ -89,6 +93,10 @@ extension MainRepositoryImpl {
) {
SyncInstance.didReceiveRemoteNotification(userInfo: userInfo, fetchCompletionHandler: completionHandler)
}
func saveSuccessSyncDate(_ date: Date?) {
userDefaultsRepository.saveSuccessSyncDate(date)
}
}
private extension MainRepositoryImpl {

View File

@ -0,0 +1,70 @@
//
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
// Copyright © 2024 Two Factor Authentication Service, Inc.
// Contributed by Zbigniew Cisiński. 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
extension MainRepositoryImpl {
var localNotificationPublicationDate: Date? {
userDefaultsRepository.localNotificationPublicationDate
}
func saveLocalNotificationPublicationDate(_ date: Date?) {
userDefaultsRepository.saveLocalNotificationPublicationDate(date)
}
var localNotificationPublicationID: String? {
userDefaultsRepository.localNotificationPublicationID
}
func saveLocalNotificationPublicationID(_ ID: String?) {
userDefaultsRepository.saveLocalNotificationPublicationID(ID)
}
var localNotificationWasRead: Bool {
userDefaultsRepository.localNotificationWasRead
}
func saveLocalNotificationWasRead(_ wasRead: Bool) {
userDefaultsRepository.saveLocalNotificationWasRead(wasRead)
}
var localNotificationCycle: Int {
userDefaultsRepository.localNotificationCycle
}
func saveLocalNotificationCycle(_ cycle: Int) {
userDefaultsRepository.saveLocalNotificationCycle(cycle)
}
var runCount: Int {
userDefaultsRepository.runCount
}
func saveRunCount(_ count: Int) {
userDefaultsRepository.saveRunCount(count)
}
var localNotificationsHandled: Bool {
_areLocalNotificationsHandled
}
func markLocalNotificationsAsHandled() {
_areLocalNotificationsHandled = true
}
}

View File

@ -0,0 +1,46 @@
//
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
// Copyright © 2024 Two Factor Authentication Service, Inc.
// Contributed by Zbigniew Cisiński. 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
extension MainRepositoryImpl {
var mdmIsBackupBlocked: Bool {
mdmRepository.isBackupBlocked
}
var mdmIsBiometryBlocked: Bool {
mdmRepository.isBiometryBlocked
}
var mdmIsBrowserExtensionBlocked: Bool {
mdmRepository.isBrowserExtensionBlocked
}
var mdmLockoutAttempts: AppLockAttempts? {
mdmRepository.lockoutAttepts
}
var mdmLockoutBlockTime: AppLockBlockTime? {
mdmRepository.lockoutBlockTime
}
var mdmIsPasscodeRequried: Bool {
mdmRepository.isPasscodeRequried
}
}

View File

@ -50,6 +50,7 @@ final class MainRepositoryImpl: MainRepository {
let serviceDefinitionDatabase: ServiceDefinitionDatabase = ServiceDefinitionDatabaseImpl()
let iconDescriptionDatabase: IconDescriptionDatabase = IconDescriptionDatabaseImpl()
let initialPermissionStateDataController = PermissionsStateDataController()
let mdmRepository: MDMRepository = MDMRepositoryImpl()
let serviceNameTranslation: String
let notificationCenter = NotificationCenter.default
@ -86,6 +87,7 @@ final class MainRepositoryImpl: MainRepository {
var storageError: ((String) -> Void)?
var _isLockScreenActive = false
var _areLocalNotificationsHandled = false
// Cached values for higher pefrormance
var cachedSortType: SortType?

View File

@ -97,4 +97,24 @@ protocol UserDefaultsRepository: AnyObject {
var exchangeToken: String? { get }
func setExchangeToken(_ key: String)
func clearExchangeToken()
var successSyncDate: Date? { get }
func saveSuccessSyncDate(_ date: Date?)
// MARK: - Local Notifications
var localNotificationPublicationDate: Date? { get }
func saveLocalNotificationPublicationDate(_ date: Date?)
var localNotificationPublicationID: String? { get }
func saveLocalNotificationPublicationID(_ ID: String?)
var localNotificationWasRead: Bool { get }
func saveLocalNotificationWasRead(_ wasRead: Bool)
var localNotificationCycle: Int { get }
func saveLocalNotificationCycle(_ cycle: Int)
var runCount: Int { get }
func saveRunCount(_ count: Int)
}

View File

@ -51,6 +51,12 @@ final class UserDefaultsRepositoryImpl: UserDefaultsRepository {
case mainMenuPortraitCollapsed
case mainMenuLandscapeCollapsed
case dateOfFirstRun
case syncSuccessDate
case localNotificationPublicationDate
case localNotificationPublicationID
case localNotificationWasRead
case localNotificationCycle
case runCount
}
private let userDefaults = UserDefaults()
private let sharedDefaults = UserDefaults(suiteName: Config.suiteName)!
@ -279,6 +285,16 @@ final class UserDefaultsRepositoryImpl: UserDefaultsRepository {
userDefaults.bool(forKey: Keys.introductionWasShown.rawValue)
}
// MARK: - Sync success
var successSyncDate: Date? {
userDefaults.object(forKey: Keys.syncSuccessDate.rawValue) as? Date
}
func saveSuccessSyncDate(_ date: Date?) {
userDefaults.set(date, forKey: Keys.syncSuccessDate.rawValue)
userDefaults.synchronize()
}
// MARK: - View Path
func clearViewPath() {
@ -332,6 +348,53 @@ final class UserDefaultsRepositoryImpl: UserDefaultsRepository {
sharedDefaults.synchronize()
}
// MARK: - Local Notifications
var localNotificationPublicationDate: Date? {
userDefaults.object(forKey: Keys.localNotificationPublicationDate.rawValue) as? Date
}
func saveLocalNotificationPublicationDate(_ date: Date?) {
userDefaults.set(date, forKey: Keys.localNotificationPublicationDate.rawValue)
userDefaults.synchronize()
}
var localNotificationPublicationID: String? {
userDefaults.string(forKey: Keys.localNotificationPublicationID.rawValue)
}
func saveLocalNotificationPublicationID(_ ID: String?) {
userDefaults.set(ID, forKey: Keys.localNotificationPublicationID.rawValue)
userDefaults.synchronize()
}
var localNotificationWasRead: Bool {
userDefaults.bool(forKey: Keys.localNotificationWasRead.rawValue)
}
func saveLocalNotificationWasRead(_ wasRead: Bool) {
userDefaults.set(wasRead, forKey: Keys.localNotificationWasRead.rawValue)
userDefaults.synchronize()
}
var localNotificationCycle: Int {
userDefaults.integer(forKey: Keys.localNotificationCycle.rawValue)
}
func saveLocalNotificationCycle(_ cycle: Int) {
userDefaults.set(cycle, forKey: Keys.localNotificationCycle.rawValue)
userDefaults.synchronize()
}
var runCount: Int {
userDefaults.integer(forKey: Keys.runCount.rawValue)
}
func saveRunCount(_ count: Int) {
userDefaults.set(count, forKey: Keys.runCount.rawValue)
userDefaults.synchronize()
}
// MARK: - Clear all
func clearAll() {

75
TwoFAS/MDM/Description.md Normal file
View File

@ -0,0 +1,75 @@
# Managed App Configuration
This is an early version of MAC support. We've implemented basic parameters requested via GitHub issue.
# Parameters
Provided parameters are optional and should work for new deployments. Those scenarios were also tested on existing deployments, but please note that there could be bugs.
If you need more parameters and you want to support the project please don't hesitate to contact us.
## Block Backup
### Key
`blockBackup`
### Value type
`bool`
### Value
`true` to blocked backup, `null` or `false` otherwise.
### Description
Blocks Sync, Copy Secret and file Export functionality.
### Existing deployment
iCloud sync, if enabled, is wipe out and disabled.
## Block Biometry
### Key
`blockBiometry`
### Value type
`bool`
### Value
`true` to block biometry, `null` or `false` otherwise.
### Description
Blocks usage of biometry to authorize user into the app.
### Existing deployment
Disables biometry on next run.
## Block Browser Extension
### Key
`blockBrowserExtension`
### Value type
`bool`
### Value
`true` to block Browser Extension, `null` or `false` otherwise.
### Description
Blocks usage of Browser Extension.
### Existing deployment
Unpairs the device.
## Lockout - Attempts
### Key
`lockoutAttempts`
### Value type
`int`
### Value
`0`, `3`, `5` or `10`. `null` for app's default value (3).
### Description
Locks app after 3, 5 or 10 wrong authorisation attempts. `0` - no limit.
### Existing deployment
Sets the value.
## Lockout - Block Time
### Key
`lockoutBlockTime`
### Value type
`int`
### Value
`3`, `5` or `10`. `null` for app's default value (10).
### Description
Block the app for 3, 5, 10 minutes.
### Existing deployment
Sets the value.
## Require Passcode
### Key
`requirePasscode`
### Value type
`bool`
### Value
`true` to require passcode for user authorisation, `null` or `false` otherwise.
### Description
Requries user to setup a passcode. Blocks disabling it. User can change existing passcode.
### Existing deployment
If passcode is not set than after the next run user will be required to setup one.

92
TwoFAS/MDM/specfile.xml Normal file
View File

@ -0,0 +1,92 @@
<?xml version="1.0"?>
<managedAppConfiguration>
<version>1.0.0</version>
<bundleId>com.twofas.org</bundleId>
<dict>
<boolean keyName="blockBackup">
<constraints nullable="true">
</constraints>
</boolean>
<boolean keyName="blockBiometry">
<constraints nullable="true">
</constraints>
</boolean>
<boolean keyName="blockBrowserExtension">
<constraints nullable="true">
</constraints>
</boolean>
<integer keyName="lockoutAttempts">
<constraints nullable="true">
<values>
<value>0</value>
<value>3</value>
<value>5</value>
<value>10</value>
</values>
</constraints>
</integer>
<integer keyName="lockoutBlockTime">
<constraints nullable="true">
<values>
<value>3</value>
<value>5</value>
<value>10</value>
</values>
</constraints>
</integer>
<boolean keyName="requirePasscode">
<constraints nullable="true">
</constraints>
</boolean>
</dict>
<presentation defaultLocale="en-US">
<field keyName="blockBackup" type="checkbox">
<label>
<language value="en-US">Block backup</language>
</label>
<description>
<language value="en-US">Blocks iCloud sync and Export functionality</language>
</description>
</field>
<field keyName="blockBiometry" type="checkbox">
<label>
<language value="en-US">Block biometry</language>
</label>
<description>
<language value="en-US">Blocks usage of biometry to authenticate</language>
</description>
</field>
<field keyName="blockBrowserExtension" type="checkbox">
<label>
<language value="en-US">Block Browser Extension</language>
</label>
<description>
<language value="en-US">Blocks usage of Browser Extension</language>
</description>
</field>
<field keyName="lockoutAttempts" type="input">
<label>
<language value="en-US">Lockout Attempts</language>
</label>
<description>
<language value="en-US">Block app after X login attempts. Valid values: 0 - No limit, 3, 5 and 10 attempts</language>
</description>
</field>
<field keyName="lockoutBlockTime" type="input">
<label>
<language value="en-US">Lockout Block Time</language>
</label>
<description>
<language value="en-US">Block authentication for X minutes. Valid values are: 3, 5, and 10 (minutes).</language>
</description>
</field>
<field keyName="requirePasscode" type="checkbox">
<label>
<language value="en-US">Require Passcode</language>
</label>
<description>
<language value="en-US">Require Passcode</language>
</description>
</field>
</presentation>
</managedAppConfiguration>

View File

@ -85,7 +85,8 @@ private extension StorageRepositoryImpl {
message: entity.message,
publishedAt: entity.publishedAt,
createdAt: entity.createdAt,
wasRead: entity.wasRead
wasRead: entity.wasRead,
internalLink: nil
)
})
}

View File

@ -85,6 +85,8 @@ final class CloudHandler: CloudHandlerType {
private let itemHandlerMigrationProxy: ItemHandlerMigrationProxy
private let cloudKit: CloudKit
private let notificationCenter = NotificationCenter.default
private var isClearing = false
private var listeners: [String: CloudHandlerStateListener] = [:]
@ -313,6 +315,7 @@ final class CloudHandler: CloudHandlerType {
private func setDisabled() {
Log("Cloud Handler - Set Disabled", module: .cloudSync)
ConstStorage.cloudEnabled = false
notificationCenter.post(name: .clearSyncCompletedSuccessfuly, object: nil)
}
private var isEnabled: Bool { ConstStorage.cloudEnabled }
@ -344,6 +347,8 @@ final class CloudHandler: CloudHandlerType {
if isClearing {
clearBackup()
} else {
notificationCenter.post(name: .syncCompletedSuccessfuly, object: nil)
}
}

View File

@ -20,6 +20,7 @@
import Foundation
import CloudKit
import Common
import UIKit
final class CloudKit {
typealias DeletedEntries = ([(name: String, type: String)]) -> Void
@ -37,6 +38,7 @@ final class CloudKit {
var useriCloudProblem: Callback?
var userLoggedOut: Callback?
var resetStack: Callback?
var abortSync: Callback?
var fetchFinishedSuccessfuly: Callback?
var changesSavedSuccessfuly: Callback?
@ -472,6 +474,15 @@ final class CloudKit {
zoneUpdated = false
DispatchQueue.main.async {
if UIApplication.shared.applicationState == .background {
self.abortSync?()
self.syncTokenHandler.prepare()
self.clearRecordChanges()
self.operation?.cancel()
self.operation = nil
return
}
if !self.deletedRecords.isEmpty {
Log("CloudKit - deletedRecords not empty", module: .cloudSync)
self.deletedEntries?(self.deletedRecords.map { (name: $0.record.recordName, type: $0.type) })

View File

@ -62,6 +62,7 @@ final class SyncHandler {
cloudKit.updatedEntries = { [weak self] entries in self?.updateEntries(entries) }
cloudKit.fetchFinishedSuccessfuly = { [weak self] in self?.fetchFinishedSuccessfuly() }
cloudKit.changesSavedSuccessfuly = { [weak self] in self?.changesSavedSuccessfuly() }
cloudKit.abortSync = { [weak self] in self?.abortSync() }
cloudKit.resetStack = { [weak self] in
Log("SyncHandler - resetStack", module: .cloudSync)
@ -386,5 +387,11 @@ final class SyncHandler {
private func dateOffsetet(for logEntity: LogEntity) -> Date {
logEntity.date.addingTimeInterval(TimeInterval(timeOffset))
}
private func abortSync() {
isSyncing = false
applyingChanges = false
}
// swiftlint:enable line_length
}

View File

@ -142,6 +142,10 @@
C2286034266972B5005E88E3 /* TokenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2286033266972B5005E88E3 /* TokenCell.swift */; };
C22C1399267682B4001AA5F1 /* CategoryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22C1398267682B4001AA5F1 /* CategoryData.swift */; };
C22C139B267684DC001AA5F1 /* CategoryHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22C139A267684DC001AA5F1 /* CategoryHandler.swift */; };
C22CD3922B913E94005FE348 /* MDMRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22CD3912B913E94005FE348 /* MDMRepository.swift */; };
C22CD3942B913EB4005FE348 /* MDMRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22CD3932B913EB4005FE348 /* MDMRepositoryImpl.swift */; };
C22CD3962B91419B005FE348 /* MainRepositoryImpl+MDM.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22CD3952B91419B005FE348 /* MainRepositoryImpl+MDM.swift */; };
C22CD3982B9142CA005FE348 /* MDMInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22CD3972B9142CA005FE348 /* MDMInteractor.swift */; };
C22CF3DF27413F0F004F6A03 /* LogStorage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C22CF3DD27413F0F004F6A03 /* LogStorage.xcdatamodeld */; };
C22CF3E327414073004F6A03 /* LogEntryEntity+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22CF3E127414073004F6A03 /* LogEntryEntity+CoreDataClass.swift */; };
C22CF3E427414073004F6A03 /* LogEntryEntity+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22CF3E227414073004F6A03 /* LogEntryEntity+CoreDataProperties.swift */; };
@ -404,6 +408,7 @@
C274C7742ADD3CB000B8AAC1 /* CounterState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24DFB2027D1770D00F3EACC /* CounterState.swift */; };
C274C7762ADD3DF900B8AAC1 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = C274C7752ADD3DF900B8AAC1 /* FirebaseMessaging */; };
C274C77C2ADD3E3500B8AAC1 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = C274C77B2ADD3E3500B8AAC1 /* FirebaseCrashlytics */; };
C276D1712B9A672C008C9CD4 /* LocalNotificationStateInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C276D1702B9A672C008C9CD4 /* LocalNotificationStateInteractor.swift */; };
C277C32C245C3FD6009214F3 /* MainContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C277C32B245C3FD6009214F3 /* MainContainerViewController.swift */; };
C278121C27F9F3E600F31453 /* ExportPublicKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C278121B27F9F3E600F31453 /* ExportPublicKey.swift */; };
C278121F27F9F4C000F31453 /* SwCrypt in Frameworks */ = {isa = PBXBuildFile; productRef = C278121E27F9F4C000F31453 /* SwCrypt */; };
@ -667,6 +672,7 @@
C2BBD1D42B8113A9009A91FB /* TwoFASWidgetInline.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BBD1D22B81127C009A91FB /* TwoFASWidgetInline.swift */; };
C2BBD1D72B812117009A91FB /* TwoFASWidgetCircular.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BBD1D52B8120F4009A91FB /* TwoFASWidgetCircular.swift */; };
C2BBD2382B8130B0009A91FB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C2BBD2332B81308C009A91FB /* Localizable.strings */; };
C2BC3FC92B94E70F004C4BA0 /* NewPINNavigationFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BC3FC82B94E70F004C4BA0 /* NewPINNavigationFlowController.swift */; };
C2BD82B2236619E800FBD69A /* BackupAreaWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BD82B1236619E800FBD69A /* BackupAreaWarningView.swift */; };
C2BD85DA2640290E0087D087 /* IntroductionFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BD85D92640290E0087D087 /* IntroductionFlowController.swift */; };
C2BD85DC26402A910087D087 /* IntroductionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BD85DB26402A910087D087 /* IntroductionContainerView.swift */; };
@ -1019,6 +1025,8 @@
C2EA566D285696FE00026BFE /* AskForAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2EA566C285696FE00026BFE /* AskForAuthViewController.swift */; };
C2EA566F28569A5E00026BFE /* AskForAuthPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2EA566E28569A5E00026BFE /* AskForAuthPresenter.swift */; };
C2EA567128569ADD00026BFE /* AskForAuthFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2EA567028569ADD00026BFE /* AskForAuthFlowController.swift */; };
C2EA6E5C2BA10AFE0034A964 /* LocalNotificationFetchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2EA6E5B2BA10AFE0034A964 /* LocalNotificationFetchInteractor.swift */; };
C2EA6E5E2BA110FA0034A964 /* MainRepositoryImpl+LocalNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2EA6E5D2BA110FA0034A964 /* MainRepositoryImpl+LocalNotifications.swift */; };
C2EBDFFC2AC44750008FD744 /* GuideMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2EBDFFB2AC44750008FD744 /* GuideMenuViewController.swift */; };
C2EBDFFE2AC4476D008FD744 /* GuideMenuPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2EBDFFD2AC4476D008FD744 /* GuideMenuPresenter.swift */; };
C2EBE0022AC44787008FD744 /* GuideMenuFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2EBE0012AC44787008FD744 /* GuideMenuFlowController.swift */; };
@ -1674,6 +1682,10 @@
C22A8669287F60BD009A43FE /* MainRepositoryImpl+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainRepositoryImpl+Notifications.swift"; sourceTree = "<group>"; };
C22C1398267682B4001AA5F1 /* CategoryData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryData.swift; sourceTree = "<group>"; };
C22C139A267684DC001AA5F1 /* CategoryHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryHandler.swift; sourceTree = "<group>"; };
C22CD3912B913E94005FE348 /* MDMRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MDMRepository.swift; sourceTree = "<group>"; };
C22CD3932B913EB4005FE348 /* MDMRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MDMRepositoryImpl.swift; sourceTree = "<group>"; };
C22CD3952B91419B005FE348 /* MainRepositoryImpl+MDM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainRepositoryImpl+MDM.swift"; sourceTree = "<group>"; };
C22CD3972B9142CA005FE348 /* MDMInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MDMInteractor.swift; sourceTree = "<group>"; };
C22CD79A2630D43F009C0258 /* FileInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileInteractor.swift; sourceTree = "<group>"; };
C22CF3DE27413F0F004F6A03 /* LogStorage.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LogStorage.xcdatamodel; sourceTree = "<group>"; };
C22CF3E127414073004F6A03 /* LogEntryEntity+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LogEntryEntity+CoreDataClass.swift"; sourceTree = "<group>"; };
@ -1969,6 +1981,7 @@
C274BDC1276941FB00EE28BC /* AppSecurityPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSecurityPresenter.swift; sourceTree = "<group>"; };
C274BDC32769425900EE28BC /* AppSecurityPresenter+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppSecurityPresenter+Menu.swift"; sourceTree = "<group>"; };
C274BDC52769428700EE28BC /* AppSecurityModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSecurityModels.swift; sourceTree = "<group>"; };
C276D1702B9A672C008C9CD4 /* LocalNotificationStateInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationStateInteractor.swift; sourceTree = "<group>"; };
C277C32B245C3FD6009214F3 /* MainContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContainerViewController.swift; sourceTree = "<group>"; };
C278121B27F9F3E600F31453 /* ExportPublicKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportPublicKey.swift; sourceTree = "<group>"; };
C278122027F9FA9900F31453 /* RSAEncryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSAEncryption.swift; sourceTree = "<group>"; };
@ -2283,6 +2296,7 @@
C2BBD24C2B813162009A91FB /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = TwoFAS/Other/uk.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; };
C2BBD24D2B813162009A91FB /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = TwoFAS/Other/de.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; };
C2BBD24E2B813162009A91FB /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = TwoFAS/Other/pl.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; };
C2BC3FC82B94E70F004C4BA0 /* NewPINNavigationFlowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPINNavigationFlowController.swift; sourceTree = "<group>"; };
C2BD82B1236619E800FBD69A /* BackupAreaWarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAreaWarningView.swift; sourceTree = "<group>"; };
C2BD85D92640290E0087D087 /* IntroductionFlowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroductionFlowController.swift; sourceTree = "<group>"; };
C2BD85DB26402A910087D087 /* IntroductionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroductionContainerView.swift; sourceTree = "<group>"; };
@ -2529,6 +2543,8 @@
C2EA566C285696FE00026BFE /* AskForAuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AskForAuthViewController.swift; sourceTree = "<group>"; };
C2EA566E28569A5E00026BFE /* AskForAuthPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AskForAuthPresenter.swift; sourceTree = "<group>"; };
C2EA567028569ADD00026BFE /* AskForAuthFlowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AskForAuthFlowController.swift; sourceTree = "<group>"; };
C2EA6E5B2BA10AFE0034A964 /* LocalNotificationFetchInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationFetchInteractor.swift; sourceTree = "<group>"; };
C2EA6E5D2BA110FA0034A964 /* MainRepositoryImpl+LocalNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainRepositoryImpl+LocalNotifications.swift"; sourceTree = "<group>"; };
C2EBDFFB2AC44750008FD744 /* GuideMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuideMenuViewController.swift; sourceTree = "<group>"; };
C2EBDFFD2AC4476D008FD744 /* GuideMenuPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuideMenuPresenter.swift; sourceTree = "<group>"; };
C2EBE0012AC44787008FD744 /* GuideMenuFlowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuideMenuFlowController.swift; sourceTree = "<group>"; };
@ -3316,6 +3332,9 @@
C212ACDD2AF6F929001C8665 /* RootInteractor.swift */,
C2CACFBF2AFFB7F7001E0F8E /* LoginInteractor.swift */,
C2CACFC12B015725001E0F8E /* AppStateInteractor.swift */,
C22CD3972B9142CA005FE348 /* MDMInteractor.swift */,
C276D1702B9A672C008C9CD4 /* LocalNotificationStateInteractor.swift */,
C2EA6E5B2BA10AFE0034A964 /* LocalNotificationFetchInteractor.swift */,
);
path = Interactors;
sourceTree = "<group>";
@ -3738,6 +3757,15 @@
path = Architecture;
sourceTree = "<group>";
};
C22CD3902B913E85005FE348 /* MDMRepository */ = {
isa = PBXGroup;
children = (
C22CD3912B913E94005FE348 /* MDMRepository.swift */,
C22CD3932B913EB4005FE348 /* MDMRepositoryImpl.swift */,
);
path = MDMRepository;
sourceTree = "<group>";
};
C22CF3E027414028004F6A03 /* Log */ = {
isa = PBXGroup;
children = (
@ -5629,6 +5657,7 @@
C2A7067C276D1A5F00885D79 /* Flow */ = {
isa = PBXGroup;
children = (
C2BC3FC82B94E70F004C4BA0 /* NewPINNavigationFlowController.swift */,
C2A7067D276D1A7500885D79 /* NewPINFlowController.swift */,
C2997AA82454D767006D5943 /* SelectPINLengthController.swift */,
);
@ -6710,6 +6739,7 @@
C2D44E6B275C14840043F5D6 /* MainRepository */ = {
isa = PBXGroup;
children = (
C22CD3902B913E85005FE348 /* MDMRepository */,
C2A026B42AF82FDB00DB2E52 /* DataExternalTranslations.swift */,
C240485627652E170076376E /* DataTypes */,
C2D44E69275C14790043F5D6 /* MainRepository.swift */,
@ -6746,7 +6776,9 @@
C2DF245F29859F9700762A26 /* MainRepositoryImpl+ViewPath.swift */,
C2A026B22AF7C42500DB2E52 /* MainRepositoryImpl+TimeVerification.swift */,
C2CACFC32B015847001E0F8E /* MainRepositoryImpl+LockScreen.swift */,
C2EA6E5D2BA110FA0034A964 /* MainRepositoryImpl+LocalNotifications.swift */,
C249AF62278A40FE005F9D80 /* UserDefaults */,
C22CD3952B91419B005FE348 /* MainRepositoryImpl+MDM.swift */,
);
path = MainRepository;
sourceTree = "<group>";
@ -9004,6 +9036,7 @@
C2B1208F29D76BD10020281E /* AppearancePresenter+Menu.swift in Sources */,
C2A7945127F9016D00E5C641 /* BrowserExtensionEditNameViewController.swift in Sources */,
C242B4D12A9BD950005CC1BC /* AddingServiceAdvancedSectionDividerView.swift in Sources */,
C2BC3FC92B94E70F004C4BA0 /* NewPINNavigationFlowController.swift in Sources */,
C21B3A1427AF0BC9005C603B /* ColorPickerFlowController.swift in Sources */,
C2B39E5929F5BD7800EC31F6 /* TokensCategory.swift in Sources */,
C26BCE01281010A000CA6A9A /* SelectServiceModuleInteractor.swift in Sources */,
@ -9338,6 +9371,7 @@
C2E7C4052ADB2BB400478D89 /* ImportFromFileInteractor+Parsers.swift in Sources */,
C2E7C3E02ADB2BB400478D89 /* ImportFromFileInteractor.swift in Sources */,
C2E7C3C82ADB2B9C00478D89 /* MainRepositoryImpl+DeviceName.swift in Sources */,
C22CD3982B9142CA005FE348 /* MDMInteractor.swift in Sources */,
C2E7C3E22ADB2BB400478D89 /* InteractorFactory.swift in Sources */,
C2E7C3E92ADB2BB400478D89 /* AdvancedAlertInteractor.swift in Sources */,
C274C7722ADD3CA500B8AAC1 /* Generator.swift in Sources */,
@ -9354,6 +9388,7 @@
C2E7C4042ADB2BB400478D89 /* WebExtensionEncryptionInteractor.swift in Sources */,
C2AE5F512ADC199F00AED670 /* AppEventController.swift in Sources */,
C2E7C3CE2ADB2B9C00478D89 /* MainRepositoryImpl+Code.swift in Sources */,
C2EA6E5C2BA10AFE0034A964 /* LocalNotificationFetchInteractor.swift in Sources */,
C2E7C3DC2ADB2BAC00478D89 /* UserDefaultsRepository.swift in Sources */,
C2E7C4022ADB2BB400478D89 /* ServiceModifyInteractor.swift in Sources */,
C2AE5F552ADC1A4A00AED670 /* SecurityProtocol.swift in Sources */,
@ -9369,6 +9404,7 @@
C2AE5F802ADC8B0D00AED670 /* Code+Support.swift in Sources */,
C2E7C3D62ADB2B9C00478D89 /* MainRepositoryImpl+PushNotifications.swift in Sources */,
C2E7C40B2ADB2BE500478D89 /* SocialChannel.swift in Sources */,
C22CD3962B91419B005FE348 /* MainRepositoryImpl+MDM.swift in Sources */,
C2E7C3C32ADB2B9C00478D89 /* MainRepositoryImpl+News.swift in Sources */,
C2E7C3BC2ADB2B9C00478D89 /* MainRepositoryImpl+Guides.swift in Sources */,
C2E7C3C62ADB2B9C00478D89 /* MainRepositoryImpl+Cloud.swift in Sources */,
@ -9380,11 +9416,13 @@
C2E7C3CF2ADB2B9C00478D89 /* MainRepositoryImpl+ServiceDefinition.swift in Sources */,
C2E7C3D92ADB2B9C00478D89 /* MainRepositoryImpl+General.swift in Sources */,
C2E7C3DA2ADB2BA600478D89 /* MainRepositoryImpl+ViewPath.swift in Sources */,
C2EA6E5E2BA110FA0034A964 /* MainRepositoryImpl+LocalNotifications.swift in Sources */,
C2E7C3C72ADB2B9C00478D89 /* MainRepositoryImpl+Camera.swift in Sources */,
C2E7C4062ADB2BB400478D89 /* PairingWebExtensionInteractor.swift in Sources */,
C2E7C4012ADB2BB400478D89 /* TrashingServiceInteractor.swift in Sources */,
C2E7C3D82ADB2B9C00478D89 /* MainRepositoryImpl+Storage.swift in Sources */,
C2AE5F7E2ADC8B0D00AED670 /* Extensions.swift in Sources */,
C22CD3922B913E94005FE348 /* MDMRepository.swift in Sources */,
C2E7C3F72ADB2BB400478D89 /* IconInteractor.swift in Sources */,
C2E7C4122ADB2BE500478D89 /* ExchangeData2.swift in Sources */,
C2AE5F812ADC8B0D00AED670 /* Code+TwoFASWebExtension.swift in Sources */,
@ -9419,6 +9457,7 @@
C2AE5F522ADC19E600AED670 /* Notifications.swift in Sources */,
C2E7C3C22ADB2B9C00478D89 /* MainRepositoryImpl+AdvancedAlertState.swift in Sources */,
C2E7C4152ADB2BE500478D89 /* ExchangeData.swift in Sources */,
C22CD3942B913EB4005FE348 /* MDMRepositoryImpl.swift in Sources */,
C2E7C3F32ADB2BB400478D89 /* ViewPathInteractor.swift in Sources */,
C2E7C3FB2ADB2BB400478D89 /* ServiceListingInteractor.swift in Sources */,
C2A026B32AF7C42500DB2E52 /* MainRepositoryImpl+TimeVerification.swift in Sources */,
@ -9436,6 +9475,7 @@
C2CACFC22B015725001E0F8E /* AppStateInteractor.swift in Sources */,
C2AE5F692ADC5D9D00AED670 /* Security.swift in Sources */,
C2E7C3DD2ADB2BB400478D89 /* RegisterDeviceInteractor.swift in Sources */,
C276D1712B9A672C008C9CD4 /* LocalNotificationStateInteractor.swift in Sources */,
C2E7C40F2ADB2BE500478D89 /* PINType.swift in Sources */,
C2E7C3CB2ADB2B9C00478D89 /* MainRepositoryImpl+Appearance.swift in Sources */,
C2E7C3F62ADB2BB400478D89 /* WebExtensionAuthInteractor.swift in Sources */,
@ -9882,7 +9922,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
@ -9925,7 +9965,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
@ -9967,7 +10007,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
@ -10011,7 +10051,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
@ -10053,7 +10093,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
@ -10101,7 +10141,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
@ -10273,7 +10313,7 @@
"@executable_path/Frameworks",
);
MACH_O_TYPE = mh_execute;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGED_BINARY_TYPE = manual;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
OTHER_LDFLAGS = (
@ -10318,7 +10358,7 @@
"@executable_path/Frameworks",
);
MACH_O_TYPE = mh_execute;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGED_BINARY_TYPE = manual;
OTHER_LDFLAGS = (
"$(OTHER_LDFLAGS)",
@ -10365,7 +10405,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17";
@ -10412,7 +10452,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17";
@ -10455,7 +10495,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
@ -10499,7 +10539,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
@ -10544,7 +10584,7 @@
"@loader_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
@ -10589,7 +10629,7 @@
"@loader_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
MTL_FAST_MATH = YES;
@ -10626,7 +10666,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGED_BINARY_TYPE = manual;
PRODUCT_BUNDLE_IDENTIFIER = com.twofas.org.TwoFASAuth;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -10660,7 +10700,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGED_BINARY_TYPE = manual;
PRODUCT_BUNDLE_IDENTIFIER = com.twofas.org.TwoFASAuth;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -10699,7 +10739,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
@ -10744,7 +10784,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
@ -10842,7 +10882,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGED_BINARY_TYPE = manual;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
@ -10878,7 +10918,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGED_BINARY_TYPE = manual;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.twofas.org.TwoFASWidget;
@ -10911,7 +10951,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGED_BINARY_TYPE = manual;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
@ -10945,7 +10985,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGED_BINARY_TYPE = manual;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.twofas.org.TwoFASServiceIntent;
@ -11042,7 +11082,7 @@
"@loader_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
@ -11089,7 +11129,7 @@
"@loader_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
MTL_FAST_MATH = YES;
@ -11134,7 +11174,7 @@
"@loader_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
@ -11181,7 +11221,7 @@
"@loader_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
MTL_FAST_MATH = YES;
@ -11226,7 +11266,7 @@
"@loader_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
@ -11274,7 +11314,7 @@
"@loader_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
@ -11320,7 +11360,7 @@
"@loader_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
@ -11368,7 +11408,7 @@
"@loader_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.5;
MERGEABLE_LIBRARY = NO;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";

View File

@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "f91c8167141d0279726c6f6d9d4a47c026785cbc",
"version" : "10.21.0"
"revision" : "fe09d61a539e11fdbe24f269bba10144b6145fe2",
"version" : "10.22.0"
}
},
{
@ -41,8 +41,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "cb8617fab75d181270a1d8f763f26b15c73e2e1e",
"version" : "10.21.0"
"revision" : "bf3bb24f6b60a7acedaef504e9ce97154203217a",
"version" : "10.22.0"
}
},
{
@ -50,8 +50,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "a732a4b47f59e4f725a2ea10f0c77e93a7131117",
"version" : "9.3.0"
"revision" : "a637d318ae7ae246b02d7305121275bc75ed5565",
"version" : "9.4.0"
}
},
{
@ -59,8 +59,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "bc27fad73504f3d4af235de451f02ee22586ebd3",
"version" : "7.12.1"
"revision" : "830ffa9276e10267881f2697283c2fcd867603fd",
"version" : "7.13.0"
}
},
{
@ -113,8 +113,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "9d108e9112aa1d65ce508facf804674546116d9c",
"version" : "1.22.3"
"revision" : "43aaef65e0c665daadf848761d560e446d350d3d",
"version" : "1.22.4"
}
},
{
@ -122,8 +122,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692",
"version" : "2.30909.0"
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
"version" : "2.30910.0"
}
},
{
@ -140,8 +140,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e",
"version" : "2.3.1"
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
"version" : "2.4.0"
}
},
{

View File

@ -50,6 +50,12 @@ extension UIViewController {
definesPresentationContext = true
}
func configureAsFullscreenModal() {
modalPresentationStyle = .fullScreen
isModalInPresentation = true
definesPresentationContext = true
}
func setCustomLeftBackButton() {
navigationItem.leftBarButtonItem = createCustomLeftBackButton()
}

View File

@ -28,6 +28,7 @@ final class PasswordTextField: LimitedTextField {
var isActive: Callback?
var textDidChange: TextDidChange?
var didResign: Callback?
var verifyPassword = true
override func textField(
_ textField: UITextField,
@ -43,13 +44,13 @@ final class PasswordTextField: LimitedTextField {
let matches = string.matches(ExportFileRules.regExp) || string.isBackspace
if !matches {
if !matches && verifyPassword {
notAllowedCharacter?()
} else {
textDidChange?(newString as String)
}
return matches
return matches || !verifyPassword
}
override func resignFirstResponder() -> Bool {

View File

@ -51,6 +51,16 @@ final class RevealPasswordInput: UIView {
input.text ?? ""
}
var verifyPassword: Bool {
get {
input.verifyPassword
}
set {
input.verifyPassword = newValue
}
}
private let lineHeight: CGFloat = 25
private let titleLabel = TitleLabel()

View File

@ -32,7 +32,8 @@ final class ModuleInteractorFactory {
registerDeviceInteractor: InteractorFactory.shared.registerDeviceInteractor(),
appStateInteractor: InteractorFactory.shared.appStateInteractor(),
notificationInteractor: InteractorFactory.shared.notificationInteractor(),
widgetsInteractor: InteractorFactory.shared.widgetsInteractor()
widgetsInteractor: InteractorFactory.shared.widgetsInteractor(),
localNotificationStateInteractor: InteractorFactory.shared.localNotificationStateInteractor()
)
}
@ -46,14 +47,16 @@ final class ModuleInteractorFactory {
pushNotifications: InteractorFactory.shared.pushNotificationRegistrationInteractor(),
protectionInteractor: InteractorFactory.shared.protectionInteractor(),
networkStatusInteractor: InteractorFactory.shared.networkStatusInteractor(),
pairingDeviceInteractor: InteractorFactory.shared.pairingWebExtensionInteractor()
pairingDeviceInteractor: InteractorFactory.shared.pairingWebExtensionInteractor(),
mdmInteractor: InteractorFactory.shared.mdmInteractor()
)
}
func backupMenuModuleInteractor() -> BackupMenuModuleInteracting {
BackupMenuModuleInteractor(
serviceListingInteractor: InteractorFactory.shared.serviceListingInteractor(),
cloudBackup: InteractorFactory.shared.cloudBackupStateInteractor(listenerID: "BackupMenuModuleInteractor")
cloudBackup: InteractorFactory.shared.cloudBackupStateInteractor(listenerID: "BackupMenuModuleInteractor"),
mdmInteractor: InteractorFactory.shared.mdmInteractor()
)
}
@ -122,13 +125,15 @@ final class ModuleInteractorFactory {
func appSecurityModuleInteractor() -> AppSecurityModuleInteracting {
AppSecurityModuleInteractor(
protectionInteractor: InteractorFactory.shared.protectionInteractor(),
appLockStateInteractor: InteractorFactory.shared.appLockStateInteractor()
appLockStateInteractor: InteractorFactory.shared.appLockStateInteractor(),
mdmInteractor: InteractorFactory.shared.mdmInteractor()
)
}
func appLockModuleInteractor() -> AppLockModuleInteracting {
AppLockModuleInteractor(
appLockInteractor: InteractorFactory.shared.appLockStateInteractor()
appLockInteractor: InteractorFactory.shared.appLockStateInteractor(),
mdmInteractor: InteractorFactory.shared.mdmInteractor()
)
}
@ -140,8 +145,8 @@ final class ModuleInteractorFactory {
)
}
func newPINModuleInteractor() -> NewPINModuleInteracting {
NewPINModuleInteractor()
func newPINModuleInteractor(lockNavigation: Bool) -> NewPINModuleInteracting {
NewPINModuleInteractor(lockNavigation: lockNavigation)
}
func trashModuleInteractor() -> TrashModuleInteracting {
@ -161,7 +166,8 @@ final class ModuleInteractorFactory {
func cameraScannerModuleInteractor() -> CameraScannerModuleInteracting {
CameraScannerModuleInteractor(
newCodeInteractor: InteractorFactory.shared.newCodeInteractor(),
pushNotificationPermission: InteractorFactory.shared.pushNotificationRegistrationInteractor()
pushNotificationPermission: InteractorFactory.shared.pushNotificationRegistrationInteractor(),
cameraPermissionInteractor: InteractorFactory.shared.cameraPermissionInteractor()
)
}
@ -198,7 +204,8 @@ final class ModuleInteractorFactory {
sectionInteractor: InteractorFactory.shared.sectionInteractor(),
notificationsInteractor: InteractorFactory.shared.notificationInteractor(),
serviceDefinitionInteractor: InteractorFactory.shared.serviceDefinitionInteractor(),
advancedAlertInteractor: InteractorFactory.shared.advancedAlertInteractor()
advancedAlertInteractor: InteractorFactory.shared.advancedAlertInteractor(),
mdmInteractor: InteractorFactory.shared.mdmInteractor()
)
}
@ -276,7 +283,10 @@ final class ModuleInteractorFactory {
}
func newsModuleInteractor() -> NewsModuleInteracting {
NewsModuleInteractor(newsInteractor: InteractorFactory.shared.newsInteractor())
NewsModuleInteractor(
newsInteractor: InteractorFactory.shared.newsInteractor(),
localNotificationFetchInteractor: InteractorFactory.shared.localNotificationFetchInteractor()
)
}
func composeServiceCategorySelectionModuleInteractor(
@ -318,7 +328,8 @@ final class ModuleInteractorFactory {
widgetsInteractor: InteractorFactory.shared.widgetsInteractor(),
newCodeInteractor: InteractorFactory.shared.newCodeInteractor(),
newsInteractor: InteractorFactory.shared.newsInteractor(),
rootInteractor: InteractorFactory.shared.rootInteractor()
rootInteractor: InteractorFactory.shared.rootInteractor(),
localNotificationFetchInteractor: InteractorFactory.shared.localNotificationFetchInteractor()
)
}
@ -331,7 +342,9 @@ final class ModuleInteractorFactory {
newVersionInteractor: InteractorFactory.shared.newVersionInteractor(),
networkStatusInteractor: InteractorFactory.shared.networkStatusInteractor(),
appInfoInteractor: InteractorFactory.shared.appInfoInteractor(),
rootInteractor: InteractorFactory.shared.rootInteractor()
rootInteractor: InteractorFactory.shared.rootInteractor(),
mdmInteractor: InteractorFactory.shared.mdmInteractor(),
protectionInteractor: InteractorFactory.shared.protectionInteractor()
)
}

View File

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

View File

@ -24,7 +24,7 @@ extension SocialChannel {
var url: URL {
switch self {
case .discord:
return URL(string: "https://discord.gg/q4cP6qh2g5")!
return URL(string: "https://2fas.com/discord")!
case .youtube:
return URL(string: "https://www.youtube.com/@2fas")!
case .twitter:

View File

@ -112,6 +112,7 @@ internal enum Asset {
internal static let navibarNewsIconBadge = ImageAsset(name: "NavibarNewsIconBadge")
internal static let notificationFeatures = ImageAsset(name: "NotificationFeatures")
internal static let notificationNews = ImageAsset(name: "NotificationNews")
internal static let notificationTips = ImageAsset(name: "NotificationTips")
internal static let notificationUpdates = ImageAsset(name: "NotificationUpdates")
internal static let notificationYoutube = ImageAsset(name: "NotificationYoutube")
internal static let openGallery = ImageAsset(name: "OpenGallery")

View File

@ -44,7 +44,7 @@ protocol RootModuleInteracting: AnyObject {
)
func lockScreenActive()
func lockScreenInactive()
func lockScreenInactive()
}
final class RootModuleInteractor {
@ -57,6 +57,7 @@ final class RootModuleInteractor {
private let appStateInteractor: AppStateInteracting
private let notificationInteractor: NotificationInteracting
private let widgetsInteractor: WidgetsInteracting
private let localNotificationStateInteractor: LocalNotificationStateInteracting
init(
rootInteractor: RootInteracting,
@ -65,7 +66,8 @@ final class RootModuleInteractor {
registerDeviceInteractor: RegisterDeviceInteracting,
appStateInteractor: AppStateInteracting,
notificationInteractor: NotificationInteracting,
widgetsInteractor: WidgetsInteracting
widgetsInteractor: WidgetsInteracting,
localNotificationStateInteractor: LocalNotificationStateInteracting
) {
self.rootInteractor = rootInteractor
self.linkInteractor = linkInteractor
@ -74,6 +76,7 @@ final class RootModuleInteractor {
self.appStateInteractor = appStateInteractor
self.notificationInteractor = notificationInteractor
self.widgetsInteractor = widgetsInteractor
self.localNotificationStateInteractor = localNotificationStateInteractor
rootInteractor.storageError = { [weak self] error in
self?.storageError?(error)
@ -94,7 +97,7 @@ extension RootModuleInteractor: RootModuleInteracting {
rootInteractor.initializeApp()
registerDeviceInteractor.initialize()
}
func lockApplicationIfNeeded(presentLoginImmediately: @escaping () -> Void) {
rootInteractor.lockApplicationIfNeeded(
presentLoginImmediately: presentLoginImmediately
@ -120,6 +123,7 @@ extension RootModuleInteractor: RootModuleInteracting {
didCopyToken()
}
rootInteractor.applicationDidBecomeActive()
localNotificationStateInteractor.activate()
}
func shouldHandleURL(url: URL) -> Bool {

View File

@ -44,6 +44,7 @@ protocol CameraScannerFlowControlling: AnyObject {
func toPushPermissions(extensionID: ExtensionID)
func toRename(currentName: String, secret: String)
func toServiceWasCreated(serviceData: ServiceData)
func toCameraNotAvailable()
}
final class CameraScannerFlowController: FlowController {
@ -189,6 +190,11 @@ extension CameraScannerFlowController: CameraScannerFlowControlling {
viewController.present(alert, animated: true, completion: nil)
}
func toCameraNotAvailable() {
let ac = AlertController.cameraNotAvailable
viewController.present(ac, animated: true)
}
func toPushPermissions(extensionID: ExtensionID) {
guard let navi = viewController.navigationController else { return }
navi.setNavigationBarHidden(true, animated: false)

View File

@ -26,6 +26,10 @@ protocol CameraScannerModuleInteracting: AnyObject {
var shouldRename: ((String, String) -> Void)? { get set }
var wasUserAskedAboutPush: Bool { get }
func isCameraAvailable() -> Bool
func isCameraAllowed() -> Bool
func registerCamera(callback: @escaping (Bool) -> Void)
func addCode(_ code: Code, force: Bool)
func codeExists(_ code: Code) -> Bool
func filterImportableCodes(_ codes: [Code]) -> [Code]
@ -38,19 +42,39 @@ protocol CameraScannerModuleInteracting: AnyObject {
final class CameraScannerModuleInteractor {
private let newCodeInteractor: NewCodeInteracting
private let pushNotificationPermission: PushNotificationRegistrationInteracting
private let cameraPermissionInteractor: CameraPermissionInteracting
var serviceWasCreated: ((ServiceData) -> Void)?
var shouldRename: ((String, String) -> Void)?
init(newCodeInteractor: NewCodeInteracting, pushNotificationPermission: PushNotificationRegistrationInteracting) {
init(
newCodeInteractor: NewCodeInteracting,
pushNotificationPermission: PushNotificationRegistrationInteracting,
cameraPermissionInteractor: CameraPermissionInteracting
) {
self.newCodeInteractor = newCodeInteractor
self.pushNotificationPermission = pushNotificationPermission
self.cameraPermissionInteractor = cameraPermissionInteractor
newCodeInteractor.serviceWasCreated = { [weak self] in self?.serviceWasCreated?($0) }
newCodeInteractor.shouldRename = { [weak self] in self?.shouldRename?($0, $1) }
}
}
extension CameraScannerModuleInteractor: CameraScannerModuleInteracting {
func isCameraAvailable() -> Bool {
cameraPermissionInteractor.isCameraAvailable
}
func isCameraAllowed() -> Bool {
cameraPermissionInteractor.isCameraAllowed
}
func registerCamera(callback: @escaping (Bool) -> Void) {
cameraPermissionInteractor.register { status in
callback(status == .granted)
}
}
var wasUserAskedAboutPush: Bool {
pushNotificationPermission.wasUserAsked
}

View File

@ -39,6 +39,20 @@ final class CameraScannerPresenter {
}
extension CameraScannerPresenter {
func viewDidAppear() {
if interactor.isCameraAvailable() {
if !interactor.isCameraAllowed() {
interactor.registerCamera { [weak self] isGranted in
if !isGranted {
self?.handleCameraNotAvailable()
}
}
}
} else {
handleCameraNotAvailable()
}
}
func handleOpenGallery() {
lockScanning = true
flowController.toGallery()
@ -137,6 +151,12 @@ extension CameraScannerPresenter {
}
}
func handleCameraNotAvailable() {
view?.feedback()
flowController.toCameraNotAvailable()
}
func handleCameraError(_ str: String) {
view?.enableOverlay()
view?.feedback()

View File

@ -66,6 +66,11 @@ final class CameraScannerViewController: UIViewController {
navigationController?.setNavigationBarHidden(true, animated: false)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
presenter.viewDidAppear()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

View File

@ -83,6 +83,10 @@ protocol ComposeServiceModuleInteracting: AnyObject {
var actionType: ComposeServiceModuleInteractorActionType { get }
var privateKeyError: ComposeServiceModuleInteractorPrivateKeyError? { get }
var isBrowserExtensionAllowed: Bool { get }
var isSecretCopyingBlocked: Bool { get }
var isDataCorrectNotifier: ((Bool) -> Void)? { get set }
var serviceWasCreated: ((ServiceData) -> Void)? { get set }
@ -138,6 +142,10 @@ final class ComposeServiceModuleInteractor {
private(set) var digits: Digits = .defaultValue
private(set) var counter: Int?
var isSecretCopyingBlocked: Bool {
mdmInteractor.isBackupBlocked
}
// MARK: Section
private var sectionState: ComposeServiceModuleInteractorSectionState = .none
@ -158,6 +166,7 @@ final class ComposeServiceModuleInteractor {
private let notificationsInteractor: NotificationInteracting
private let serviceDefinitionInteractor: ServiceDefinitionInteracting
private let advancedAlertInteractor: AdvancedAlertInteracting
private let mdmInteractor: MDMInteracting
private(set) var serviceData: ServiceData?
@ -170,7 +179,8 @@ final class ComposeServiceModuleInteractor {
sectionInteractor: SectionInteracting,
notificationsInteractor: NotificationInteracting,
serviceDefinitionInteractor: ServiceDefinitionInteracting,
advancedAlertInteractor: AdvancedAlertInteracting
advancedAlertInteractor: AdvancedAlertInteracting,
mdmInteractor: MDMInteracting
) {
self.modifyInteractor = modifyInteractor
self.trashingServiceInteractor = trashingServiceInteractor
@ -181,6 +191,7 @@ final class ComposeServiceModuleInteractor {
self.notificationsInteractor = notificationsInteractor
self.serviceDefinitionInteractor = serviceDefinitionInteractor
self.advancedAlertInteractor = advancedAlertInteractor
self.mdmInteractor = mdmInteractor
updateValues()
}
@ -211,6 +222,10 @@ extension ComposeServiceModuleInteractor: ComposeServiceModuleInteracting {
serviceDefinitionInteractor.name(for: iconTypeID) ?? ""
}
var isBrowserExtensionAllowed: Bool {
!mdmInteractor.isBrowserExtensionBlocked
}
func setServiceName(_ newServiceName: String?) {
serviceName = newServiceName
validate()

View File

@ -53,6 +53,7 @@ struct ComposeServiceSectionCell: Hashable {
enum PrivateKeyKind: Hashable {
case empty
case hidden
case hiddenNonCopyable
}
enum PrivateKeyError: Hashable {

View File

@ -34,6 +34,9 @@ extension ComposeServicePresenter {
}()
let privateKeyKind: ComposeServiceSectionCell.PrivateKeyConfig.PrivateKeyKind = {
if interactor.actionType == .edit {
if interactor.isSecretCopyingBlocked {
return .hiddenNonCopyable
}
return .hidden
}
return .empty
@ -118,7 +121,9 @@ extension ComposeServicePresenter {
]
if interactor.actionType == .edit {
array.append(webExtension)
if interactor.isBrowserExtensionAllowed {
array.append(webExtension)
}
array.append(remove)
}

View File

@ -37,6 +37,8 @@ final class ComposeServiceFormReveal: UIView {
return button
}()
private var stack: ComposeServiceFormRow?
var buttonPressed: Callback?
enum State {
@ -64,10 +66,16 @@ final class ComposeServiceFormReveal: UIView {
addSubview(stack)
stack.pinToParent()
self.stack = stack
changeState(newState: .masked)
}
func removeActionButton() {
stack?.removeArrangedSubviews()
stack?.addArrangedSubview(privateKey)
}
func changeState(newState: State) {
switch newState {
case .masked:

View File

@ -133,6 +133,10 @@ final class ComposeServicePrivateKeyCell: UITableViewCell, ComposeServiceInputCe
case .hidden:
privateKeyInput.isHidden = true
privateKeyReveal.isHidden = false
case .hiddenNonCopyable:
privateKeyInput.isHidden = true
privateKeyReveal.isHidden = false
privateKeyReveal.removeActionButton()
}
}

View File

@ -28,4 +28,5 @@ enum ComposeServiceInputKind: String {
enum ComposeServicePrivateKeyKind {
case empty
case hidden
case hiddenNonCopyable
}

View File

@ -55,8 +55,9 @@ extension ComposeServiceViewController {
) as? ComposeServicePrivateKeyCell else { return UITableViewCell() }
let kind: ComposeServicePrivateKeyKind = {
switch config.privateKeyKind {
case .empty: return .empty
case .hidden: return .hidden
case .empty: .empty
case .hidden: .hidden
case .hiddenNonCopyable: .hiddenNonCopyable
}
}()
cell.configure(privateKeyKind: kind)

View File

@ -30,6 +30,7 @@ protocol MainFlowControlling: AnyObject {
func toSecretSyncError(_ serviceName: String)
func toOpenFileImport(url: URL)
func toSetupSplit()
func toSetPIN()
// MARK: - App update
func toShowNewVersionAlert(for appStoreURL: URL, skip: @escaping Callback)
@ -137,6 +138,12 @@ extension MainFlowController: MainFlowControlling {
viewController.present(alertController, animated: true, completion: nil)
}
// MARK: - MDM requriments
func toSetPIN() {
NewPINNavigationFlowController.present(on: viewController, parent: self)
}
}
extension MainFlowController: ImporterOpenFileHeadlessFlowControllerParent {
@ -200,4 +207,17 @@ extension MainFlowController: MainSplitFlowControllerParent {
func navigationSwitchedToSettingsExternalImport() {
viewController.presenter.handleSwitchToExternalImport()
}
func navigationSwitchedToSettingsBackup() {
viewController.presenter.handleSwitchToBackup()
}
}
extension MainFlowController: NewPINNavigationFlowControllerParent {
func pinGathered(with PIN: String, pinType: PINType) {
viewController.presenter.handleSavePIN(PIN, pinType: pinType)
viewController.dismiss(animated: true) { [weak viewController] in
viewController?.presenter.handleViewIsVisible()
}
}
}

View File

@ -23,11 +23,18 @@ import Data
protocol MainModuleInteracting: AnyObject {
var secretSyncError: ((String) -> Void)? { get set }
var isAppLocked: Bool { get }
var isBrowserExtensionAllowed: Bool { get }
var shouldSetPasscode: Bool { get }
func applyMDMRules()
func initialize()
func checkForImport() -> URL?
func clearImportedFileURL()
func savePIN(_ PIN: String, ofType pinType: PINType)
func saveSuccessSync()
func clearSavesuccessSync()
// MARK: - New app version
func checkForNewAppVersion(completion: @escaping (URL?) -> Void)
func skipAppVersion()
@ -40,6 +47,14 @@ final class MainModuleInteractor {
rootInteractor.isAuthenticationRequired
}
var isBrowserExtensionAllowed: Bool {
!mdmInteractor.isBrowserExtensionBlocked
}
var shouldSetPasscode: Bool {
mdmInteractor.shouldSetPasscode
}
private let logUploadingInteractor: LogUploadingInteracting
private let cloudBackupStateInteractor: CloudBackupStateInteracting
private let fileInteractor: FileInteracting
@ -47,6 +62,8 @@ final class MainModuleInteractor {
private let networkStatusInteractor: NetworkStatusInteracting
private let appInfoInteractor: AppInfoInteracting
private let rootInteractor: RootInteracting
private let mdmInteractor: MDMInteracting
private let protectionInteractor: ProtectionInteracting
init(
logUploadingInteractor: LogUploadingInteracting,
@ -56,7 +73,9 @@ final class MainModuleInteractor {
newVersionInteractor: NewVersionInteracting,
networkStatusInteractor: NetworkStatusInteracting,
appInfoInteractor: AppInfoInteracting,
rootInteractor: RootInteracting
rootInteractor: RootInteracting,
mdmInteractor: MDMInteracting,
protectionInteractor: ProtectionInteracting
) {
self.logUploadingInteractor = logUploadingInteractor
self.cloudBackupStateInteractor = cloudBackupStateInteractor
@ -65,6 +84,8 @@ final class MainModuleInteractor {
self.networkStatusInteractor = networkStatusInteractor
self.appInfoInteractor = appInfoInteractor
self.rootInteractor = rootInteractor
self.mdmInteractor = mdmInteractor
self.protectionInteractor = protectionInteractor
cloudBackupStateInteractor.secretSyncError = { [weak self] in self?.secretSyncError?($0) }
}
@ -85,6 +106,22 @@ extension MainModuleInteractor: MainModuleInteracting {
fileInteractor.markAsHandled()
}
func applyMDMRules() {
mdmInteractor.apply()
}
func savePIN(_ PIN: String, ofType pinType: PINType) {
protectionInteractor.savePIN(PIN, typeOfPIN: pinType)
}
func saveSuccessSync() {
cloudBackupStateInteractor.saveSuccessSyncDate()
}
func clearSavesuccessSync() {
cloudBackupStateInteractor.clearSaveSuccessSync()
}
// MARK: - New app version
func checkForNewAppVersion(completion: @escaping (URL?) -> Void) {

View File

@ -18,6 +18,7 @@
//
import UIKit
import Data
final class MainPresenter {
weak var view: MainViewControlling?
@ -42,7 +43,7 @@ final class MainPresenter {
func viewWillAppear() {
viewIsVisible()
}
func handleSwitchToSetupPIN() {
view?.navigateToViewPath(.settings(option: .security))
}
@ -55,6 +56,10 @@ final class MainPresenter {
view?.navigateToViewPath(.settings(option: .externalImport))
}
func handleSwitchToBackup() {
view?.navigateToViewPath(.settings(option: .backup))
}
func handleSwitchedToSettings() {
view?.settingsTabActive()
}
@ -65,11 +70,11 @@ final class MainPresenter {
func handleRefreshAuthList() {
guard !interactor.isAppLocked else { return }
flowController.toAuthRequestFetch()
handleAuthRequest()
}
func handleAuthorize(for tokenRequestID: String) {
guard !interactor.isAppLocked else { return }
guard !interactor.isAppLocked && interactor.isBrowserExtensionAllowed else { return }
flowController.toAuthorize(for: tokenRequestID)
}
@ -82,13 +87,28 @@ final class MainPresenter {
func handleViewIsVisible() {
viewIsVisible()
}
func handleSyncCompletedSuccessfuly() {
interactor.saveSuccessSync()
}
func handleClearSyncCompletedSuccessfuly() {
interactor.clearSavesuccessSync()
}
func handleSavePIN(_ PIN: String, pinType: PINType) {
interactor.savePIN(PIN, ofType: pinType)
}
}
private extension MainPresenter {
func viewIsVisible() {
guard !interactor.isAppLocked && !handlingViewIsVisible else { return }
handlingViewIsVisible = true
if let url = interactor.checkForImport() {
interactor.applyMDMRules()
if interactor.shouldSetPasscode {
flowController.toSetPIN()
} else if let url = interactor.checkForImport() {
flowController.toOpenFileImport(url: url)
interactor.clearImportedFileURL()
handlingViewIsVisible = false
@ -113,6 +133,8 @@ private extension MainPresenter {
}
func handleAuthRequest() {
flowController.toAuthRequestFetch()
if interactor.isBrowserExtensionAllowed {
flowController.toAuthRequestFetch()
}
}
}

View File

@ -30,6 +30,7 @@ final class MainViewController: UIViewController {
var presenter: MainPresenter!
private let settingsEventController = SettingsEventController()
private let notificationCenter = NotificationCenter.default
var splitView: MainSplitViewController?
@ -48,66 +49,78 @@ final class MainViewController: UIViewController {
}
deinit {
NotificationCenter.default.removeObserver(self)
notificationCenter.removeObserver(self)
}
}
extension MainViewController {
private func setupEvents() {
NotificationCenter.default.addObserver(
notificationCenter.addObserver(
self,
selector: #selector(refreshAuthList),
name: .pushNotificationRefreshAuthList,
object: nil
)
NotificationCenter.default.addObserver(
notificationCenter.addObserver(
self,
selector: #selector(refreshAuthList),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
NotificationCenter.default.addObserver(
notificationCenter.addObserver(
self,
selector: #selector(clearAuthList),
name: UIApplication.willResignActiveNotification,
object: nil
)
NotificationCenter.default.addObserver(
notificationCenter.addObserver(
self,
selector: #selector(authorizeFromApp),
name: .pushNotificationAuthorizeFromApp,
object: nil
)
NotificationCenter.default.addObserver(
notificationCenter.addObserver(
self,
selector: #selector(switchToSetupPIN),
name: .switchToSetupPIN,
object: nil
)
NotificationCenter.default.addObserver(
notificationCenter.addObserver(
self,
selector: #selector(switchToBrowserExtension),
name: .switchToBrowserExtension,
object: nil
)
NotificationCenter.default.addObserver(
notificationCenter.addObserver(
self,
selector: #selector(fileAwaitsOpening),
name: .fileAwaitsOpening,
object: nil
)
NotificationCenter.default.addObserver(
notificationCenter.addObserver(
self,
selector: #selector(tokensVisible),
name: .tokensScreenIsVisible,
object: nil
)
NotificationCenter.default.addObserver(
notificationCenter.addObserver(
self,
selector: #selector(tokensVisible),
name: .userLoggedIn,
object: nil
)
notificationCenter.addObserver(
self,
selector: #selector(syncCompletedSuccessfuly),
name: .syncCompletedSuccessfuly,
object: nil
)
notificationCenter.addObserver(
self,
selector: #selector(clearSyncCompletedSuccessfuly),
name: .clearSyncCompletedSuccessfuly,
object: nil
)
}
@objc
@ -151,6 +164,16 @@ extension MainViewController {
private func tokensVisible() {
presenter.handleViewIsVisible()
}
@objc
private func syncCompletedSuccessfuly() {
presenter.handleSyncCompletedSuccessfuly()
}
@objc
private func clearSyncCompletedSuccessfuly() {
presenter.handleClearSyncCompletedSuccessfuly()
}
}
extension MainViewController: MainViewControlling {

View File

@ -24,6 +24,7 @@ protocol MainSplitFlowControllerParent: AnyObject {
func navigationSwitchedToTokens()
func navigationSwitchedToSettings()
func navigationSwitchedToSettingsExternalImport()
func navigationSwitchedToSettingsBackup()
}
protocol MainSplitFlowControlling: AnyObject {
@ -175,6 +176,10 @@ extension MainSplitFlowController: TokensPlainFlowControllerParent {
func tokensSwitchToSettingsExternalImport() {
parent?.navigationSwitchedToSettingsExternalImport()
}
func tokensSwitchToSettingsBackup() {
parent?.navigationSwitchedToSettingsBackup()
}
}
extension MainSplitFlowController: SettingsFlowControllerParent {

View File

@ -22,6 +22,7 @@ import Common
protocol NewsNavigationFlowControllerParent: AnyObject {
func newsClose()
func newsToBackup()
}
final class NewsNavigationFlowController: NavigationFlowController {
@ -48,4 +49,8 @@ extension NewsNavigationFlowController: NewsPlainFlowControllerParent {
func newsClose() {
parent?.newsClose()
}
func newsToBackup() {
parent?.newsToBackup()
}
}

View File

@ -18,14 +18,17 @@
//
import UIKit
import Common
protocol NewsPlainFlowControllerParent: AnyObject {
func newsClose()
func newsToBackup()
}
protocol NewsPlainFlowControlling: AnyObject {
func openWeb(with url: URL)
func toClose()
func toInternalLink(_ internalLink: ListNewsEntry.InternalLink)
}
final class NewsPlainFlowController: FlowController {
@ -63,4 +66,11 @@ extension NewsPlainFlowController: NewsPlainFlowControlling {
func toClose() {
parent?.newsClose()
}
func toInternalLink(_ internalLink: ListNewsEntry.InternalLink) {
switch internalLink {
case .backup:
parent?.newsToBackup()
}
}
}

View File

@ -24,7 +24,6 @@ import Data
protocol NewsModuleInteracting: AnyObject {
var hasUnreadNews: Bool { get }
func localList() -> [ListNewsEntry]
func fetchList(completion: @escaping ([ListNewsEntry]) -> Void)
func markAsRead(newsEntry: ListNewsEntry)
func markAllAsRead()
@ -32,9 +31,11 @@ protocol NewsModuleInteracting: AnyObject {
final class NewsModuleInteractor {
private let newsInteractor: NewsInteracting
init(newsInteractor: NewsInteracting) {
private let localNotificationFetchInteractor: LocalNotificationFetchInteracting
init(newsInteractor: NewsInteracting, localNotificationFetchInteractor: LocalNotificationFetchInteracting) {
self.newsInteractor = newsInteractor
self.localNotificationFetchInteractor = localNotificationFetchInteractor
}
}
@ -43,22 +44,88 @@ extension NewsModuleInteractor: NewsModuleInteracting {
newsInteractor.hasUnreadNews
}
func localList() -> [ListNewsEntry] {
newsInteractor.localList()
}
func fetchList(completion: @escaping ([ListNewsEntry]) -> Void) {
newsInteractor.fetchList(completion: { [weak self] in
guard let self else { return }
completion(self.newsInteractor.localList())
})
newsInteractor.fetchList { [weak self] in
self?.localNotificationFetchInteractor.getNotification { [weak self] notification in
guard let self else { return }
let list = self.prepareList(news: self.newsInteractor.localList(), notification: notification)
completion(list)
}
}
}
func markAsRead(newsEntry: ListNewsEntry) {
newsInteractor.markAsRead(newsEntry: newsEntry)
let localList = newsInteractor.localList()
if localList.contains(newsEntry) {
newsInteractor.markAsRead(newsEntry: newsEntry)
} else {
localNotificationFetchInteractor.markNotificationAsRead()
}
}
func markAllAsRead() {
localNotificationFetchInteractor.markNotificationAsRead()
newsInteractor.clearHasUnreadNews()
}
}
private extension NewsModuleInteractor {
func prepareList(news: [ListNewsEntry], notification: LocalNotification?) -> [ListNewsEntry] {
guard let notification else {
return news.sorted
}
let notificationNewsEntry = notification.toNewsEntry()
var completeList = news
completeList.append(notificationNewsEntry)
return completeList.sorted
}
}
extension LocalNotification {
func toNewsEntry() -> ListNewsEntry {
let icon: ListNewsEntry.Icon
let link: URL?
let message: String
let internalLink: ListNewsEntry.InternalLink?
switch self.kind {
case .tipsNTricks:
icon = .tips
link = URL(string: "https://2fas.com/2fasauth-tutorial")
message = T.periodicNotificationTips
internalLink = nil
case .backup:
icon = .updates
link = nil
message = T.periodicNotificationBackup
internalLink = .backup
case .browserExtension:
icon = .news
link = URL(string: "https://2fas.com/browser-extension/")
message = T.periodicNotificationBrowserExtension
internalLink = nil
case .donation:
icon = .features
link = URL(string: "https://2fas.com/donate/")
message = T.periodicNotificationDonate
internalLink = nil
}
return .init(
newsID: self.id,
icon: icon,
link: link,
message: message,
publishedAt: self.publishedAt,
createdAt: nil,
wasRead: self.wasRead,
internalLink: internalLink,
localNotificationType: kind.rawValue
)
}
}
extension Array where Element == ListNewsEntry {
var sorted: Self {
sorted(by: { $0.publishedAt <= $1.publishedAt })
}
}

View File

@ -45,6 +45,8 @@ extension ListNewsEntry.Icon {
.withRenderingMode(.alwaysTemplate)
case .youtube: return Asset.notificationYoutube.image
.withRenderingMode(.alwaysTemplate)
case .tips: return Asset.notificationTips.image
.withRenderingMode(.alwaysTemplate)
}
}
}

View File

@ -47,16 +47,27 @@ final class NewsPresenter {
func viewWillAppear() {
refreshView()
}
func handleRefreshView() {
refreshView()
}
func handleSelection(at row: Int) {
guard let entry = interactor.localList()[safe: row], let link = entry.link else { return }
interactor.markAsRead(newsEntry: entry)
AppEventLog(.articleRead(entry.newsID))
flowController.openWeb(with: link)
interactor.fetchList { [weak self] list in
guard let entry = list[safe: row] else { return }
self?.interactor.markAsRead(newsEntry: entry)
if let type = entry.localNotificationType {
AppEventLog(.localNotificationRead(type))
} else {
AppEventLog(.articleRead(entry.newsID))
}
if let internalLink = entry.internalLink {
self?.flowController.toInternalLink(internalLink)
} else if let link = entry.link {
self?.flowController.openWeb(with: link)
}
}
}
func close() {
@ -79,25 +90,25 @@ private extension NewsPresenter {
func reload() {
let now = Date()
let cells = interactor
.localList()
.map { entry in
interactor.fetchList { [weak self] news in
let cells = news.map { entry in
NewsCell(
icon: entry.icon.image,
title: entry.message ?? entry.link?.absoluteString ?? "",
wasRead: entry.wasRead,
publishedAgo: dateFormatter.localizedString(for: entry.publishedAt, relativeTo: now),
publishedAgo: self?.dateFormatter.localizedString(for: entry.publishedAt, relativeTo: now) ?? "",
hasURL: entry.link != nil,
newsItem: entry
)
}
guard viewIsLoaded else { return }
if cells.isEmpty {
view?.showEmptyScreen()
} else {
view?.reload(with: NewsSection(cells: cells))
guard self?.viewIsLoaded == true else { return }
if cells.isEmpty {
self?.view?.showEmptyScreen()
} else {
self?.view?.reload(with: NewsSection(cells: cells))
}
}
}
}

View File

@ -21,6 +21,9 @@ import Foundation
import Data
protocol AppLockModuleInteracting: AnyObject {
var isLockoutAttemptsChangeBlocked: Bool { get }
var isLockoutBlockTimeChangeBlocked: Bool { get }
var selectedAttempts: AppLockAttempts { get }
var selectedBlockTime: AppLockBlockTime { get }
@ -30,15 +33,19 @@ protocol AppLockModuleInteracting: AnyObject {
final class AppLockModuleInteractor {
private let appLockInteractor: AppLockStateInteracting
private let mdmInteractor: MDMInteracting
init(appLockInteractor: AppLockStateInteracting) {
init(appLockInteractor: AppLockStateInteracting, mdmInteractor: MDMInteracting) {
self.appLockInteractor = appLockInteractor
self.mdmInteractor = mdmInteractor
}
}
extension AppLockModuleInteractor: AppLockModuleInteracting {
var selectedAttempts: AppLockAttempts { appLockInteractor.appLockAttempts }
var selectedBlockTime: AppLockBlockTime { appLockInteractor.appLockBlockTime }
var isLockoutAttemptsChangeBlocked: Bool { mdmInteractor.isLockoutAttemptsChangeBlocked }
var isLockoutBlockTimeChangeBlocked: Bool { mdmInteractor.isLockoutBlockTimeChangeBlocked }
func setAttempts(_ value: AppLockAttempts) {
appLockInteractor.setAppLockAttempts(value)

View File

@ -34,4 +34,5 @@ struct AppLockMenuSection: TableViewSection {
struct AppLockMenuCell: Hashable {
let title: String
let checkmark: Bool
let disabled: Bool
}

View File

@ -27,7 +27,11 @@ extension AppLockPresenter {
title: T.Settings.tooManyAttemptsHeader,
cells:
AppLockAttempts.allCases.map {
AppLockMenuCell(title: $0.localized, checkmark: selectedAttempt == $0)
AppLockMenuCell(
title: $0.localized,
checkmark: selectedAttempt == $0,
disabled: interactor.isLockoutAttemptsChangeBlocked
)
},
footer: T.Settings.howManyAttemptsFooter
)
@ -37,7 +41,11 @@ extension AppLockPresenter {
title: T.Settings.blockFor,
cells:
AppLockBlockTime.allCases.map {
AppLockMenuCell(title: $0.localized, checkmark: selectedBlockTime == $0)
AppLockMenuCell(
title: $0.localized,
checkmark: selectedBlockTime == $0,
disabled: interactor.isLockoutBlockTimeChangeBlocked
)
}
)
var menu: [AppLockMenuSection] = [

View File

@ -37,10 +37,14 @@ final class AppLockPresenter {
func handleSelection(at indexPath: IndexPath) {
if indexPath.section == 0 {
guard let value = AppLockAttempts.allCases[safe: indexPath.row] else { return }
guard let value = AppLockAttempts.allCases[
safe: indexPath.row
], !interactor.isLockoutAttemptsChangeBlocked else { return }
interactor.setAttempts(value)
} else {
guard let value = AppLockBlockTime.allCases[safe: indexPath.row] else { return }
guard let value = AppLockBlockTime.allCases[
safe: indexPath.row
], !interactor.isLockoutBlockTimeChangeBlocked else { return }
interactor.setBlockTime(value)
}
reload()

View File

@ -132,6 +132,9 @@ extension AppLockViewController {
cell.update(icon: nil, title: data.title, kind: accessory)
cell.tintColor = Theme.Colors.Fill.theme
cell.selectionStyle = .none
if data.disabled {
cell.disable()
}
return cell
}

View File

@ -103,13 +103,14 @@ extension AppSecurityFlowController: AppSecurityFlowControlling {
on: navi,
parent: self,
action: newAction,
step: .second(PIN: PIN, pinType: typeOfPIN)
step: .second(PIN: PIN, pinType: typeOfPIN),
lockNavigation: false
)
}
func toCreatePIN(pinType: PINType) {
let navi = navigationControllerForModal()
NewPINFlowController.setRoot(in: navi, parent: self, pinType: pinType)
NewPINFlowController.setRoot(in: navi, parent: self, pinType: pinType, lockNavigation: false)
viewController.present(navi, animated: true, completion: nil)
}
@ -164,7 +165,13 @@ extension AppSecurityFlowController: VerifyPINFlowControllerParent {
dismiss()
case .change(let currentPINType):
guard let navi = viewController.presentedViewController as? UINavigationController else { return }
NewPINFlowController.push(on: navi, parent: self, action: .change, step: .first(pinType: currentPINType))
NewPINFlowController.push(
on: navi,
parent: self,
action: .change,
step: .first(pinType: currentPINType),
lockNavigation: false
)
case .authorize:
viewController.presenter.handleInitialAutorization()
guard let vc = viewController.children.first(where: { $0 is VerifyPINViewController }) else { return }
@ -190,7 +197,7 @@ extension AppSecurityFlowController: NewPINFlowControllerParent {
dismiss()
}
func pingGathered(
func pinGathered(
with PIN: String,
pinType: PINType,
action: NewPINFlowController.Action,

Some files were not shown because too many files have changed in this diff Show More