Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/ViewControllers/AppSettings/Account/AccountSettingsViewController.swift
1 views
//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import SignalUI

class AccountSettingsViewController: OWSTableViewController2 {

    private let appReadiness: AppReadinessSetter
    private let context: ViewControllerContext

    init(appReadiness: AppReadinessSetter) {
        self.appReadiness = appReadiness
        // TODO[ViewContextPiping]
        self.context = ViewControllerContext.shared
        super.init()
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        title = OWSLocalizedString("SETTINGS_ACCOUNT", comment: "Title for the 'account' link in settings.")

        updateTableContents()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        updateTableContents()
        tableView.layoutIfNeeded()
    }

    func updateTableContents() {
        let contents = OWSTableContents()

        // Show the change pin and reglock sections
        if DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegisteredPrimaryDevice {
            let pinSection = OWSTableSection()
            let isPinEnabled = SSKEnvironment.shared.ows2FAManagerRef.isPinEnabledWithSneakyTransaction

            pinSection.headerTitle = OWSLocalizedString(
                "SETTINGS_PINS_TITLE",
                comment: "Title for the 'PINs' section of the privacy settings.",
            )
            pinSection.footerAttributedTitle = NSAttributedString.composed(of: [
                OWSLocalizedString(
                    "SETTINGS_PINS_FOOTER",
                    comment: "Footer for the 'PINs' section of the privacy settings.",
                ),
                " ",
                CommonStrings.learnMore.styled(with: .link(URL.Support.pin)),
            ])
            .styled(with: defaultFooterTextStyle)

            pinSection.add(.disclosureItem(
                withText: isPinEnabled
                    ? OWSLocalizedString(
                        "SETTINGS_PINS_ITEM",
                        comment: "Label for the 'pins' item of the privacy settings when the user does have a pin.",
                    )
                    : OWSLocalizedString(
                        "SETTINGS_PINS_ITEM_CREATE",
                        comment: "Label for the 'pins' item of the privacy settings when the user doesn't have a pin.",
                    ),
                actionBlock: { [weak self] in
                    if isPinEnabled {
                        self?.showChangePin()
                    } else {
                        self?.showCreatePin()
                    }
                },
            ))

            // Reminders toggle.
            if isPinEnabled {
                pinSection.add(.switch(
                    withText: OWSLocalizedString(
                        "SETTINGS_PIN_REMINDER_SWITCH_LABEL",
                        comment: "Label for the 'pin reminder' switch of the privacy settings.",
                    ),
                    isOn: { SSKEnvironment.shared.ows2FAManagerRef.areRemindersEnabled },
                    target: self,
                    selector: #selector(arePINRemindersEnabledDidChange),
                ))
            }

            contents.add(pinSection)

            let regLockSection = OWSTableSection()
            regLockSection.footerTitle = OWSLocalizedString(
                "SETTINGS_TWO_FACTOR_PINS_AUTH_FOOTER",
                comment: "Footer for the 'two factor auth' section of the privacy settings when Signal PINs are available.",
            )

            regLockSection.add(.switch(
                withText: OWSLocalizedString(
                    "SETTINGS_TWO_FACTOR_AUTH_SWITCH_LABEL",
                    comment: "Label for the 'enable registration lock' switch of the privacy settings.",
                ),
                isOn: { SSKEnvironment.shared.ows2FAManagerRef.isRegistrationLockV2Enabled },
                target: self,
                selector: #selector(isRegistrationLockV2EnabledDidChange),
            ))

            contents.add(regLockSection)

            let advancedSection = OWSTableSection()
            advancedSection.add(.disclosureItem(
                withText: OWSLocalizedString(
                    "SETTINGS_ADVANCED_PIN_SETTINGS",
                    comment: "Label for the 'advanced pin settings' button.",
                ),
                actionBlock: { [weak self] in
                    let vc = AdvancedPinSettingsTableViewController()
                    self?.navigationController?.pushViewController(vc, animated: true)
                },
            ))
            contents.add(advancedSection)
        }

        let accountSection = OWSTableSection()
        accountSection.headerTitle = OWSLocalizedString("SETTINGS_ACCOUNT", comment: "Title for the 'account' link in settings.")

        let tsRegistrationState = DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction
        if tsRegistrationState.isDeregistered {
            accountSection.add(.actionItem(
                withText: tsRegistrationState.isPrimaryDevice ?? true
                    ? OWSLocalizedString("SETTINGS_REREGISTER_BUTTON", comment: "Label for re-registration button.")
                    : OWSLocalizedString("SETTINGS_RELINK_BUTTON", comment: "Label for re-link button."),
                textColor: .ows_accentBlue,
                accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "reregister"),
                actionBlock: { [weak self] in
                    self?.reregisterUser()
                },
            ))
            accountSection.add(.actionItem(
                withText: OWSLocalizedString(
                    "SETTINGS_DELETE_DATA_BUTTON",
                    comment: "Label for 'delete data' button.",
                ),
                textColor: .ows_accentRed,
                accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "delete_data"),
                actionBlock: { [weak self] in
                    self?.deleteUnregisteredUserData()
                },
            ))
        } else if tsRegistrationState.isRegisteredPrimaryDevice {
            switch self.changeNumberState() {
            case .disallowed:
                break
            case .allowed:
                accountSection.add(.actionItem(
                    withText: OWSLocalizedString("SETTINGS_CHANGE_PHONE_NUMBER_BUTTON", comment: "Label for button in settings views to change phone number"),
                    accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "change_phone_number"),
                    actionBlock: { [weak self] in
                        guard let self else {
                            return
                        }
                        // Fetch the state again in case it changed from under us
                        // between when the button was rendered and when it was tapped.
                        switch self.changeNumberState() {
                        case .disallowed:
                            return
                        case .allowed(let changeNumberParams):
                            self.changePhoneNumber(changeNumberParams)
                        }
                    },
                ))
            }
            accountSection.add(.actionItem(
                withText: OWSLocalizedString(
                    "SETTINGS_ACCOUNT_DATA_REPORT_BUTTON",
                    comment: "Label for button in settings to get your account data report",
                ),
                accessibilityIdentifier: UIView.accessibilityIdentifier(
                    in: self,
                    name: "request_account_data_report",
                ),
                actionBlock: { [weak self] in
                    self?.requestAccountDataReport()
                },
            ))
            accountSection.add(.item(
                name: OWSLocalizedString("SETTINGS_DELETE_ACCOUNT_BUTTON", comment: ""),
                textColor: .ows_accentRed,
                accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "delete_account"),
                actionBlock: { [weak self] in
                    self?.unregisterUser()
                },
            ))
        } else {
            accountSection.add(.actionItem(
                withText: OWSLocalizedString(
                    "SETTINGS_DELETE_DATA_BUTTON",
                    comment: "Label for 'delete data' button.",
                ),
                textColor: .ows_accentRed,
                accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "delete_data"),
                actionBlock: { [weak self] in
                    self?.deleteLinkedData()
                },
            ))
        }

        contents.add(accountSection)

        self.contents = contents
    }

    // MARK: - Account

    private func reregisterUser() {
        RegistrationUtils.showReregistrationUI(fromViewController: self, appReadiness: appReadiness)
    }

    private func deleteLinkedData() {
        OWSActionSheets.showConfirmationAlert(
            title: OWSLocalizedString("CONFIRM_DELETE_LINKED_DATA_TITLE", comment: ""),
            message: OWSLocalizedString("CONFIRM_DELETE_LINKED_DATA_TEXT", comment: ""),
            proceedTitle: OWSLocalizedString("PROCEED_BUTTON", comment: ""),
            proceedStyle: .destructive,
            proceedAction: { _ in
                let localDeviceId = DependenciesBridge.shared.tsAccountManager.storedDeviceIdWithMaybeTransaction
                let keyFetcher = SSKEnvironment.shared.databaseStorageRef.keyFetcher
                let registrationStateChangeManager = DependenciesBridge.shared.registrationStateChangeManager

                ModalActivityIndicatorViewController.present(
                    fromViewController: self,
                    title: CommonStrings.deletingModal,
                ) { _ in
                    await SignalApp.shared.resetLinkedAppDataAndExit(
                        localDeviceId: localDeviceId,
                        keyFetcher: keyFetcher,
                        registrationStateChangeManager: registrationStateChangeManager,
                    )
                }
            },
            fromViewController: self,
        )
    }

    private func unregisterUser() {
        let vc = DeleteAccountConfirmationViewController(appReadiness: appReadiness)
        presentFormSheet(OWSNavigationController(rootViewController: vc), animated: true)
    }

    private func deleteUnregisteredUserData() {
        OWSActionSheets.showConfirmationAlert(
            title: OWSLocalizedString("CONFIRM_DELETE_DATA_TITLE", comment: ""),
            message: OWSLocalizedString("CONFIRM_DELETE_DATA_TEXT", comment: ""),
            proceedTitle: OWSLocalizedString("PROCEED_BUTTON", comment: ""),
            proceedStyle: .destructive,
            proceedAction: { _ in
                ModalActivityIndicatorViewController.present(
                    fromViewController: self,
                    title: CommonStrings.deletingModal,
                ) { _ in
                    let keyFetcher = SSKEnvironment.shared.databaseStorageRef.keyFetcher
                    SignalApp.shared.resetAppDataAndExit(keyFetcher: keyFetcher)
                }
            },
            fromViewController: self,
        )
    }

    private func requestAccountDataReport() {
        let vc = RequestAccountDataReportViewController()
        navigationController?.pushViewController(vc, animated: true)
    }

    enum ChangeNumberState {
        case disallowed
        case allowed(RegistrationMode.ChangeNumberParams)
    }

    private func changeNumberState() -> ChangeNumberState {
        return SSKEnvironment.shared.databaseStorageRef.read { transaction -> ChangeNumberState in
            let tsAccountManager = DependenciesBridge.shared.tsAccountManager
            let registeredState = try? tsAccountManager.registeredState(tx: transaction)
            guard let registeredState else {
                return .disallowed
            }
            let loader = RegistrationCoordinatorLoaderImpl(dependencies: .from(self))
            switch loader.restoreLastMode(transaction: transaction) {
            case .none, .changingNumber:
                break
            case .registering, .reRegistering:
                // Don't allow changing number if we are in the middle of registering.
                return .disallowed
            }
            guard
                let localE164 = E164(registeredState.localIdentifiers.phoneNumber),
                let authToken = tsAccountManager.storedServerAuthToken(tx: transaction),
                let localDeviceId = tsAccountManager.storedDeviceId(tx: transaction).ifValid
            else {
                return .disallowed
            }

            return .allowed(RegistrationMode.ChangeNumberParams(
                oldE164: localE164,
                oldAuthToken: authToken,
                localAci: registeredState.localIdentifiers.aci,
                localDeviceId: localDeviceId,
            ))
        }
    }

    private func changePhoneNumber(_ params: RegistrationMode.ChangeNumberParams) {
        let logger = PrefixedLogger(prefix: "[ChangeNum]")
        logger.info("Attempting to start change number from settings")
        let dependencies = RegistrationCoordinatorDependencies.from(NSObject())
        let desiredMode = RegistrationMode.changingNumber(params)
        let loader = RegistrationCoordinatorLoaderImpl(dependencies: dependencies)
        let coordinator = SSKEnvironment.shared.databaseStorageRef.write {
            return loader.coordinator(
                forDesiredMode: desiredMode,
                transaction: $0,
                logger: logger,
            )
        }
        let navController = RegistrationNavigationController.withCoordinator(coordinator, appReadiness: appReadiness)
        let window: UIWindow = CurrentAppContext().mainWindow!
        window.rootViewController = navController
    }

    // MARK: - PINs

    @objc
    private func arePINRemindersEnabledDidChange(_ sender: UISwitch) {
        if sender.isOn {
            SSKEnvironment.shared.databaseStorageRef.write { transaction in
                SSKEnvironment.shared.ows2FAManagerRef.setAreRemindersEnabled(true, transaction: transaction)
            }
        } else {
            let pinConfirmationVC = PinConfirmationViewController(
                title: OWSLocalizedString(
                    "SETTINGS_PIN_REMINDER_DISABLE_CONFIRMATION_TITLE",
                    comment: "The title for the dialog asking user to confirm their PIN to disable reminders",
                ),
                explanation: OWSLocalizedString(
                    "SETTINGS_PIN_REMINDER_DISABLE_CONFIRMATION_EXPLANATION",
                    comment: "The explanation for the dialog asking user to confirm their PIN to disable reminders",
                ),
                actionText: OWSLocalizedString(
                    "SETTINGS_PIN_REMINDER_DISABLE_CONFIRMATION_ACTION",
                    comment: "The button text for the dialog asking user to confirm their PIN to disable reminders",
                ),
            ) { [weak self] confirmed in
                guard let self else { return }
                if confirmed {
                    SSKEnvironment.shared.databaseStorageRef.write { transaction in
                        SSKEnvironment.shared.ows2FAManagerRef.setAreRemindersEnabled(false, transaction: transaction)
                    }

                    ExperienceUpgradeManager.dismissPINReminderIfNecessary()
                } else {
                    self.updateTableContents()
                }
            }
            present(pinConfirmationVC, animated: true)
        }
    }

    @objc
    private func isRegistrationLockV2EnabledDidChange(_ sender: UISwitch) {
        let shouldBeEnabled = sender.isOn

        guard shouldBeEnabled != SSKEnvironment.shared.ows2FAManagerRef.isRegistrationLockV2Enabled else { return }

        let actionSheet: ActionSheetController
        if shouldBeEnabled {
            actionSheet = ActionSheetController(
                title: OWSLocalizedString(
                    "SETTINGS_REGISTRATION_LOCK_TURN_ON_TITLE",
                    comment: "Title for the alert confirming that the user wants to turn on registration lock.",
                ),
                message: OWSLocalizedString(
                    "SETTINGS_REGISTRATION_LOCK_TURN_ON_MESSAGE",
                    comment: "Body for the alert confirming that the user wants to turn on registration lock.",
                ),
            )

            let turnOnAction = ActionSheetAction(title: OWSLocalizedString(
                "SETTINGS_REGISTRATION_LOCK_TURN_ON",
                comment: "Action to turn on registration lock",
            )) { _ in
                guard SSKEnvironment.shared.ows2FAManagerRef.isPinEnabledWithSneakyTransaction else {
                    OWSActionSheets.showActionSheet(
                        message: OWSLocalizedString(
                            "SETTINGS_REGISTRATION_LOCK_TURN_ON_ERROR_PIN_REQUIRED",
                            comment: "Message shown in an action sheet when attempting to enable registration lock, but the user does not have a PIN.",
                        ),
                        fromViewController: self,
                    )
                    self.updateTableContents()
                    return
                }

                Task {
                    do {
                        try await ModalActivityIndicatorViewController.presentAndPropagateResult(
                            from: self,
                            title: CommonStrings.updatingModal,
                        ) {
                            try await SSKEnvironment.shared.ows2FAManagerRef.enableRegistrationLockV2(logger: PrefixedLogger(prefix: "[Settings]"))
                        }
                    } catch where error.isNetworkFailureOrTimeout {
                        owsFailDebug("Network error enabling reglock.")

                        OWSActionSheets.showActionSheet(
                            message: OWSLocalizedString(
                                "SETTINGS_REGISTRATION_LOCK_TURN_ON_ERROR_NETWORK",
                                comment: "Message shown in an action sheet when attempting to enable registration lock, but encountering a network error.",
                            ),
                            fromViewController: self,
                        )
                    } catch {
                        owsFailDebug("Failed to enable reglock! \(error)")

                        OWSActionSheets.showContactSupportActionSheet(
                            message: OWSLocalizedString(
                                "SETTINGS_REGISTRATION_LOCK_TURN_ON_ERROR_GENERIC",
                                comment: "Message shown in an action sheet when attempting to enable registration lock, but encountering a generic error.",
                            ),
                            emailFilter: .custom("RegLockEnableFailure"),
                            fromViewController: self,
                        )
                    }

                    self.updateTableContents()
                }
            }
            actionSheet.addAction(turnOnAction)
        } else {
            actionSheet = ActionSheetController(title: OWSLocalizedString(
                "SETTINGS_REGISTRATION_LOCK_TURN_OFF_TITLE",
                comment: "Title for the alert confirming that the user wants to turn off registration lock.",
            ))

            let turnOffAction = ActionSheetAction(
                title: OWSLocalizedString(
                    "SETTINGS_REGISTRATION_LOCK_TURN_OFF",
                    comment: "Action to turn off registration lock",
                ),
                style: .destructive,
            ) { _ in
                Task {
                    do {
                        try await ModalActivityIndicatorViewController.presentAndPropagateResult(
                            from: self,
                            title: CommonStrings.updatingModal,
                        ) {
                            try await SSKEnvironment.shared.ows2FAManagerRef.disableRegistrationLockV2()
                        }
                    } catch where error.isNetworkFailureOrTimeout {
                        owsFailDebug("Network error disabling reglock.")

                        OWSActionSheets.showActionSheet(
                            message: OWSLocalizedString(
                                "SETTINGS_REGISTRATION_LOCK_TURN_OFF_ERROR_NETWORK",
                                comment: "Message shown in an action sheet when attempting to enable registration lock, but encountering a network error.",
                            ),
                            fromViewController: self,
                        )
                    } catch {
                        owsFailDebug("Failed to disable reglock! \(error)")

                        OWSActionSheets.showContactSupportActionSheet(
                            message: OWSLocalizedString(
                                "SETTINGS_REGISTRATION_LOCK_TURN_OFF_ERROR_GENERIC",
                                comment: "Message shown in an action sheet when attempting to enable registration lock, but encountering a generic error.",
                            ),
                            emailFilter: .custom("RegLockDisableFailure"),
                            fromViewController: self,
                        )
                    }

                    self.updateTableContents()
                }
            }
            actionSheet.addAction(turnOffAction)
        }

        let cancelAction = ActionSheetAction(
            title: CommonStrings.cancelButton,
            style: .cancel,
        ) { _ in
            self.updateTableContents()
        }
        actionSheet.addAction(cancelAction)

        presentActionSheet(actionSheet)
    }

    // MARK: -

    private func showChangePin() {
        let vc = PinSetupViewController(mode: .changing) { [weak self] _, _ in
            guard let self else { return }
            self.navigationController?.popToViewController(self, animated: true)
        }
        navigationController?.pushViewController(vc, animated: true)
    }

    private func showCreatePin() {
        let vc = PinSetupViewController(
            mode: .creating,
        ) { [weak self] _, _ in
            guard let self else { return }
            self.navigationController?.popToViewController(self, animated: true)
        }
        navigationController?.pushViewController(vc, animated: true)
    }
}