Files
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

2151 lines
104 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import UIKit
import Display
import AccountContext
import WalletContext
import PasscodeCore
import SwiftSignalKit
import TelegramNotices
import TelegramPresentationData
import ComponentFlow
import ViewControllerComponent
import MultilineTextComponent
import ListSectionComponent
import ListActionItemComponent
import PresentationDataUtils
import TelegramStringFormatting
import AlertComponent
import AlertCheckComponent
import UndoUI
import WalletAuthorizationUI
import WalletImportScreen
private final class WalletSettingsScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
let walletContext: WalletContext
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>), walletContext: WalletContext) {
self.context = context
self.updatedPresentationData = updatedPresentationData
self.walletContext = walletContext
}
static func ==(lhs: WalletSettingsScreenComponent, rhs: WalletSettingsScreenComponent) -> Bool {
return lhs.context === rhs.context
&& lhs.updatedPresentationData.initial === rhs.updatedPresentationData.initial
&& lhs.updatedPresentationData.signal === rhs.updatedPresentationData.signal
&& lhs.walletContext === rhs.walletContext
}
final class View: UIView {
private enum WalletFlow { case create, enableBackup, disableBackup }
private enum BackupAction {
case enable, disable
func isAvailable(for info: WalletContext.WalletInfo) -> Bool {
switch self {
case .enable:
return info.canEnableBackup
case .disable:
return info.backupEnabled
}
}
}
private struct BackupAccessRequest {
enum Phase { case restoring, importing, ready }
let action: BackupAction
let walletContext: WalletContext
let address: String
let publicKey: String
var phase: Phase = .restoring
}
private var walletFlow: WalletFlow?
private var flowSession: PasscodeSession?
private var flowGeneration: UInt64 = 0
private var backupAccessRequest: BackupAccessRequest?
private var backupAccessGeneration: UInt64 = 0
private var backupAccessSession: PasscodeSession?
private let backupAccessDisposable = MetaDisposable()
private var isVisible = false
private let scrollView: UIScrollView
private let recoverySection = ComponentView<Empty>()
private let backupSection = ComponentView<Empty>()
private let replacementSection = ComponentView<Empty>()
private let previousWalletsSection = ComponentView<Empty>()
private let debugSection = ComponentView<Empty>()
private let debugRemoveMnemonicDisposable = MetaDisposable()
private var isRemovingMnemonic = false
private var component: WalletSettingsScreenComponent?
private var environment: EnvironmentType?
private weak var state: EmptyComponentState?
private var isUpdating = false
private let operationDisposable = MetaDisposable()
private let backupOperationDisposable = MetaDisposable()
private let walletStateDisposable = MetaDisposable()
private var walletState: WalletContext.State?
private let previousWalletsDisposable = MetaDisposable()
private var previousWallets: [WalletContext.PreviousWallet] = []
private var previousWalletsGeneration: UInt64 = 0
private let previousWalletPhraseDisposable = MetaDisposable()
private var previousWalletPhraseGeneration: UInt64 = 0
private var isOpeningPreviousWalletPhrase = false
private weak var backupWordsController: ViewController?
private weak var disableBackupVerificationController: WalletImportScreen?
private var preparedBackupDisable: WalletContext.PreparedBackupDisable? {
didSet {
if let previous = oldValue, previous.id != self.preparedBackupDisable?.id {
self.component?.walletContext.discardPreparedBackupDisable(previous)
}
}
}
private weak var disableBackupPreparationController: AlertScreen?
private var disableBackupPreparationProgress: ValuePromise<Bool>?
private var disableBackupPreparationContent: Promise<[AnyComponentWithIdentity<AlertComponentEnvironment>]>?
private var disableBackupPreparationActions: Promise<[AlertScreen.Action]>?
private var disableBackupCheckState: AlertCheckComponent.ExternalState?
private let disableBackupChoiceDisposable = MetaDisposable()
private var disableBackupPreparationGeneration: UInt64 = 0
private var disableBackupUpdateSecretPhrase = false
private var disableBackupRequiresTopUp = false
private var disableBackupRotationPreview: WalletContext.PreparedBackupDisable?
private var disableBackupPreparationError: WalletContext.WalletError?
private var disableBackupWalletIdentity: (address: String, publicKey: String)?
private var isPreparingBackupDisable = false
private weak var disableBackupConfirmationController: AlertScreen?
private var disableBackupProgress: ValuePromise<Bool>?
private var isDisablingBackup = false
private weak var replacementOptionsController: AlertScreen?
private var replacementCreationProgress: ValuePromise<Bool>?
private var isCreatingReplacementWallet = false
private func currentPresentationData(for component: WalletSettingsScreenComponent) -> (initial: PresentationData, signal: Signal<PresentationData, NoError>) {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
return (
initial: presentationData.withUpdated(theme: self.environment?.theme ?? component.updatedPresentationData.initial.theme),
signal: component.updatedPresentationData.signal
)
}
override init(frame: CGRect) {
self.scrollView = UIScrollView()
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.scrollsToTop = true
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.alwaysBounceVertical = true
super.init(frame: frame)
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.flowSession?.invalidate()
self.backupAccessSession?.invalidate()
if let prepared = self.preparedBackupDisable { self.component?.walletContext.discardPreparedBackupDisable(prepared) }
self.operationDisposable.dispose()
self.backupOperationDisposable.dispose()
self.backupAccessDisposable.dispose()
self.disableBackupChoiceDisposable.dispose()
self.walletStateDisposable.dispose()
self.previousWalletsDisposable.dispose()
self.previousWalletPhraseDisposable.dispose()
self.debugRemoveMnemonicDisposable.dispose()
}
private func endWalletFlow() {
self.flowGeneration &+= 1
self.flowSession?.invalidate()
self.flowSession = nil
self.walletFlow = nil
}
fileprivate func abandonWalletFlow() {
self.abandonBackupAccess()
self.cancelPreviousWalletPhrase()
self.disableBackupPreparationGeneration &+= 1
self.resetBackupDisableVerificationProgress()
self.disableBackupChoiceDisposable.set(nil)
self.disableBackupPreparationContent = nil
self.disableBackupPreparationActions = nil
self.disableBackupCheckState = nil
self.disableBackupRequiresTopUp = false
self.disableBackupRotationPreview = nil
self.disableBackupPreparationError = nil
self.disableBackupWalletIdentity = nil
self.isPreparingBackupDisable = false
self.operationDisposable.set(nil)
self.backupOperationDisposable.set(nil)
self.preparedBackupDisable = nil
self.endWalletFlow()
}
private func walletFlowAuthorization(for flow: WalletFlow) -> Signal<PasscodeSession, WalletContext.WalletError> {
guard let component = self.component else { return .fail(.authorizationCancelled) }
if self.walletFlow != flow {
self.endWalletFlow()
self.walletFlow = flow
}
if let session = self.flowSession, session.isValid { return .single(session) }
let generation = self.flowGeneration
return component.walletContext.beginWalletFlow(reason: String(describing: flow))
|> deliverOnMainQueue
|> mapToSignal { [weak self] session -> Signal<PasscodeSession, WalletContext.WalletError> in
guard let self, self.flowGeneration == generation else { session.invalidate(); return .fail(.authorizationCancelled) }
self.flowSession?.invalidate()
self.flowSession = session
return .single(session)
}
}
func scrollToTop() {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
fileprivate func visibilityUpdated(_ isVisible: Bool) {
self.isVisible = isVisible
if isVisible {
if self.backupAccessRequest?.phase == .importing {
self.abandonBackupAccess()
} else {
self.resumeBackupActionIfReady()
}
} else {
self.cancelPreviousWalletPhrase()
}
}
private func reloadPreviousWallets() {
guard let component = self.component else {
return
}
self.previousWalletsGeneration &+= 1
let generation = self.previousWalletsGeneration
let walletContext = component.walletContext
self.previousWalletsDisposable.set((walletContext.previousWallets()
|> deliverOnMainQueue).start(next: { [weak self] previousWallets in
guard let self,
self.previousWalletsGeneration == generation,
self.component?.walletContext === walletContext else {
return
}
var seenAddresses = Set<String>()
self.previousWallets = previousWallets
.sorted { $0.lastUsedAt > $1.lastUsedAt }
.filter { seenAddresses.insert($0.address).inserted }
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.25))
}
}))
}
private func cancelPreviousWalletPhrase() {
self.previousWalletPhraseGeneration &+= 1
self.previousWalletPhraseDisposable.set(nil)
self.isOpeningPreviousWalletPhrase = false
}
private func openPreviousWalletPhrase(id: String) {
guard self.isVisible,
!self.isOpeningPreviousWalletPhrase,
let component = self.component,
let controller = self.environment?.controller(),
component.walletContext.stateValue.activeOperation == nil,
self.previousWallets.contains(where: { $0.id == id }) else {
return
}
self.abandonWalletFlow()
self.isOpeningPreviousWalletPhrase = true
let generation = self.previousWalletPhraseGeneration
let walletContext = component.walletContext
self.previousWalletPhraseDisposable.set((walletContext.previousWalletRecoveryPhrase(id: id)
|> deliverOnMainQueue).start(next: { [weak self, weak controller] words in
guard let self,
self.previousWalletPhraseGeneration == generation,
self.component?.walletContext === walletContext,
self.isVisible,
let controller else {
return
}
self.isOpeningPreviousWalletPhrase = false
controller.push(component.context.sharedContext.makeWalletWordsScreen(
context: component.context,
words: words,
verify: false,
dismissOnBackgroundOrLock: true,
completion: nil
))
}, error: { [weak self] error in
guard let self,
self.previousWalletPhraseGeneration == generation,
self.component?.walletContext === walletContext,
self.isVisible else {
return
}
self.isOpeningPreviousWalletPhrase = false
self.presentRecoveryPhraseError(error: error)
}))
}
private func abandonBackupAccess() {
self.backupAccessGeneration &+= 1
self.backupAccessRequest = nil
self.backupAccessDisposable.set(nil)
self.backupAccessSession?.invalidate()
self.backupAccessSession = nil
}
private func beginBackupAction(_ action: BackupAction) {
guard self.backupAccessRequest == nil,
let component = self.component,
component.walletContext.stateValue.activeOperation == nil,
case let .wallet(info) = component.walletContext.stateValue.phase,
action.isAvailable(for: info) else {
return
}
self.abandonWalletFlow()
if case .enable = action {
self.enableBackup()
return
}
if info.canSign {
self.performBackupAction(action)
return
}
self.backupAccessRequest = BackupAccessRequest(
action: action,
walletContext: component.walletContext,
address: info.address,
publicKey: info.publicKey
)
self.resolveBackupAccess()
}
private func performBackupAction(_ action: BackupAction) {
switch action {
case .enable:
self.enableBackup()
case .disable:
self.presentDisableBackupAlert()
}
}
private func backupAccessAuthorization() -> Signal<PasscodeSession, WalletContext.WalletError> {
guard let request = self.backupAccessRequest else { return .fail(.authorizationCancelled) }
if let session = self.backupAccessSession, session.isValid { return .single(session) }
let generation = self.backupAccessGeneration
return request.walletContext.beginWalletFlow(reason: "Restore wallet")
|> deliverOnMainQueue
|> mapToSignal { [weak self] session -> Signal<PasscodeSession, WalletContext.WalletError> in
guard let self, self.backupAccessGeneration == generation else {
session.invalidate()
return .fail(.authorizationCancelled)
}
self.backupAccessSession?.invalidate()
self.backupAccessSession = session
return .single(session)
}
}
private func resolveBackupAccess() {
guard let request = self.backupAccessRequest,
let component = self.component,
let controller = self.environment?.controller() else {
return
}
guard component.walletContext === request.walletContext,
case let .wallet(info) = request.walletContext.stateValue.phase,
info.address == request.address, info.publicKey == request.publicKey,
request.action.isAvailable(for: info) else {
self.abandonBackupAccess()
return
}
if info.canSign {
self.backupAccessRequest?.phase = .ready
self.resumeBackupActionIfReady()
return
}
let generation = self.backupAccessGeneration
if info.canExportPhrase {
self.backupAccessDisposable.set(performWalletAuthorizedOperation(
context: component.context,
updatedPresentationData: self.currentPresentationData(for: component),
present: { [weak controller] alert in
controller?.present(alert, in: .window(.root))
},
operation: { [weak self] password -> Signal<[String], WalletContext.WalletError> in
guard let self, self.backupAccessGeneration == generation else { return .fail(.authorizationCancelled) }
return self.backupAccessAuthorization()
|> mapToSignal { session in request.walletContext.recoveryPhrase(password: password, session: session) }
},
next: { [weak self] _ in
guard let self, self.backupAccessGeneration == generation else { return }
self.backupAccessDisposable.set(nil)
self.backupAccessSession?.invalidate()
self.backupAccessSession = nil
self.backupAccessRequest?.phase = .ready
self.resumeBackupActionIfReady()
},
failed: { [weak self] error in
guard let self, self.backupAccessGeneration == generation else { return }
self.presentBackupAccessError(error)
}
))
} else {
self.openRecoveryPhraseImport(backupAccessGeneration: generation)
}
}
private func resumeBackupActionIfReady() {
guard let request = self.backupAccessRequest else { return }
guard self.component?.walletContext === request.walletContext,
case let .wallet(info) = request.walletContext.stateValue.phase,
info.address == request.address, info.publicKey == request.publicKey,
request.action.isAvailable(for: info) else {
self.abandonBackupAccess()
return
}
guard request.phase == .ready, self.isVisible,
info.canSign, request.walletContext.stateValue.activeOperation == nil,
self.walletState?.activeOperation == nil else {
return
}
self.abandonBackupAccess()
self.performBackupAction(request.action)
}
private func presentBackupAccessError(_ error: WalletContext.WalletError) {
if error == .authorizationCancelled {
self.abandonBackupAccess()
return
}
guard let component = self.component, let controller = self.environment?.controller() else { return }
let generation = self.backupAccessGeneration
let strings = self.currentPresentationData(for: component).initial.strings
let message = walletAuthorizationErrorMessage(error, strings: strings)
controller.present(textAlertController(
context: component.context,
updatedPresentationData: self.currentPresentationData(for: component),
title: message?.title ?? strings.Wallet_RestoreErrorTitle,
text: message?.text ?? strings.Wallet_NetworkError,
actions: [
TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: { [weak self] in
guard let self, self.backupAccessGeneration == generation else { return }
self.abandonBackupAccess()
}),
TextAlertAction(type: .defaultAction, title: strings.Wallet_Retry, action: { [weak self] in
guard let self, self.backupAccessGeneration == generation else { return }
self.resolveBackupAccess()
})
],
dismissOnOutsideTap: false
), in: .window(.root))
}
private func openRecoveryPhrase() {
self.abandonWalletFlow()
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
controller.push(component.context.sharedContext.makeWalletInfoScreen(
context: component.context,
updatedPresentationData: self.currentPresentationData(for: component),
mode: .recovery,
completion: { [weak self] in
guard let self, let component = self.component, let controller = self.environment?.controller() else {
return
}
self.operationDisposable.set(performWalletAuthorizedOperation(
context: component.context,
updatedPresentationData: self.currentPresentationData(for: component),
present: { [weak controller] alert in
controller?.present(alert, in: .window(.root))
},
operation: { password in
component.walletContext.recoveryPhrase(password: password)
},
next: { words in
controller.push(component.context.sharedContext.makeWalletWordsScreen(
context: component.context,
words: words,
verify: false,
dismissOnBackgroundOrLock: true,
completion: nil
))
},
failed: { [weak self] error in
self?.presentRecoveryPhraseError(error: error)
}
))
}
))
}
private func presentRecoveryPhraseError(error: WalletContext.WalletError) {
guard error != .authorizationCancelled else { return }
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let strings = self.currentPresentationData(for: component).initial.strings
let message = walletAuthorizationErrorMessage(error, strings: strings)
controller.present(textAlertController(
context: component.context,
updatedPresentationData: self.currentPresentationData(for: component),
title: message?.title ?? strings.Wallet_Settings_ShowPhraseErrorTitle,
text: message?.text ?? strings.Wallet_NetworkError,
actions: [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {
})]
), in: .window(.root))
}
private func openRecoveryPhraseImport(backupAccessGeneration: UInt64? = nil) {
if let backupAccessGeneration {
guard self.backupAccessGeneration == backupAccessGeneration else { return }
self.backupAccessRequest?.phase = .importing
} else {
self.abandonWalletFlow()
}
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
controller.push(component.context.sharedContext.makeWalletImportScreen(
context: component.context,
mode: .enterRecoveryPhrase,
completion: { [weak self] in
self?.completeRecoveryPhraseImport(backupAccessGeneration: backupAccessGeneration)
}
))
}
private func completeRecoveryPhraseImport(backupAccessGeneration: UInt64? = nil) {
guard let component = self.component,
let settingsController = self.environment?.controller(),
let navigationController = settingsController.navigationController as? NavigationController,
let settingsControllerIndex = navigationController.viewControllers.firstIndex(where: { $0 === settingsController }) else {
return
}
let viewControllers = Array(navigationController.viewControllers.prefix(through: settingsControllerIndex))
if let backupAccessGeneration {
if self.backupAccessGeneration == backupAccessGeneration {
self.backupAccessRequest?.phase = .ready
}
navigationController.setViewControllers(viewControllers, animated: true)
return
}
let presentationData = self.currentPresentationData(for: component).initial
navigationController.setViewControllers(viewControllers, animated: true)
Queue.mainQueue().after(0.4) { [weak settingsController] in
settingsController?.present(UndoOverlayController(
presentationData: presentationData,
content: .actionSucceeded(
title: component.context.sharedContext.currentPresentationData.with { $0 }.strings.Wallet_Settings_WalletImportedTitle,
text: component.context.sharedContext.currentPresentationData.with { $0 }.strings.Wallet_Settings_WalletImportedText,
cancel: nil,
destructive: false
),
position: .bottom,
action: { _ in false }
), in: .current)
}
}
private func presentDisableBackupAlert(restarting: Bool = false) {
if restarting, self.preparedBackupDisable == nil || self.backupWordsController == nil {
return
}
guard let component = self.component,
let controller = self.environment?.controller(),
case let .wallet(info) = component.walletContext.stateValue.phase,
info.backupEnabled,
self.walletState?.activeOperation == nil,
self.disableBackupPreparationController == nil,
!self.isPreparingBackupDisable else {
return
}
let updateSecretPhrase = restarting && self.preparedBackupDisable?.updateSecretPhrase == true
if restarting {
self.dismissBackupWordsFlow()
}
self.disableBackupWalletIdentity = (info.address, info.publicKey)
self.disableBackupUpdateSecretPhrase = updateSecretPhrase
self.disableBackupRequiresTopUp = updateSecretPhrase && self.disableBackupHasLowBalance
self.disableBackupRotationPreview = nil
self.disableBackupPreparationError = nil
let checkState = AlertCheckComponent.ExternalState()
self.disableBackupCheckState = checkState
let content = Promise<[AnyComponentWithIdentity<AlertComponentEnvironment>]>()
self.disableBackupPreparationContent = content
let actions = Promise<[AlertScreen.Action]>()
self.disableBackupPreparationActions = actions
let progress = ValuePromise<Bool>(false, ignoreRepeated: true)
self.disableBackupPreparationProgress = progress
self.updateDisableBackupPreparationContent()
let alertController = AlertScreen(
configuration: AlertScreen.Configuration(dismissOnOutsideTap: false),
contentSignal: content.get(),
actionsSignal: actions.get(),
updatedPresentationData: self.currentPresentationData(for: component)
)
self.disableBackupPreparationController = alertController
alertController.dismissed = { [weak self, weak alertController] _ in
guard let self, self.disableBackupPreparationController === alertController else {
return
}
self.disableBackupPreparationController = nil
self.disableBackupPreparationProgress = nil
self.abandonWalletFlow()
}
self.disableBackupChoiceDisposable.set((checkState.valueSignal
|> deliverOnMainQueue).start(next: { [weak self, weak checkState] _ in
guard let self, let checkState,
self.disableBackupCheckState === checkState,
self.disableBackupUpdateSecretPhrase != checkState.value else {
return
}
self.updateDisableBackupPreparationState(updateSecretPhrase: checkState.value)
}))
controller.present(alertController, in: .window(.root))
if updateSecretPhrase {
self.prepareDisableBackup(openWordsWhenReady: false)
}
}
private var disableBackupHasLowBalance: Bool {
guard let balance = self.walletState?.balance.currentValue else {
return false
}
return balance < 500_000
}
private func updateDisableBackupPreparationState(updateSecretPhrase: Bool) {
guard self.disableBackupCheckState != nil else {
return
}
let requiresTopUp = updateSecretPhrase && self.disableBackupHasLowBalance
guard self.disableBackupUpdateSecretPhrase != updateSecretPhrase || self.disableBackupRequiresTopUp != requiresTopUp else {
self.updateDisableBackupPreparationContent()
return
}
self.disableBackupUpdateSecretPhrase = updateSecretPhrase
self.disableBackupRequiresTopUp = requiresTopUp
self.disableBackupPreparationGeneration &+= 1
self.backupOperationDisposable.set(nil)
if requiresTopUp {
self.disableBackupRotationPreview = nil
} else if let prepared = self.preparedBackupDisable, prepared.updateSecretPhrase {
self.disableBackupRotationPreview = prepared
}
self.preparedBackupDisable = updateSecretPhrase ? self.disableBackupRotationPreview : nil
self.isPreparingBackupDisable = false
self.disableBackupPreparationProgress?.set(false)
self.disableBackupPreparationError = nil
self.updateDisableBackupPreparationContent()
if updateSecretPhrase, !requiresTopUp, self.disableBackupRotationPreview == nil {
self.prepareDisableBackup(openWordsWhenReady: false)
}
}
private func disableBackupFeeText(_ fee: Int64) -> String {
guard let component = self.component else {
return ""
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
var text = formatTonAmountText(
fee,
dateTimeFormat: presentationData.dateTimeFormat,
maxDecimalPositions: 5,
formatString: presentationData.strings.Currency_Grams
)
if let fiat = self.walletState?.fiat,
let rate = fiat.selectedRate,
rate.unitsPerGram.isFinite, rate.unitsPerGram > 0.0 {
let fiatText = formatTonFiatValue(
fee,
rate: rate.unitsPerGram,
currencySymbol: fiat.selectedCurrency.symbol,
dateTimeFormat: presentationData.dateTimeFormat
)
if fiatText.hasSuffix("0\(presentationData.dateTimeFormat.decimalSeparator)00") {
text += " (<\(fiat.selectedCurrency.symbol)0\(presentationData.dateTimeFormat.decimalSeparator)01)"
} else {
text += " (~\(fiatText))"
}
}
return text
}
private func updateDisableBackupPreparationContent() {
guard let component = self.component,
let content = self.disableBackupPreparationContent,
let actions = self.disableBackupPreparationActions,
let progress = self.disableBackupPreparationProgress,
let checkState = self.disableBackupCheckState else {
return
}
let strings = self.currentPresentationData(for: component).initial.strings
var items: [AnyComponentWithIdentity<AlertComponentEnvironment>] = [
AnyComponentWithIdentity(
id: "title",
component: AnyComponent(AlertTitleComponent(title: strings.Wallet_Backup_DisableTitle))
),
AnyComponentWithIdentity(
id: "text",
component: AnyComponent(AlertTextComponent(content: .plain(
strings.Wallet_Backup_DisableText
)))
),
AnyComponentWithIdentity(
id: "updateSecretPhrase",
component: AnyComponent(AlertCheckComponent(
title: strings.Wallet_Backup_UpdatePhrase,
alignment: .default,
initialValue: self.disableBackupUpdateSecretPhrase,
externalState: checkState
))
)
]
if self.disableBackupRequiresTopUp {
items.append(AnyComponentWithIdentity(
id: "topUpInfo",
component: AnyComponent(AlertTextComponent(
content: .plain(strings.Wallet_Backup_UpdatePhraseTopUpText),
alignment: .center,
color: .primary,
style: .background(.small),
insets: UIEdgeInsets(top: 8.0, left: 8.0, bottom: 0.0, right: 8.0)
))
))
} else if self.disableBackupUpdateSecretPhrase, let prepared = self.preparedBackupDisable {
var text = strings.Wallet_Backup_NewPhraseText
if let fee = prepared.networkFeeNanograms {
text += "\n\n" + strings.Wallet_Backup_NetworkFee(self.disableBackupFeeText(fee)).string
}
items.append(AnyComponentWithIdentity(
id: "rotationInfo",
component: AnyComponent(AlertTextComponent(
content: .plain(text),
alignment: .center,
style: .background(.small),
insets: UIEdgeInsets(top: 8.0, left: 8.0, bottom: 0.0, right: 8.0)
))
))
}
if !self.disableBackupRequiresTopUp, let error = self.disableBackupPreparationError {
let message = self.disableBackupErrorMessage(error, strings: strings)
items.append(AnyComponentWithIdentity(
id: "preparationError",
component: AnyComponent(AlertTextComponent(
content: .plain(strings.Wallet_Backup_DisableRetryText(message.text).string),
color: .destructive,
style: .plain(.small),
insets: UIEdgeInsets(top: 8.0, left: 0.0, bottom: 0.0, right: 0.0)
))
))
}
content.set(.single(items))
actions.set(.single([
AlertScreen.Action(title: strings.Common_Cancel, action: { [weak self] in
self?.abandonWalletFlow()
}),
AlertScreen.Action(
id: "primary",
title: self.disableBackupRequiresTopUp ? strings.Wallet_Backup_TopUp : strings.Wallet_Backup_Disable,
type: self.disableBackupRequiresTopUp ? .default : .destructive,
action: { [weak self] in
guard let self else {
return
}
if self.disableBackupRequiresTopUp {
self.openDisableBackupTopUp()
} else {
self.beginDisableBackupPreparation()
}
},
autoDismiss: false,
isEnabled: progress.get() |> map { !$0 },
progress: progress.get()
)
]))
}
private func openDisableBackupTopUp() {
guard let component = self.component,
let controller = self.environment?.controller(),
let alert = self.disableBackupPreparationController,
let identity = self.disableBackupWalletIdentity,
case let .wallet(info) = component.walletContext.stateValue.phase,
info.address == identity.address, info.publicKey == identity.publicKey else {
return
}
self.disableBackupPreparationController = nil
self.disableBackupPreparationProgress = nil
self.abandonWalletFlow()
alert.dismiss { [weak controller] in
controller?.push(component.context.sharedContext.makeWalletReceiveScreen(
context: component.context,
address: info.address
))
}
}
private func beginDisableBackupPreparation() {
guard !self.isPreparingBackupDisable, !self.disableBackupRequiresTopUp else {
return
}
if self.disableBackupPreparationError == nil,
let prepared = self.preparedBackupDisable,
prepared.updateSecretPhrase == self.disableBackupUpdateSecretPhrase {
self.continueWithPreparedBackupDisable(prepared)
} else {
self.prepareDisableBackup(openWordsWhenReady: !self.disableBackupUpdateSecretPhrase)
}
}
private func prepareDisableBackup(openWordsWhenReady: Bool) {
guard !self.isPreparingBackupDisable,
!self.disableBackupRequiresTopUp,
self.disableBackupPreparationController != nil,
let component = self.component else {
return
}
self.disableBackupPreparationGeneration &+= 1
let generation = self.disableBackupPreparationGeneration
let updateSecretPhrase = self.disableBackupUpdateSecretPhrase
self.isPreparingBackupDisable = true
self.disableBackupPreparationError = nil
self.preparedBackupDisable = nil
self.disableBackupPreparationProgress?.set(true)
self.updateDisableBackupPreparationContent()
self.backupOperationDisposable.set((self.walletFlowAuthorization(for: .disableBackup)
|> mapToSignal { [weak self] session -> Signal<WalletContext.PreparedBackupDisable, WalletContext.WalletError> in
guard let self, self.disableBackupPreparationGeneration == generation else {
return .fail(.authorizationCancelled)
}
return component.walletContext.prepareDisableBackup(updateSecretPhrase: updateSecretPhrase, session: session)
}
|> deliverOnMainQueue).start(next: { [weak self] prepared in
guard let self, self.disableBackupPreparationGeneration == generation,
self.disableBackupPreparationController != nil,
self.disableBackupUpdateSecretPhrase == prepared.updateSecretPhrase else {
component.walletContext.discardPreparedBackupDisable(prepared)
return
}
self.isPreparingBackupDisable = false
self.disableBackupPreparationProgress?.set(false)
self.preparedBackupDisable = prepared
self.updateDisableBackupPreparationContent()
if openWordsWhenReady {
self.continueWithPreparedBackupDisable(prepared)
}
}, error: { [weak self] error in
guard let self, self.disableBackupPreparationGeneration == generation,
self.disableBackupPreparationController != nil else {
return
}
self.isPreparingBackupDisable = false
self.disableBackupPreparationProgress?.set(false)
if error == .authorizationCancelled {
self.endWalletFlow()
} else {
self.disableBackupPreparationError = error
}
self.updateDisableBackupPreparationContent()
}))
}
private func continueWithPreparedBackupDisable(_ prepared: WalletContext.PreparedBackupDisable) {
if let fee = prepared.networkFeeNanograms {
guard let balance = self.walletState?.balance.currentValue else {
self.disableBackupPreparationError = .network
self.updateDisableBackupPreparationContent()
return
}
guard balance >= fee else {
self.disableBackupPreparationError = .insufficientBalance(required: fee)
self.updateDisableBackupPreparationContent()
return
}
}
let generation = self.disableBackupPreparationGeneration
let presentWords = { [weak self] in
guard let self, self.disableBackupPreparationGeneration == generation,
self.preparedBackupDisable?.id == prepared.id else {
return
}
self.openBackupDisablePhrase(prepared: prepared)
}
self.disableBackupChoiceDisposable.set(nil)
self.disableBackupPreparationContent = nil
self.disableBackupPreparationActions = nil
self.disableBackupCheckState = nil
self.disableBackupRequiresTopUp = false
self.disableBackupRotationPreview = nil
self.disableBackupPreparationProgress = nil
if let alert = self.disableBackupPreparationController {
self.disableBackupPreparationController = nil
alert.dismiss(completion: presentWords)
} else {
presentWords()
}
}
private func openBackupDisablePhrase(prepared: WalletContext.PreparedBackupDisable) {
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
self.preparedBackupDisable = prepared
let wordsController = component.context.sharedContext.makeWalletWordsScreen(
context: component.context,
words: prepared.words,
mode: .backupDisable(updateSecretPhrase: prepared.updateSecretPhrase),
completion: { [weak self] in
self?.presentFinalDisableBackupAlert()
}
)
self.backupWordsController = wordsController
if let wordsController = wordsController as? ViewControllerComponentContainer {
wordsController.wasDismissed = { [weak self, weak wordsController] in
Queue.mainQueue().justDispatch { [weak self, weak wordsController] in
guard let self,
self.backupWordsController === wordsController,
self.preparedBackupDisable?.id == prepared.id else {
return
}
if let wordsController,
let navigationController = wordsController.navigationController,
navigationController.viewControllers.contains(where: { $0 === wordsController }) {
return
}
self.backupWordsController = nil
self.isDisablingBackup = false
self.isPreparingBackupDisable = false
self.abandonWalletFlow()
}
}
}
controller.push(wordsController)
}
private func resetBackupDisableVerificationProgress() {
self.disableBackupVerificationController?.setVerificationInProgress(false)
self.disableBackupVerificationController = nil
}
private func presentFinalDisableBackupAlert(refresh: Bool = true) {
guard let component = self.component,
let prepared = self.preparedBackupDisable,
let controller = self.backupWordsController?.navigationController?.topViewController as? ViewController
?? self.environment?.controller() else {
self.resetBackupDisableVerificationProgress()
return
}
if refresh {
guard !self.isDisablingBackup else { return }
let generation = self.disableBackupPreparationGeneration
self.isDisablingBackup = true
self.disableBackupVerificationController = controller as? WalletImportScreen
self.disableBackupVerificationController?.setVerificationInProgress(true)
self.backupOperationDisposable.set((self.walletFlowAuthorization(for: .disableBackup)
|> mapToSignal { session in component.walletContext.refreshPreparedBackupDisable(prepared, session: session) }
|> deliverOnMainQueue).start(next: { [weak self] updated in
guard let self, self.disableBackupPreparationGeneration == generation,
self.preparedBackupDisable?.id == prepared.id else { return }
self.isDisablingBackup = false
self.preparedBackupDisable = updated
self.presentFinalDisableBackupAlert(refresh: false)
}, error: { [weak self] error in
guard let self, self.disableBackupPreparationGeneration == generation,
self.preparedBackupDisable?.id == prepared.id else { return }
self.isDisablingBackup = false
if error == .preparedBackupDisableExpired {
self.resetBackupDisableVerificationProgress()
self.presentDisableBackupAlert(restarting: true)
return
}
self.presentFinalDisableBackupAlert(refresh: false)
self.presentDisableBackupError(error: error)
}))
return
}
let strings = self.currentPresentationData(for: component).initial.strings
let text = prepared.updateSecretPhrase
? strings.Wallet_Backup_DisableWithNewPhraseText
: strings.Wallet_Backup_DisableConfirmationText
let progress = ValuePromise<Bool>(false, ignoreRepeated: true)
let actionsEnabled = progress.get() |> map { !$0 }
let alertController = AlertScreen(
configuration: AlertScreen.Configuration(dismissOnOutsideTap: false, allowInputInset: true),
content: [
AnyComponentWithIdentity(
id: "title",
component: AnyComponent(AlertTitleComponent(title: strings.Wallet_Backup_DisableTitle))
),
AnyComponentWithIdentity(
id: "text",
component: AnyComponent(AlertTextComponent(content: .plain(text)))
)
],
actions: [
AlertScreen.Action(title: strings.Common_Cancel, action: { [weak self] in
self?.dismissBackupWordsFlow()
}, isEnabled: actionsEnabled),
AlertScreen.Action(
title: strings.Wallet_Backup_Disable,
type: .destructive,
action: { [weak self] in
self?.submitDisableBackup(prepared: prepared)
},
autoDismiss: false,
isEnabled: actionsEnabled,
progress: progress.get()
)
],
updatedPresentationData: self.currentPresentationData(for: component)
)
self.disableBackupConfirmationController = alertController
self.disableBackupProgress = progress
alertController.dismissed = { [weak self, weak alertController] _ in
guard let self, self.disableBackupConfirmationController === alertController else {
return
}
self.disableBackupConfirmationController = nil
self.disableBackupProgress = nil
self.isDisablingBackup = false
}
controller.present(alertController, in: .window(.root))
self.resetBackupDisableVerificationProgress()
}
private func submitDisableBackup(prepared: WalletContext.PreparedBackupDisable) {
guard !self.isDisablingBackup,
self.preparedBackupDisable?.id == prepared.id,
let component = self.component else {
return
}
let generation = self.disableBackupPreparationGeneration
self.isDisablingBackup = true
self.disableBackupProgress?.set(true)
self.backupOperationDisposable.set(performWalletAuthorizedOperation(
context: component.context,
updatedPresentationData: self.currentPresentationData(for: component),
present: { [weak self] alert in
self?.environment?.controller()?.present(alert, in: .window(.root))
},
operation: { [weak self] password -> Signal<WalletContext.WalletInfo, WalletContext.WalletError> in
guard let self else { return .fail(.authorizationCancelled) }
return self.walletFlowAuthorization(for: .disableBackup)
|> mapToSignal { session in
component.walletContext.disableBackup(prepared, password: password, session: session)
}
},
next: { [weak self] _ in
guard let self, self.disableBackupPreparationGeneration == generation,
self.preparedBackupDisable?.id == prepared.id else { return }
let complete: () -> Void = { [weak self] in
guard let self, self.disableBackupPreparationGeneration == generation else { return }
self.dismissBackupWordsFlow()
self.presentBackupDisabledToast()
}
if let alertController = self.disableBackupConfirmationController {
alertController.dismiss(completion: complete)
} else {
complete()
}
},
failed: { [weak self] error in
guard let self, self.disableBackupPreparationGeneration == generation,
self.preparedBackupDisable?.id == prepared.id else { return }
self.isDisablingBackup = false
self.disableBackupProgress?.set(false)
if error == .preparedBackupDisableExpired {
let restart: () -> Void = { [weak self] in
guard let self, self.disableBackupPreparationGeneration == generation,
self.preparedBackupDisable?.id == prepared.id else { return }
self.presentDisableBackupAlert(restarting: true)
}
if let alert = self.disableBackupConfirmationController {
alert.dismiss(completion: restart)
} else {
restart()
}
return
}
if case let .backupDisableNeedsConfirmation(updated) = error {
self.preparedBackupDisable = updated
let confirm: () -> Void = { [weak self] in
guard let self, self.disableBackupPreparationGeneration == generation,
self.preparedBackupDisable?.id == updated.id else { return }
self.presentFinalDisableBackupAlert(refresh: false)
}
if let alert = self.disableBackupConfirmationController {
alert.dismiss(completion: confirm)
} else {
confirm()
}
return
}
if error == .authorizationCancelled { self.endWalletFlow() }
self.presentDisableBackupError(error: error)
}
))
}
private func dismissBackupWordsFlow() {
let wordsController = self.backupWordsController
self.backupWordsController = nil
self.isDisablingBackup = false
self.abandonWalletFlow()
guard let wordsController else {
return
}
if let navigationController = wordsController.navigationController as? NavigationController,
let index = navigationController.viewControllers.firstIndex(where: { $0 === wordsController }) {
navigationController.setViewControllers(
Array(navigationController.viewControllers.prefix(upTo: index)),
animated: true
)
} else {
wordsController.dismiss()
}
}
private func disableBackupErrorMessage(_ error: WalletContext.WalletError?, strings: PresentationStrings) -> (title: String, text: String) {
switch error {
case .proofInvalid?:
return (strings.Wallet_Authorization_VerifyWalletErrorTitle, strings.Wallet_Backup_DisableVerificationErrorText)
case .proofExpired?:
return (strings.Wallet_Authorization_VerificationExpiredTitle, strings.Wallet_Backup_DisableVerificationExpiredText)
case .rotationNotFound?:
return (
strings.Wallet_Backup_PhraseUpdatePendingTitle,
strings.Wallet_Backup_DisablePhraseUpdatePendingText
)
case .keyRotationFailed?:
return (strings.Wallet_Backup_UpdatePhraseErrorTitle, strings.Wallet_Backup_DisablePhraseUpdateErrorText)
case let .insufficientBalance(required)?:
return (
strings.Wallet_Backup_InsufficientFundsTitle,
strings.Wallet_Backup_RequiredBalanceText(self.disableBackupFeeText(required)).string
)
default:
return error.flatMap { walletAuthorizationErrorMessage($0, strings: strings) }
?? (strings.Wallet_Backup_DisableErrorTitle, strings.Wallet_Backup_DisableErrorText)
}
}
private func presentDisableBackupError(error: WalletContext.WalletError? = nil) {
guard error != .authorizationCancelled else { return }
guard let component = self.component,
let controller = self.backupWordsController?.navigationController?.topViewController as? ViewController
?? self.environment?.controller() else {
return
}
let strings = self.currentPresentationData(for: component).initial.strings
let message = self.disableBackupErrorMessage(error, strings: strings)
controller.present(textAlertController(
context: component.context,
updatedPresentationData: self.currentPresentationData(for: component),
title: message.title,
text: message.text,
actions: [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]
), in: .window(.root))
}
private func reconcileBackupDisableWalletState() {
guard let identity = self.disableBackupWalletIdentity,
let walletState = self.walletState else {
return
}
switch walletState.phase {
case .restoring, .creating:
return
case let .wallet(info):
if info.address == identity.address {
if info.publicKey == identity.publicKey {
if !info.backupEnabled, let alert = self.disableBackupPreparationController {
self.disableBackupPreparationController = nil
self.disableBackupPreparationProgress = nil
self.abandonWalletFlow()
alert.dismiss()
return
}
self.updateDisableBackupPreparationState(updateSecretPhrase: self.disableBackupUpdateSecretPhrase)
return
}
if let newPublicKey = self.preparedBackupDisable?.rotation?.newPublicKey,
info.publicKey == newPublicKey.map({ String(format: "%02x", $0) }).joined() {
return
}
}
case .empty, .failed:
break
}
let preparationController = self.disableBackupPreparationController
let confirmationController = self.disableBackupConfirmationController
self.disableBackupPreparationController = nil
self.disableBackupConfirmationController = nil
self.disableBackupPreparationProgress = nil
self.disableBackupProgress = nil
self.dismissBackupWordsFlow()
preparationController?.dismiss()
confirmationController?.dismiss()
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let strings = self.currentPresentationData(for: component).initial.strings
controller.present(textAlertController(
context: component.context,
updatedPresentationData: self.currentPresentationData(for: component),
title: strings.Wallet_Backup_WalletChangedTitle,
text: strings.Wallet_Backup_DisableWalletChangedText,
actions: [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]
), in: .window(.root))
}
private func presentBackupDisabledToast() {
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let presentationData = self.currentPresentationData(for: component).initial
controller.present(UndoOverlayController(
presentationData: presentationData,
content: .actionSucceeded(
title: presentationData.strings.Wallet_Backup_DisabledTitle,
text: presentationData.strings.Wallet_Backup_DisabledText,
cancel: nil,
destructive: false
),
position: .bottom,
action: { _ in false }
), in: .current)
}
private func enableBackup() {
guard let component = self.component,
case let .wallet(info) = component.walletContext.stateValue.phase,
info.canEnableBackup, component.walletContext.stateValue.activeOperation == nil else {
return
}
guard info.canSign else {
self.openBackupPhraseInput(expectedAddress: info.address)
return
}
self.backupOperationDisposable.set((self.walletFlowAuthorization(for: .enableBackup)
|> mapToSignal { session in
component.walletContext.enableBackup(expectedAddress: info.address, session: session)
}
|> deliverOnMainQueue).start(next: { [weak self] _ in
self?.endWalletFlow()
self?.presentBackupEnabledToast()
}, error: { [weak self] error in
self?.endWalletFlow()
self?.presentEnableBackupError(error, expectedAddress: info.address)
}))
}
private func openBackupPhraseInput(expectedAddress: String) {
guard let component = self.component, let controller = self.environment?.controller(),
case let .wallet(info) = component.walletContext.stateValue.phase,
info.address == expectedAddress, info.canEnableBackup else { return }
self.abandonWalletFlow()
controller.push(component.context.sharedContext.makeWalletImportScreen(
context: component.context, mode: .enableBackup(expectedAddress: expectedAddress),
completion: { [weak self, weak controller] in
guard let self, let controller,
let navigationController = controller.navigationController as? NavigationController,
let index = navigationController.viewControllers.firstIndex(where: { $0 === controller }) else { return }
navigationController.setViewControllers(Array(navigationController.viewControllers.prefix(through: index)), animated: true)
self.presentBackupEnabledToast()
}
))
}
private func presentEnableBackupError(_ error: WalletContext.WalletError, expectedAddress: String) {
guard error != .authorizationCancelled,
let component = self.component, let controller = self.environment?.controller() else { return }
let strings = self.currentPresentationData(for: component).initial.strings
let message = walletBackupEnableErrorMessage(error, strings: strings)
var actions = [TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: {})]
switch error {
case .rotationNotFound, .proofExpired, .network:
actions.append(TextAlertAction(type: .defaultAction, title: strings.Wallet_Retry, action: { [weak self] in
guard case let .wallet(info)? = self?.component?.walletContext.stateValue.phase,
info.address == expectedAddress else { return }
self?.enableBackup()
}))
default:
break
}
switch error {
case .walletKeyMismatch, .storage(.identityMismatch), .proofInvalid, .invalidMnemonic, .rotationNotFound:
actions.append(TextAlertAction(type: .defaultAction, title: strings.Wallet_Backup_EnterCurrentPhrase, action: { [weak self] in
self?.openBackupPhraseInput(expectedAddress: expectedAddress)
}))
default:
break
}
controller.present(textAlertController(
context: component.context, updatedPresentationData: self.currentPresentationData(for: component),
title: message.title, text: message.text, actions: actions
), in: .window(.root))
}
private func presentBackupEnabledToast() {
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let presentationData = self.currentPresentationData(for: component).initial
controller.present(UndoOverlayController(
presentationData: presentationData,
content: .actionSucceeded(
title: presentationData.strings.Wallet_Backup_EnabledTitle,
text: presentationData.strings.Wallet_Backup_EnabledText,
cancel: nil,
destructive: false
),
position: .bottom,
action: { _ in false }
), in: .current)
}
private func presentBackupOperationError(_ error: WalletContext.WalletError) {
guard error != .authorizationCancelled else { return }
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let strings = self.currentPresentationData(for: component).initial.strings
let message = walletAuthorizationErrorMessage(error, strings: strings)
controller.present(textAlertController(
context: component.context,
updatedPresentationData: self.currentPresentationData(for: component),
title: message?.title ?? strings.Wallet_Backup_UpdateErrorTitle,
text: message?.text ?? strings.Wallet_NetworkError,
actions: [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]
), in: .window(.root))
}
private func presentDeleteWalletAlert() {
self.abandonWalletFlow()
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let strings = self.currentPresentationData(for: component).initial.strings
controller.present(textAlertController(
context: component.context,
updatedPresentationData: self.currentPresentationData(for: component),
title: strings.Wallet_Settings_DeleteWalletTitle,
text: strings.Wallet_Settings_DeleteWalletText,
actions: [
TextAlertAction(type: .destructiveAction, title: strings.Wallet_Settings_DeleteAnyway, action: { [weak self] in
Queue.mainQueue().after(0.25) { [weak self] in
self?.presentWalletReplacementOptionsAlert()
}
}),
TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: {})
],
actionLayout: .vertical
), in: .window(.root))
}
private func presentWalletReplacementOptionsAlert() {
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let strings = self.currentPresentationData(for: component).initial.strings
let creationProgress = ValuePromise<Bool>(false, ignoreRepeated: true)
let actionsEnabled = creationProgress.get()
|> map { !$0 }
let alertController = AlertScreen(
configuration: AlertScreen.Configuration(
actionAlignment: .vertical,
dismissOnOutsideTap: true
),
content: [
AnyComponentWithIdentity(
id: "title",
component: AnyComponent(AlertTitleComponent(
title: strings.Wallet_Settings_ReplaceWalletTitle
))
)
],
actions: [
AlertScreen.Action(
title: strings.Wallet_Settings_CreateWallet,
action: { [weak self] in
self?.createReplacementWallet()
},
autoDismiss: false,
isEnabled: actionsEnabled,
progress: creationProgress.get()
),
AlertScreen.Action(
title: strings.Wallet_Settings_ImportWallet,
action: { [weak self] in
self?.openReplacementImport()
},
autoDismiss: false,
isEnabled: actionsEnabled
)
],
updatedPresentationData: self.currentPresentationData(for: component)
)
self.replacementOptionsController = alertController
self.replacementCreationProgress = creationProgress
alertController.dismissed = { [weak self, weak alertController] outside in
guard let self, self.replacementOptionsController === alertController else {
return
}
self.replacementOptionsController = nil
self.replacementCreationProgress = nil
self.isCreatingReplacementWallet = false
if outside { self.abandonWalletFlow() }
else { self.endWalletFlow() }
}
controller.present(alertController, in: .window(.root))
}
private func openReplacementImport() {
guard !self.isCreatingReplacementWallet,
let component = self.component,
let controller = self.environment?.controller(),
let alertController = self.replacementOptionsController else {
return
}
alertController.dismiss { [weak self, weak controller] in
controller?.push(component.context.sharedContext.makeWalletImportScreen(
context: component.context,
mode: .importWallet,
completion: { [weak self] in
self?.completeWalletReplacement(
toastTitle: component.context.sharedContext.currentPresentationData.with { $0 }.strings.Wallet_Settings_WalletImportedTitle,
toastText: component.context.sharedContext.currentPresentationData.with { $0 }.strings.Wallet_Settings_WalletImportedText
)
}
))
}
}
private func completeWalletReplacement(toastTitle: String, toastText: String) {
guard let component = self.component else {
return
}
let accountManager = component.context.sharedContext.accountManager
let _ = (ApplicationSpecificNotice.resetWalletGramTooltip(accountManager: accountManager)
|> deliverOnMainQueue).startStandalone(next: { [weak self] in
guard let self,
let component = self.component,
let settingsController = self.environment?.controller(),
let navigationController = settingsController.navigationController as? NavigationController,
let settingsControllerIndex = navigationController.viewControllers.firstIndex(where: { $0 === settingsController }),
settingsControllerIndex > 0 else {
return
}
let remainingViewControllers = Array(navigationController.viewControllers.prefix(upTo: settingsControllerIndex))
guard let walletController = remainingViewControllers.last as? ViewController else {
return
}
let presentationData = self.currentPresentationData(for: component).initial
navigationController.setViewControllers(remainingViewControllers, animated: true)
Queue.mainQueue().after(0.4) { [weak walletController] in
guard let walletController else {
return
}
walletController.present(UndoOverlayController(
presentationData: presentationData,
content: .actionSucceeded(
title: toastTitle,
text: toastText,
cancel: nil,
destructive: false
),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in
return false
}
), in: .current)
}
})
}
private func createReplacementWallet() {
guard !self.isCreatingReplacementWallet,
let component = self.component,
let controller = self.environment?.controller(),
let alertController = self.replacementOptionsController,
let creationProgress = self.replacementCreationProgress else {
return
}
self.isCreatingReplacementWallet = true
creationProgress.set(true)
let createdWallet = Atomic<WalletContext.WalletInfo?>(value: nil)
self.operationDisposable.set(performWalletAuthorizedOperation(
context: component.context,
updatedPresentationData: self.currentPresentationData(for: component),
present: { [weak controller] alert in
controller?.present(alert, in: .window(.root))
},
operation: { [weak self] password -> Signal<WalletContext.WalletInfo, WalletContext.WalletError> in
guard let self else { return .fail(.authorizationCancelled) }
return self.walletFlowAuthorization(for: .create) |> mapToSignal { session in
let creation: Signal<WalletContext.WalletInfo, WalletContext.WalletError>
if let created = createdWallet.with({ $0 }) {
creation = .single(created)
} else {
creation = component.walletContext.createWallet(password: password, session: session)
}
return creation |> deliverOnMainQueue |> mapToSignal { created in
_ = createdWallet.swap(created)
return self.walletFlowAuthorization(for: .create) |> mapToSignal { session in
component.walletContext.completeWalletCreation(created, password: password, session: session)
}
}
}
},
next: { [weak self, weak alertController] _ in
self?.endWalletFlow()
let complete: () -> Void = { [weak self] in
self?.completeWalletReplacement(
toastTitle: component.context.sharedContext.currentPresentationData.with { $0 }.strings.Wallet_Settings_WalletCreatedTitle,
toastText: component.context.sharedContext.currentPresentationData.with { $0 }.strings.Wallet_Settings_WalletCreatedText
)
}
if let alertController {
alertController.dismiss(completion: complete)
} else {
complete()
}
},
failed: { [weak self] error in
guard let self else {
return
}
self.isCreatingReplacementWallet = false
self.replacementCreationProgress?.set(false)
if error == .authorizationCancelled { self.endWalletFlow() }
self.presentReplacementError(error)
}
))
}
private func presentReplacementError(_ error: WalletContext.WalletError) {
guard error != .authorizationCancelled else { return }
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let strings = self.currentPresentationData(for: component).initial.strings
let message = walletAuthorizationErrorMessage(error, strings: strings)
controller.present(textAlertController(
context: component.context,
updatedPresentationData: self.currentPresentationData(for: component),
title: message?.title ?? strings.Wallet_Settings_ReplaceWalletErrorTitle,
text: message?.text ?? strings.Wallet_NetworkError,
actions: [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]
), in: .window(.root))
}
private func debugRemoveMnemonicFromKeychain() {
guard !self.isRemovingMnemonic, let component = self.component, let controller = self.environment?.controller() else {
return
}
self.isRemovingMnemonic = true
self.debugRemoveMnemonicDisposable.set((component.walletContext.debugRemoveMnemonicFromKeychain()
|> deliverOnMainQueue).start(next: { [weak self, weak controller] _ in
guard let self else {
return
}
self.isRemovingMnemonic = false
guard let controller else {
return
}
let presentationData = self.currentPresentationData(for: component).initial
controller.present(UndoOverlayController(
presentationData: presentationData,
content: .actionSucceeded(
title: "Mnemonic Deleted",
text: "The local mnemonic was deleted from Keychain.",
cancel: nil,
destructive: false
),
position: .bottom,
action: { _ in false }
), in: .current)
}, error: { [weak self, weak controller] _ in
guard let self else {
return
}
self.isRemovingMnemonic = false
guard let controller else {
return
}
controller.present(textAlertController(
context: component.context,
updatedPresentationData: self.currentPresentationData(for: component),
title: "Couldn’t Delete Mnemonic",
text: "The mnemonic could not be deleted from Keychain. Please try again.",
actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]
), in: .window(.root))
}))
}
func update(
component: WalletSettingsScreenComponent,
availableSize: CGSize,
state: EmptyComponentState,
environment: Environment<EnvironmentType>,
transition: ComponentTransition
) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[EnvironmentType.self].value
let previousWalletContext = self.component?.walletContext
self.component = component
self.environment = environment
self.state = state
if previousWalletContext !== component.walletContext {
self.abandonBackupAccess()
self.cancelPreviousWalletPhrase()
self.previousWalletsGeneration &+= 1
self.previousWalletsDisposable.set(nil)
self.previousWallets = []
self.walletState = component.walletContext.stateValue
if self.isVisible {
self.reloadPreviousWallets()
}
let observedWalletContext = component.walletContext
self.walletStateDisposable.set((component.walletContext.state
|> deliverOnMainQueue).start(next: { [weak self] walletState in
guard let self, self.component?.walletContext === observedWalletContext else {
return
}
let previousPhase = self.walletState?.phase
let previousWalletAddress = self.walletState?.walletAddress
self.walletState = walletState
if previousPhase != walletState.phase || previousWalletAddress != walletState.walletAddress, self.isVisible {
self.reloadPreviousWallets()
}
self.reconcileBackupDisableWalletState()
self.resumeBackupActionIfReady()
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.25))
}
}))
self.reloadPreviousWallets()
}
let theme = environment.theme
self.backgroundColor = theme.list.blocksBackgroundColor
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let headerFont = Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize)
let footerFont = Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize)
let actionFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
let sideInset = 16.0 + max(environment.safeInsets.left, environment.safeInsets.right)
let sectionSpacing: CGFloat = 24.0
let sectionWidth = availableSize.width - sideInset * 2.0
var contentHeight = environment.navigationHeight + 16.0
let canDisableBackup: Bool
let canEnableBackup: Bool
let canRevealPhrase: Bool
let canEnterRecoveryPhrase: Bool
let backupEnabled: Bool
if let phase = self.walletState?.phase, case let .wallet(info) = phase {
canDisableBackup = info.backupEnabled
canEnableBackup = info.canEnableBackup
canRevealPhrase = info.canRevealPhrase
canEnterRecoveryPhrase = !info.canSign && !info.canExportPhrase
backupEnabled = info.backupEnabled
} else {
canDisableBackup = false
canEnableBackup = false
canRevealPhrase = false
canEnterRecoveryPhrase = false
backupEnabled = false
}
self.recoverySection.parentState = self.state
let recoverySectionSize = self.recoverySection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: theme,
style: .glass,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Wallet_SecretPhrase.uppercased(),
font: headerFont,
textColor: theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: canEnterRecoveryPhrase ? environment.strings.Wallet_Settings_EnterSecretPhraseInfo : environment.strings.Wallet_Settings_SecretPhraseInfo,
font: footerFont,
textColor: theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: [
AnyComponentWithIdentity(id: "showRecoveryPhrase", component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: canEnterRecoveryPhrase ? environment.strings.Wallet_Settings_EnterSecretPhrase : environment.strings.Wallet_ShowSecretPhrase,
font: actionFont,
textColor: theme.list.itemAccentColor
)),
maximumNumberOfLines: 0
)),
accessory: nil,
action: { [weak self] _ in
if canEnterRecoveryPhrase {
self?.openRecoveryPhraseImport()
} else {
self?.openRecoveryPhrase()
}
}
)))
]
)),
environment: {},
containerSize: CGSize(width: sectionWidth, height: 10000.0)
)
if canRevealPhrase || canEnterRecoveryPhrase, let recoverySectionView = self.recoverySection.view {
if recoverySectionView.superview == nil {
self.scrollView.addSubview(recoverySectionView)
}
transition.setFrame(
view: recoverySectionView,
frame: CGRect(
origin: CGPoint(x: sideInset, y: contentHeight),
size: recoverySectionSize
)
)
contentHeight += recoverySectionSize.height
contentHeight += sectionSpacing
} else {
self.recoverySection.view?.removeFromSuperview()
}
var backupItems: [AnyComponentWithIdentity<Empty>] = []
if canEnableBackup {
backupItems.append(AnyComponentWithIdentity(id: "enableBackup", component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Wallet_Settings_EnableBackup,
font: actionFont,
textColor: theme.list.itemAccentColor
)),
maximumNumberOfLines: 0
)),
accessory: self.walletState?.activeOperation == .enablingBackup ? .activity : nil,
action: { [weak self] _ in
self?.beginBackupAction(.enable)
}
))))
}
if canDisableBackup {
backupItems.append(AnyComponentWithIdentity(id: "disableBackup", component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Wallet_Settings_DisableBackup,
font: actionFont,
textColor: theme.list.itemDestructiveColor
)),
maximumNumberOfLines: 0
)),
accessory: nil,
action: { [weak self] _ in
self?.beginBackupAction(.disable)
}
))))
}
if !backupItems.isEmpty {
self.backupSection.parentState = self.state
let backupSectionSize = self.backupSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: theme,
style: .glass,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Wallet_Settings_Backup.uppercased(),
font: headerFont,
textColor: theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: backupEnabled ? environment.strings.Wallet_Settings_BackupEnabledInfo : environment.strings.Wallet_Settings_BackupDisabledInfo,
font: footerFont,
textColor: theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: backupItems
)),
environment: {},
containerSize: CGSize(width: sectionWidth, height: 10000.0)
)
if let backupSectionView = self.backupSection.view {
if backupSectionView.superview == nil {
self.scrollView.addSubview(backupSectionView)
}
transition.setFrame(
view: backupSectionView,
frame: CGRect(
origin: CGPoint(x: sideInset, y: contentHeight),
size: backupSectionSize
)
)
}
contentHeight += backupSectionSize.height
contentHeight += sectionSpacing
} else {
self.backupSection.view?.removeFromSuperview()
}
self.replacementSection.parentState = self.state
let replacementSectionSize = self.replacementSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: theme,
style: .glass,
header: nil,
footer: nil,
items: [
AnyComponentWithIdentity(id: "deleteWallet", component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Wallet_Settings_DeleteWallet,
font: actionFont,
textColor: theme.list.itemDestructiveColor
)),
maximumNumberOfLines: 0
)),
accessory: nil,
action: { [weak self] _ in
self?.presentDeleteWalletAlert()
}
)))
]
)),
environment: {},
containerSize: CGSize(width: sectionWidth, height: 10000.0)
)
if let replacementSectionView = self.replacementSection.view {
if replacementSectionView.superview == nil {
self.scrollView.addSubview(replacementSectionView)
}
transition.setFrame(
view: replacementSectionView,
frame: CGRect(
origin: CGPoint(x: sideInset, y: contentHeight),
size: replacementSectionSize
)
)
}
contentHeight += replacementSectionSize.height
if !self.previousWallets.isEmpty {
let addressFont = Font.with(size: presentationData.listsFontSize.baseDisplaySize * 15.0 / 17.0, design: .monospace)
let subtitleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)
let calendar = Calendar.current
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: environment.strings.baseLanguageCode)
dateFormatter.timeZone = calendar.timeZone
dateFormatter.setLocalizedDateFormatFromTemplate("d MMM yyyy")
let previousWalletItems: [AnyComponentWithIdentity<Empty>] = self.previousWallets.map { wallet in
let text = NSMutableAttributedString(string: "")
var addressIndex = wallet.address.startIndex
var groupIndex = 0
while addressIndex < wallet.address.endIndex {
let endIndex = wallet.address.index(addressIndex, offsetBy: 4, limitedBy: wallet.address.endIndex) ?? wallet.address.endIndex
if groupIndex != 0 {
text.append(NSAttributedString(
string: groupIndex.isMultiple(of: 6) ? "\n" : " ",
font: addressFont,
textColor: theme.list.itemPrimaryTextColor
))
}
text.append(NSAttributedString(
string: String(wallet.address[addressIndex ..< endIndex]),
font: addressFont,
textColor: (groupIndex + groupIndex / 6).isMultiple(of: 2) ? theme.list.itemPrimaryTextColor : theme.list.itemSecondaryTextColor
))
addressIndex = endIndex
groupIndex += 1
}
let balanceText: String
if let balance = wallet.balance {
balanceText = formatTonAmountText(
balance,
dateTimeFormat: environment.dateTimeFormat,
maxDecimalPositions: 2,
formatString: environment.strings.Currency_Grams
)
} else {
balanceText = environment.strings.Currency_Grams(100).replacingOccurrences(of: "100", with: "—")
}
let lastUsedDate = Date(timeIntervalSince1970: Double(wallet.lastUsedAt))
let lastUsedText: String
if calendar.isDateInToday(lastUsedDate) {
lastUsedText = environment.strings.Weekday_Today.lowercased()
} else if calendar.isDateInYesterday(lastUsedDate) {
lastUsedText = environment.strings.Weekday_Yesterday.lowercased()
} else {
lastUsedText = dateFormatter.string(from: lastUsedDate)
}
text.append(NSAttributedString(
string: environment.strings.Wallet_Settings_LastUsed(balanceText, lastUsedText).string,
font: subtitleFont,
textColor: theme.list.itemSecondaryTextColor
))
return AnyComponentWithIdentity(id: wallet.id, component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(text),
maximumNumberOfLines: 0
)),
accessory: .arrow,
action: { [weak self] _ in
self?.openPreviousWalletPhrase(id: wallet.id)
}
)))
}
contentHeight += sectionSpacing
self.previousWalletsSection.parentState = self.state
var transition = transition
if self.previousWalletsSection.view == nil {
transition = .immediate
}
let previousWalletsSectionSize = self.previousWalletsSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: theme,
style: .glass,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Wallet_Settings_PreviousWallets.uppercased(),
font: headerFont,
textColor: theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Wallet_Settings_PreviousWalletsInfo,
font: footerFont,
textColor: theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: previousWalletItems
)),
environment: {},
containerSize: CGSize(width: sectionWidth, height: 10000.0)
)
if let previousWalletsSectionView = self.previousWalletsSection.view {
if previousWalletsSectionView.superview == nil {
self.scrollView.addSubview(previousWalletsSectionView)
}
transition.setFrame(
view: previousWalletsSectionView,
frame: CGRect(
origin: CGPoint(x: sideInset, y: contentHeight),
size: previousWalletsSectionSize
)
)
}
contentHeight += previousWalletsSectionSize.height
} else {
self.previousWalletsSection.view?.removeFromSuperview()
}
#if DEBUG && false
contentHeight += sectionSpacing
self.debugSection.parentState = self.state
let debugSectionSize = self.debugSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: theme,
style: .glass,
header: nil,
footer: nil,
items: [
AnyComponentWithIdentity(id: "deleteMnemonic", component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Delete Mnemonic from Keychain",
font: actionFont,
textColor: theme.list.itemDestructiveColor
)),
maximumNumberOfLines: 0
)),
accessory: nil,
action: { [weak self] _ in
self?.debugRemoveMnemonicFromKeychain()
}
)))
]
)),
environment: {},
containerSize: CGSize(width: sectionWidth, height: 10000.0)
)
if let debugSectionView = self.debugSection.view {
if debugSectionView.superview == nil {
self.scrollView.addSubview(debugSectionView)
}
transition.setFrame(
view: debugSectionView,
frame: CGRect(
origin: CGPoint(x: sideInset, y: contentHeight),
size: debugSectionSize
)
)
}
contentHeight += debugSectionSize.height
#endif
contentHeight += 24.0 + environment.safeInsets.bottom
transition.setFrame(
view: self.scrollView,
frame: CGRect(origin: CGPoint(), size: availableSize)
)
let contentSize = CGSize(
width: availableSize.width,
height: max(contentHeight, availableSize.height + 1.0)
)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollInsets = UIEdgeInsets(
top: environment.navigationHeight,
left: 0.0,
bottom: environment.safeInsets.bottom,
right: 0.0
)
if self.scrollView.verticalScrollIndicatorInsets != scrollInsets {
self.scrollView.verticalScrollIndicatorInsets = scrollInsets
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(
view: View,
availableSize: CGSize,
state: EmptyComponentState,
environment: Environment<EnvironmentType>,
transition: ComponentTransition
) -> CGSize {
return view.update(
component: self,
availableSize: availableSize,
state: state,
environment: environment,
transition: transition
)
}
}
public final class WalletSettingsScreen: ViewControllerComponentContainer {
public init(context: AccountContext, walletContext: WalletContext) {
let updatedPresentationData = presentationDataWithDefaultAccent((
initial: context.sharedContext.currentPresentationData.with { $0 },
signal: context.sharedContext.presentationData
))
super.init(
context: context,
component: WalletSettingsScreenComponent(context: context, updatedPresentationData: updatedPresentationData, walletContext: walletContext),
navigationBarAppearance: .default,
theme: .default,
updatedPresentationData: updatedPresentationData
)
self.title = updatedPresentationData.initial.strings.Wallet_KeysAndBackup
self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? WalletSettingsScreenComponent.View else {
return
}
componentView.scrollToTop()
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? {
guard layout.metrics.widthClass == .regular else {
return nil
}
return CGSize(
width: min(480.0, layout.size.width - 20.0),
height: min(layout.size.width, layout.size.height) - 88.0
)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
(self.node.hostView.componentView as? WalletSettingsScreenComponent.View)?.visibilityUpdated(true)
}
override public func viewWillDisappear(_ animated: Bool) {
(self.node.hostView.componentView as? WalletSettingsScreenComponent.View)?.visibilityUpdated(false)
super.viewWillDisappear(animated)
}
override public func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if self.navigationController?.viewControllers.contains(where: { $0 === self }) != true {
(self.node.hostView.componentView as? WalletSettingsScreenComponent.View)?.abandonWalletFlow()
}
}
}