import Foundation import UIKit import Display import SwiftSignalKit import TelegramCore import LegacyComponents import TelegramPresentationData import ItemListUI import PresentationDataUtils import OverlayStatusController import AccountContext import AlertUI import PresentationDataUtils import UrlHandling import AccountUtils import PremiumUI import PasswordSetupUI import StorageUsageScreen private struct DeleteAccountOptionsArguments { let changePhoneNumber: () -> Void let addAccount: () -> Void let setupPrivacy: () -> Void let setupTwoStepAuth: () -> Void let setPasscode: () -> Void let clearCache: () -> Void let clearSyncedContacts: () -> Void let deleteChats: () -> Void let contactSupport: () -> Void let deleteAccount: () -> Void } private enum DeleteAccountOptionsSection: Int32 { case add case privacy case remove case support case delete } private enum DeleteAccountOptionsEntry: ItemListNodeEntry, Equatable { case changePhoneNumber(PresentationTheme, String, String) case addAccount(PresentationTheme, String, String) case changePrivacy(PresentationTheme, String, String) case setTwoStepAuth(PresentationTheme, String, String) case setPasscode(PresentationTheme, String, String) case clearCache(PresentationTheme, String, String) case clearSyncedContacts(PresentationTheme, String, String) case deleteChats(PresentationTheme, String, String) case contactSupport(PresentationTheme, String, String) case deleteAccount(PresentationTheme, String) var section: ItemListSectionId { switch self { case .changePhoneNumber, .addAccount: return DeleteAccountOptionsSection.add.rawValue case .changePrivacy, .setTwoStepAuth, .setPasscode: return DeleteAccountOptionsSection.privacy.rawValue case .clearCache, .clearSyncedContacts, .deleteChats: return DeleteAccountOptionsSection.remove.rawValue case .contactSupport: return DeleteAccountOptionsSection.support.rawValue case .deleteAccount: return DeleteAccountOptionsSection.delete.rawValue } } var stableId: Int32 { switch self { case .changePhoneNumber: return 0 case .addAccount: return 1 case .changePrivacy: return 2 case .setTwoStepAuth: return 3 case .setPasscode: return 4 case .clearCache: return 5 case .clearSyncedContacts: return 6 case .deleteChats: return 7 case .contactSupport: return 8 case .deleteAccount: return 9 } } static func <(lhs: DeleteAccountOptionsEntry, rhs: DeleteAccountOptionsEntry) -> Bool { return lhs.stableId < rhs.stableId } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! DeleteAccountOptionsArguments switch self { case let .changePhoneNumber(_, title, text): return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.changePhoneNumber, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.changePhoneNumber() }) case let .addAccount(_, title, text): return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteAddAccount, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.addAccount() }) case let .changePrivacy(_, title, text): return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.security, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.setupPrivacy() }) case let .setTwoStepAuth(_, title, text): return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteSetTwoStepAuth, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.setupTwoStepAuth() }) case let .setPasscode(_, title, text): return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteSetPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.setPasscode() }) case let .clearCache(_, title, text): return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.dataAndStorage, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.clearCache() }) case let .clearSyncedContacts(_, title, text): return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.clearSynced, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.clearSyncedContacts() }) case let .deleteChats(_, title, text): return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteChats, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.deleteChats() }) case let .contactSupport(_, title, text): return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.support, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.contactSupport() }) case let .deleteAccount(_, title): return ItemListActionItem(presentationData: presentationData, title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.deleteAccount() }) } } } private func deleteAccountOptionsEntries(presentationData: PresentationData, canAddAccounts: Bool, hasTwoStepAuth: Bool, hasPasscode: Bool) -> [DeleteAccountOptionsEntry] { var entries: [DeleteAccountOptionsEntry] = [] entries.append(.changePhoneNumber(presentationData.theme, presentationData.strings.DeleteAccount_Options_ChangePhoneNumberTitle, presentationData.strings.DeleteAccount_Options_ChangePhoneNumberText)) if canAddAccounts { entries.append(.addAccount(presentationData.theme, presentationData.strings.DeleteAccount_Options_AddAccountTitle, presentationData.strings.DeleteAccount_Options_AddAccountText)) } entries.append(.changePrivacy(presentationData.theme, presentationData.strings.DeleteAccount_Options_ChangePrivacyTitle, presentationData.strings.DeleteAccount_Options_ChangePrivacyText)) if !hasTwoStepAuth { entries.append(.setTwoStepAuth(presentationData.theme, presentationData.strings.DeleteAccount_Options_SetTwoStepAuthTitle, presentationData.strings.DeleteAccount_Options_SetTwoStepAuthText)) } if !hasPasscode { entries.append(.setPasscode(presentationData.theme, presentationData.strings.DeleteAccount_Options_SetPasscodeTitle, presentationData.strings.DeleteAccount_Options_SetPasscodeText)) } entries.append(.clearCache(presentationData.theme, presentationData.strings.DeleteAccount_Options_ClearCacheTitle, presentationData.strings.DeleteAccount_Options_ClearCacheText)) entries.append(.clearSyncedContacts(presentationData.theme, presentationData.strings.DeleteAccount_Options_ClearSyncedContactsTitle, presentationData.strings.DeleteAccount_Options_ClearSyncedContactsText)) entries.append(.deleteChats(presentationData.theme, presentationData.strings.DeleteAccount_Options_DeleteChatsTitle, presentationData.strings.DeleteAccount_Options_DeleteChatsText)) entries.append(.contactSupport(presentationData.theme, presentationData.strings.DeleteAccount_Options_ContactSupportTitle, presentationData.strings.DeleteAccount_Options_ContactSupportText)) entries.append(.deleteAccount(presentationData.theme, presentationData.strings.DeleteAccount_DeleteMyAccount)) return entries } public func deleteAccountOptionsController(context: AccountContext, navigationController: NavigationController, hasTwoStepAuth: Bool, twoStepAuthData: TwoStepVerificationAccessConfiguration?) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? var replaceTopControllerImpl: ((ViewController, Bool) -> Void)? var dismissImpl: (() -> Void)? let supportPeerDisposable = MetaDisposable() let arguments = DeleteAccountOptionsArguments(changePhoneNumber: { addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_phone_change_tap") let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.engine.account.peerId)) |> deliverOnMainQueue).start(next: { accountPeer in guard let accountPeer = accountPeer, case let .user(user) = accountPeer else { return } let introController = PrivacyIntroController(context: context, mode: .changePhoneNumber(user.phone ?? ""), proceedAction: { replaceTopControllerImpl?(ChangePhoneNumberController(context: context), false) }) pushControllerImpl?(introController) dismissImpl?() }) }, addAccount: { addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_add_account_tap") let _ = (activeAccountsAndPeers(context: context) |> take(1) |> deliverOnMainQueue ).start(next: { accountAndPeer, accountsAndPeers in var maximumAvailableAccounts: Int = 3 if accountAndPeer?.1.isPremium == true && !context.account.testingEnvironment { maximumAvailableAccounts = 4 } var count: Int = 1 for (accountContext, peer, _) in accountsAndPeers { if !accountContext.account.testingEnvironment { if peer.isPremium { maximumAvailableAccounts = 4 } count += 1 } } if count >= maximumAvailableAccounts { var replaceImpl: ((ViewController) -> Void)? let controller = PremiumLimitScreen(context: context, subject: .accounts, count: Int32(count), action: { let controller = PremiumIntroScreen(context: context, source: .accounts) replaceImpl?(controller) return true }) replaceImpl = { [weak controller] c in controller?.replace(with: c) } pushControllerImpl?(controller) } else { context.sharedContext.beginNewAuth(testingEnvironment: context.account.testingEnvironment) dismissImpl?() } }) }, setupPrivacy: { addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_privacy_tap") replaceTopControllerImpl?(makePrivacyAndSecurityController(context: context), false) }, setupTwoStepAuth: { addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_2fa_tap") if let data = twoStepAuthData { switch data { case .set: break case let .notSet(pendingEmail): if pendingEmail == nil { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = TwoFactorAuthSplashScreen(sharedContext: context.sharedContext, engine: .authorized(context.engine), mode: .intro(.init( title: presentationData.strings.TwoFactorSetup_Intro_Title, text: presentationData.strings.TwoFactorSetup_Intro_Text, actionText: presentationData.strings.TwoFactorSetup_Intro_Action, doneText: presentationData.strings.TwoFactorSetup_Done_Action, phoneNumber: nil ))) replaceTopControllerImpl?(controller, false) return } } } let controller = twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: false, data: twoStepAuthData.flatMap({ Signal.single(.access(configuration: $0)) }))) replaceTopControllerImpl?(controller, false) }, setPasscode: { addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_passcode_tap") let _ = passcodeOptionsAccessController(context: context, pushController: { controller in replaceTopControllerImpl?(controller, false) }, completion: { _ in replaceTopControllerImpl?(passcodeOptionsController(context: context), false) }).start(next: { controller in if let controller = controller { pushControllerImpl?(controller) } }) dismissImpl?() }, clearCache: { addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_clear_cache_tap") pushControllerImpl?(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in return storageUsageExceptionsScreen(context: context, category: category) })) dismissImpl?() }, clearSyncedContacts: { addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_clear_contacts_tap") replaceTopControllerImpl?(dataPrivacyController(context: context), false) }, deleteChats: { addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_delete_chats_tap") let presentationData = context.sharedContext.currentPresentationData.with { $0 } var faqUrl = presentationData.strings.DeleteAccount_DeleteMessagesURL if faqUrl == "DeleteAccount.DeleteMessagesURL" || faqUrl.isEmpty { faqUrl = "https://telegram.org/faq#q-can-i-delete-my-messages" } let resolvedUrl = resolveInstantViewUrl(account: context.account, url: faqUrl) |> mapToSignal { result -> Signal in guard case let .result(result) = result else { return .complete() } return .single(result) } let resolvedUrlPromise = Promise() resolvedUrlPromise.set(resolvedUrl) let openFaq: (Promise) -> Void = { resolvedUrl in let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) presentControllerImpl?(controller, nil) let _ = (resolvedUrl.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] resolvedUrl in controller?.dismiss() dismissImpl?() context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in pushControllerImpl?(controller) }, dismissInput: {}, contentContext: nil, progress: nil) }) } openFaq(resolvedUrlPromise) }, contactSupport: { [weak navigationController] in addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_support_tap") let presentationData = context.sharedContext.currentPresentationData.with { $0 } let supportPeer = Promise() supportPeer.set(context.engine.peers.supportPeerId()) var faqUrl = presentationData.strings.Settings_FAQ_URL if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty { faqUrl = "https://telegram.org/faq#general" } let resolvedUrl = resolveInstantViewUrl(account: context.account, url: faqUrl) |> mapToSignal { result -> Signal in guard case let .result(result) = result else { return .complete() } return .single(result) } let resolvedUrlPromise = Promise() resolvedUrlPromise.set(resolvedUrl) let openFaq: (Promise) -> Void = { resolvedUrl in let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) presentControllerImpl?(controller, nil) let _ = (resolvedUrl.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] resolvedUrl in controller?.dismiss() dismissImpl?() context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in pushControllerImpl?(controller) }, dismissInput: {}, contentContext: nil, progress: nil) }) } let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Settings_FAQ_Intro, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: { openFaq(resolvedUrlPromise) dismissImpl?() }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { supportPeerDisposable.set((supportPeer.get() |> take(1) |> deliverOnMainQueue).start(next: { peerId in guard let peerId = peerId else { return } let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { peer in guard let peer = peer else { return } if let navigationController = navigationController { dismissImpl?() context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) } }) })) }) ]) alertController.dismissed = { _ in addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_support_cancel") } presentControllerImpl?(alertController, nil) }, deleteAccount: { let controller = deleteAccountDataController(context: context, mode: .peers, twoStepAuthData: twoStepAuthData) replaceTopControllerImpl?(controller, true) }) let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.accessChallengeData(), activeAccountsAndPeers(context: context) ) |> map { presentationData, accessChallengeData, accountsAndPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) var hasPasscode = false switch accessChallengeData.data { case .numericalPassword, .plaintextPassword: hasPasscode = true default: break } let canAddAccounts = accountsAndPeers.1.count + 1 < maximumNumberOfAccounts let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.DeleteAccount_AlternativeOptionsTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: deleteAccountOptionsEntries(presentationData: presentationData, canAddAccounts: canAddAccounts, hasTwoStepAuth: hasTwoStepAuth, hasPasscode: hasPasscode), style: .blocks) return (controllerState, (listState, arguments)) } let controller = ItemListController(context: context, state: signal, tabBarItem: nil) controller.navigationPresentation = .modal pushControllerImpl = { [weak navigationController] value in navigationController?.pushViewController(value, animated: false) } presentControllerImpl = { [weak controller] value, arguments in controller?.present(value, in: .window(.root), with: arguments ?? ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } replaceTopControllerImpl = { [weak navigationController] c, complex in if complex { navigationController?.pushViewController(c, completion: { [weak navigationController, weak controller, weak c] in if let navigationController = navigationController { let controllers = navigationController.viewControllers.filter { $0 !== controller } c?.navigationPresentation = .modal navigationController.setViewControllers(controllers, animated: false) } }) } else { if c is PrivacyAndSecurityControllerImpl { if let navigationController = navigationController { if let existing = navigationController.viewControllers.first(where: { $0 is PrivacyAndSecurityControllerImpl }) as? ViewController { existing.scrollToTop?() dismissImpl?() } else { navigationController.replaceTopController(c, animated: true) } } } else { navigationController?.replaceTopController(c, animated: true) } } } dismissImpl = { [weak controller] in let _ = controller?.dismiss() } addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_show") return controller }