Signal-iOS/SignalServiceKit/ChangePhoneNumber/LegacyChangePhoneNumber.swift
2024-02-13 14:53:39 -06:00

238 lines
7.8 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import LibSignalClient
import SignalCoreKit
@objc
public class LegacyChangePhoneNumber: NSObject {
private let changePhoneNumberPniManager: ChangePhoneNumberPniManager
/// Records change-number operations that were started (by this, or
/// potentially a prior launch of the app) but not yet known to have
/// completed.
///
/// If, on a launch, we find an incomplete change-number operation in here,
/// we will attempt to recover it.
private let incompleteChangeTokenStore = IncompleteChangeTokenStore()
@objc
public override init() {
self.changePhoneNumberPniManager = DependenciesBridge.shared.changePhoneNumberPniManager
super.init()
SwiftSingletons.register(self)
AppReadiness.runNowOrWhenAppDidBecomeReadyAsync {
self.recoverInterruptedChangeNumberIfNecessary()
}
}
/// Parameters representing a successfully-completed change.
public struct SuccessfulChangeParams {
/// Our account's new E164 on the service.
let newServiceE164: E164
/// Our account's ACI on the service.
let serviceAci: Aci
/// Our account's PNI on the service.
let servicePni: Pni
public init(
newServiceE164: E164,
serviceAci: Aci,
servicePni: Pni
) {
self.newServiceE164 = newServiceE164
self.serviceAci = serviceAci
self.servicePni = servicePni
}
}
/// Mark the given change as complete, optionally including additional data
/// if the change was completed successfully.
///
/// - Parameter changeToken
/// The change to mark as complete.
/// - Parameter successfulChange
/// If the change was successful, contains parameters representing our new
/// state post-change. `nil` if the change was unsuccessful.
public func changeDidComplete(
changeToken: ChangeToken,
successfulChangeParams: SuccessfulChangeParams?,
transaction: SDSAnyWriteTransaction
) {
owsAssertDebug(CurrentAppContext().isMainApp)
if let successfulChangeParams {
do {
try updateLocalPhoneNumber(
forServiceAci: successfulChangeParams.serviceAci,
servicePni: successfulChangeParams.servicePni,
serviceE164: successfulChangeParams.newServiceE164,
transaction: transaction
)
} catch {
Logger.error("Failed to update local state while completing change!")
}
}
incompleteChangeTokenStore.clear(
changeToken: changeToken,
transaction: transaction
)
}
/// Take steps to recover from an interrupted change-number operation,
/// indicated by the presence of the given change token.
private func recoverIncompleteChangeToken(
changeToken: ChangeToken
) -> Promise<Void> {
firstly { () -> Promise<WhoAmIRequestFactory.Responses.WhoAmI> in
self.accountServiceClient.getAccountWhoAmI()
}.done(on: DispatchQueue.global()) { whoAmIResponse in
self.databaseStorage.write { transaction in
let successfulChangeParams = SuccessfulChangeParams(
newServiceE164: whoAmIResponse.e164,
serviceAci: whoAmIResponse.aci,
servicePni: whoAmIResponse.pni
)
self.changeDidComplete(
changeToken: changeToken,
successfulChangeParams: successfulChangeParams,
transaction: transaction
)
}
}.recover(on: DispatchQueue.global()) { error in
if error.isNetworkFailureOrTimeout {
// If there was a network error, we can't confirm anything.
throw error
}
self.databaseStorage.write { transaction in
self.changeDidComplete(
changeToken: changeToken,
successfulChangeParams: nil,
transaction: transaction
)
}
}
}
private func recoverInterruptedChangeNumberIfNecessary() {
guard AppReadiness.isAppReady else {
owsFailDebug("isAppReady.")
return
}
if DependenciesBridge.shared.appExpiry.isExpired {
owsFailDebug("appExpiry.")
return
}
guard
CurrentAppContext().isMainApp,
DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered
else {
return
}
let incompleteChangeToken: ChangeToken? = self.databaseStorage.read { transaction in
incompleteChangeTokenStore.existingToken(
transaction: transaction
)
}
guard let incompleteChangeToken else {
return
}
firstly { () -> Promise<Void> in
recoverIncompleteChangeToken(
changeToken: incompleteChangeToken
)
}.done(on: DispatchQueue.global()) {
Logger.info("Recovered incomplete change token!")
}.catch(on: DispatchQueue.global()) { error in
Logger.info("Failed to recover incomplete change token: \(error)")
}
}
// MARK: - Update the local phone number
/// Update local state concerning the phone number, based on values fetched
/// from the service.
///
/// - Returns
/// The persisted local phone number.
@discardableResult
private func updateLocalPhoneNumber(
forServiceAci serviceAci: Aci,
servicePni: Pni,
serviceE164: E164,
transaction: SDSAnyWriteTransaction
) throws -> E164 {
guard
let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: transaction.asV2Read),
let localE164 = E164(localIdentifiers.phoneNumber)
else {
throw OWSAssertionError("Missing or invalid local parameters!")
}
let localAci = localIdentifiers.aci
let localPni = localIdentifiers.pni
guard serviceAci == localAci else {
throw OWSAssertionError("Service ACI \(serviceAci) unexpectedly did not match local ACI!")
}
Logger.info(
"""
localAci: \(localAci),
localPni: \(localPni?.logString ?? "nil"),
localE164: \(localE164),
serviceAci: \(serviceAci),
servicePni: \(servicePni),
serviceE164: \(serviceE164)")
"""
)
let recipientMerger = DependenciesBridge.shared.recipientMerger
let localRecipient = recipientMerger.applyMergeForLocalAccount(
aci: serviceAci,
phoneNumber: serviceE164,
pni: servicePni,
tx: transaction.asV2Write
)
let recipientManager = DependenciesBridge.shared.recipientManager
recipientManager.markAsRegisteredAndSave(localRecipient, shouldUpdateStorageService: false, tx: transaction.asV2Write)
if
serviceE164 != localE164
|| servicePni != localPni
{
Logger.info(
"Recording new phone number: \(serviceE164), PNI: \(servicePni)"
)
DependenciesBridge.shared.registrationStateChangeManager.didUpdateLocalPhoneNumber(
serviceE164,
aci: serviceAci,
pni: servicePni,
tx: transaction.asV2Write
)
self.storageServiceManager.recordPendingLocalAccountUpdates()
return serviceE164
} else {
return localE164
}
}
}