diff --git a/CHANGES.rst b/CHANGES.rst index a1c97f07d..f1e9f5db6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,7 +2,7 @@ Changes to be released in next version ================================================= ✨ Features - * + * Change Pin inside the app (#3881) 🙌 Improvements * AuthVC: Update SSO button wording. diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 910125790..47b326752 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1460,6 +1460,7 @@ Tap the + to start adding people."; "pin_protection_choose_pin" = "Create a PIN for security"; "pin_protection_confirm_pin" = "Confirm your PIN"; "pin_protection_confirm_pin_to_disable" = "Confirm PIN to disable PIN"; +"pin_protection_confirm_pin_to_change" = "Confirm PIN to change PIN"; "pin_protection_enter_pin" = "Enter your PIN"; "pin_protection_forgot_pin" = "Forgot PIN"; "pin_protection_reset_alert_title" = "Reset PIN"; @@ -1472,6 +1473,7 @@ Tap the + to start adding people."; "pin_protection_settings_section_footer" = "To reset your PIN, you'll need to re-login and create a new one."; "pin_protection_settings_enabled_forced" = "PIN enabled"; "pin_protection_settings_enable_pin" = "Enable PIN"; +"pin_protection_settings_change_pin" = "Change PIN"; "pin_protection_not_allowed_pin" = "For security reasons, this PIN isn’t available. Please try another PIN"; "pin_protection_explanatory" = "Setting up a PIN lets you protect data like messages and contacts, so only you can access them by entering the PIN at the start of the app."; "pin_protection_kick_user_alert_message" = "Too many errors, you've been logged out"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 8bbb3550d..203441c6a 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2062,6 +2062,10 @@ internal enum VectorL10n { internal static var pinProtectionConfirmPin: String { return VectorL10n.tr("Vector", "pin_protection_confirm_pin") } + /// Confirm PIN to change PIN + internal static var pinProtectionConfirmPinToChange: String { + return VectorL10n.tr("Vector", "pin_protection_confirm_pin_to_change") + } /// Confirm PIN to disable PIN internal static var pinProtectionConfirmPinToDisable: String { return VectorL10n.tr("Vector", "pin_protection_confirm_pin_to_disable") @@ -2110,6 +2114,10 @@ internal enum VectorL10n { internal static var pinProtectionResetAlertTitle: String { return VectorL10n.tr("Vector", "pin_protection_reset_alert_title") } + /// Change PIN + internal static var pinProtectionSettingsChangePin: String { + return VectorL10n.tr("Vector", "pin_protection_settings_change_pin") + } /// Enable PIN internal static var pinProtectionSettingsEnablePin: String { return VectorL10n.tr("Vector", "pin_protection_settings_enable_pin") diff --git a/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.swift b/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.swift index 68ba18939..50c41980d 100644 --- a/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.swift +++ b/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.swift @@ -216,6 +216,8 @@ final class EnterPinCodeViewController: UIViewController { self.renderConfirmPinToDisable() case .inactive: self.renderInactive() + case .changePin: + self.renderChangePin() } } @@ -336,6 +338,17 @@ final class EnterPinCodeViewController: UIViewController { self.explanatoryLabel.isHidden = true } + private func renderChangePin() { + self.inactiveView.isHidden = true + self.mainStackView.isHidden = false + self.logoImageView.isHidden = true + self.informationLabel.text = VectorL10n.pinProtectionConfirmPinToChange + self.explanatoryLabel.isHidden = true + self.forgotPinButton.isHidden = true + self.bottomView.isHidden = false + self.notAllowedPinView.isHidden = true + } + private func renderPlaceholdersCount(_ count: Int, error: Bool = false) { UIView.animate(withDuration: 0.3) { for case let imageView as UIImageView in self.placeholderStackView.arrangedSubviews { diff --git a/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewModel.swift b/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewModel.swift index c3d4e5435..d749346ab 100644 --- a/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewModel.swift +++ b/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewModel.swift @@ -28,6 +28,7 @@ final class EnterPinCodeViewModel: EnterPinCodeViewModelType { private var originalViewMode: SetPinCoordinatorViewMode private var viewMode: SetPinCoordinatorViewMode + private var initialPin: String = "" private var firstPin: String = "" private var currentPin: String = "" { didSet { @@ -116,49 +117,12 @@ final class EnterPinCodeViewModel: EnterPinCodeViewModelType { switch viewMode { case .setPin, .setPinAfterLogin, .setPinAfterRegister: // choosing pin - if firstPin.isEmpty { - // check if this PIN is allowed - if pinCodePreferences.notAllowedPINs.contains(currentPin) { - viewMode = .notAllowedPin - update(viewState: .notAllowedPin) - return - } - // go to next screen - firstPin = currentPin - currentPin.removeAll() - update(viewState: .confirmPin) - } else { - // check first and second pins - if firstPin == currentPin { - // complete with a little delay - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.coordinatorDelegate?.enterPinCodeViewModel(self, didCompleteWithPin: self.firstPin) - } - } else { - update(viewState: .pinsDontMatch) - } - } + updateAfterPinSet() case .unlock, .confirmPinToDeactivate: // unlocking if currentPin != pinCodePreferences.pin { // no match - numberOfFailuresDuringEnterPIN += 1 - pinCodePreferences.numberOfPinFailures += 1 - if viewMode == .unlock && localAuthenticationService.shouldLogOutUser() { - // log out user - self.coordinatorDelegate?.enterPinCodeViewModelDidCompleteWithReset(self, dueToTooManyErrors: true) - return - } - if numberOfFailuresDuringEnterPIN < pinCodePreferences.allowedNumberOfTrialsBeforeAlert { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.viewDelegate?.enterPinCodeViewModel(self, didUpdateViewState: .wrongPin) - self.currentPin.removeAll() - } - } else { - viewDelegate?.enterPinCodeViewModel(self, didUpdateViewState: .wrongPinTooManyTimes) - numberOfFailuresDuringEnterPIN = 0 - currentPin.removeAll() - } + updateAfterUnlockFailed() } else { // match // we can use biometrics anymore, if set @@ -169,6 +133,23 @@ final class EnterPinCodeViewModel: EnterPinCodeViewModelType { self.coordinatorDelegate?.enterPinCodeViewModelDidComplete(self) } } + case .changePin: + // unlocking + if initialPin.isEmpty && currentPin != pinCodePreferences.pin { + // no match + updateAfterUnlockFailed() + } else { + // match or already unlocked + if initialPin.isEmpty { + // the user can choose a new Pin code + initialPin = currentPin + currentPin.removeAll() + update(viewState: .choosePin) + } else { + // choosing pin + updateAfterPinSet() + } + } default: break } @@ -185,6 +166,8 @@ final class EnterPinCodeViewModel: EnterPinCodeViewModelType { return .choosePinAfterLogin case .setPinAfterRegister: return .choosePinAfterRegister + case .changePin: + return .changePin default: return .inactive } @@ -201,6 +184,8 @@ final class EnterPinCodeViewModel: EnterPinCodeViewModelType { update(viewState: .confirmPinToDisable) case .inactive: update(viewState: .inactive) + case .changePin: + update(viewState: .changePin) default: break } @@ -209,4 +194,49 @@ final class EnterPinCodeViewModel: EnterPinCodeViewModelType { private func update(viewState: EnterPinCodeViewState) { self.viewDelegate?.enterPinCodeViewModel(self, didUpdateViewState: viewState) } + + private func updateAfterUnlockFailed() { + numberOfFailuresDuringEnterPIN += 1 + pinCodePreferences.numberOfPinFailures += 1 + if viewMode == .unlock && localAuthenticationService.shouldLogOutUser() { + // log out user + self.coordinatorDelegate?.enterPinCodeViewModelDidCompleteWithReset(self, dueToTooManyErrors: true) + return + } + if numberOfFailuresDuringEnterPIN < pinCodePreferences.allowedNumberOfTrialsBeforeAlert { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.viewDelegate?.enterPinCodeViewModel(self, didUpdateViewState: .wrongPin) + self.currentPin.removeAll() + } + } else { + viewDelegate?.enterPinCodeViewModel(self, didUpdateViewState: .wrongPinTooManyTimes) + numberOfFailuresDuringEnterPIN = 0 + currentPin.removeAll() + } + } + + private func updateAfterPinSet() { + if firstPin.isEmpty { + // check if this PIN is allowed + if pinCodePreferences.notAllowedPINs.contains(currentPin) { + viewMode = .notAllowedPin + update(viewState: .notAllowedPin) + return + } + // go to next screen + firstPin = currentPin + currentPin.removeAll() + update(viewState: .confirmPin) + } else { + // check first and second pins + if firstPin == currentPin { + // complete with a little delay + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.coordinatorDelegate?.enterPinCodeViewModel(self, didCompleteWithPin: self.firstPin) + } + } else { + update(viewState: .pinsDontMatch) + } + } + } } diff --git a/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewState.swift b/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewState.swift index c9a1d033a..09ecfd8b3 100644 --- a/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewState.swift +++ b/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewState.swift @@ -32,4 +32,5 @@ enum EnterPinCodeViewState { case forgotPin // after pin has been set, user tapped forgot pin case confirmPinToDisable // after pin has been set, confirm pin to disable pin case inactive // inactive state, only used when app is not active + case changePin // pin is set, user tapped change pin from settings } diff --git a/Riot/Modules/SetPinCode/SetPinCoordinator.swift b/Riot/Modules/SetPinCode/SetPinCoordinator.swift index 181036e3f..5bc9ea80f 100644 --- a/Riot/Modules/SetPinCode/SetPinCoordinator.swift +++ b/Riot/Modules/SetPinCode/SetPinCoordinator.swift @@ -64,6 +64,8 @@ final class SetPinCoordinator: SetPinCoordinatorType { return createSetupBiometricsCoordinator() case .inactive: return createEnterPinCodeCoordinator() + case .changePin: + return createEnterPinCodeCoordinator() } } diff --git a/Riot/Modules/SetPinCode/SetPinCoordinatorBridgePresenter.swift b/Riot/Modules/SetPinCode/SetPinCoordinatorBridgePresenter.swift index 1ed733551..99319c6b3 100644 --- a/Riot/Modules/SetPinCode/SetPinCoordinatorBridgePresenter.swift +++ b/Riot/Modules/SetPinCode/SetPinCoordinatorBridgePresenter.swift @@ -29,6 +29,7 @@ import Foundation case setupBiometricsFromSettings case confirmBiometricsToDeactivate case inactive + case changePin } @objc protocol SetPinCoordinatorBridgePresenterDelegate { diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m index a6cf815e6..fd01afb92 100644 --- a/Riot/Modules/Settings/Security/SecurityViewController.m +++ b/Riot/Modules/Settings/Security/SecurityViewController.m @@ -71,6 +71,7 @@ enum { enum { PIN_CODE_SETTING, PIN_CODE_DESCRIPTION, + PIN_CODE_CHANGE, PIN_CODE_BIOMETRICS, PIN_CODE_COUNT }; @@ -315,7 +316,10 @@ TableViewSectionsDelegate> // Rows [pinCodeSection addRowWithTag:PIN_CODE_SETTING]; [pinCodeSection addRowWithTag:PIN_CODE_DESCRIPTION]; - + if ([PinCodePreferences shared].isPinSet) { + [pinCodeSection addRowWithTag:PIN_CODE_CHANGE]; + } + if ([PinCodePreferences shared].isBiometricsAvailable) { [pinCodeSection addRowWithTag:PIN_CODE_BIOMETRICS]; @@ -1293,6 +1297,10 @@ TableViewSectionsDelegate> cell = [self descriptionCellForTableView:tableView withText:nil]; } } + else if (indexPath.row == PIN_CODE_CHANGE) + { + cell = [self buttonCellWithTitle:NSLocalizedStringFromTable(@"pin_protection_settings_change_pin", @"Vector", nil) action:@selector(changePinCode: ) forTableView:tableView atIndexPath:indexPath]; + } else if (indexPath.row == PIN_CODE_BIOMETRICS) { MXKTableViewCellWithLabelAndSwitch *switchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; @@ -1708,6 +1716,14 @@ TableViewSectionsDelegate> [self.setPinCoordinatorBridgePresenter presentFrom:self animated:YES]; } +- (void)changePinCode:(UIButton *)sender +{ + SetPinCoordinatorViewMode viewMode = SetPinCoordinatorViewModeChangePin; + self.setPinCoordinatorBridgePresenter = [[SetPinCoordinatorBridgePresenter alloc] initWithSession:self.mainSession viewMode:viewMode]; + self.setPinCoordinatorBridgePresenter.delegate = self; + [self.setPinCoordinatorBridgePresenter presentFrom:self animated:YES]; +} + #pragma mark - SettingsKeyBackupTableViewSectionDelegate #ifdef CROSS_SIGNING_AND_BACKUP_DEV - (void)settingsKeyBackupTableViewSectionDidUpdate:(SettingsKeyBackupTableViewSection *)settingsKeyBackupTableViewSection