Signal-iOS/Signal/Registration/ViewControllers/RegistrationNavigationController.swift
Evan Hahn f5bd3b95fe
Add (dormant) "app needs to be updated" alert during registration
If your app needs to be updated during registration, we'll show an
alert. This can happen if:

- The build has expired
- We receive an unexpected challenge (or a push challenge we cannot
  fulfill, but must)
2023-03-15 12:54:42 -05:00

329 lines
11 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public class RegistrationNavigationController: OWSNavigationController {
private let coordinator: RegistrationCoordinator
public init(coordinator: RegistrationCoordinator) {
self.coordinator = coordinator
super.init()
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if viewControllers.isEmpty, !isLoading {
pushNextController(coordinator.nextStep())
}
}
private var isLoading = false
private func pushNextController(_ step: Guarantee<RegistrationStep>) {
guard !isLoading else {
owsFailDebug("Parallel loads not allowed")
return
}
// TODO[Registration]: we want to show loading under other circumstances, too.
if self.viewControllers.isEmpty {
pushViewController(RegistrationLoadingViewController(mode: .initialLoad), animated: false, completion: nil)
}
isLoading = true
step.done(on: DispatchQueue.main) { [weak self] step in
guard let self else {
return
}
self.isLoading = false
guard let controller = self.controller(for: step) else {
return
}
var controllerToPush: UIViewController?
for viewController in self.viewControllers.reversed() {
// If we already have this controller available, update it and pop to it.
if type(of: viewController) == controller.viewType {
if let newController = controller.updateViewController(viewController) {
controllerToPush = newController
} else {
self.popToViewController(viewController, animated: true)
return
}
}
}
// If we got here, there were no matches and we should push.
let vc = controllerToPush ?? controller.makeViewController(self)
self.pushViewController(vc, animated: true, completion: nil)
}
}
private struct Controller<T: UIViewController>: AnyController {
let type: T.Type
let make: (RegistrationNavigationController) -> UIViewController
// Return a new controller to push, or nil if it should reuse the same controller.
let update: ((T) -> T?)?
var viewType: UIViewController.Type { T.self }
func makeViewController(_ presenter: RegistrationNavigationController) -> UIViewController {
return make(presenter)
}
func updateViewController<U>(_ vc: U) -> U? where U: UIViewController {
guard let vc = vc as? T else {
owsFailDebug("Invalid view controller type")
return nil
}
if let newController = update?(vc) as? U {
return newController
}
return nil
}
}
private func controller(for step: RegistrationStep) -> AnyController? {
switch step {
case .splash:
return Controller(
type: RegistrationSplashViewController.self,
make: { presenter in
return RegistrationSplashViewController(presenter: presenter)
},
// No state to update.
update: nil
)
case .permissions(let state):
return Controller(
type: RegistrationPermissionsViewController.self,
make: { presenter in
return RegistrationPermissionsViewController(state: state, presenter: presenter)
},
// The state never changes here. In theory we would build
// state update support in the permissions controller,
// but its overkill so we have not.
update: nil
)
case .phoneNumberEntry(let state):
return Controller(
type: RegistrationPhoneNumberViewController.self,
make: { presenter in
return RegistrationPhoneNumberViewController(state: state, presenter: presenter)
},
update: { controller in
controller.updateState(state)
return nil
}
)
case .verificationCodeEntry(let state):
return Controller(
type: RegistrationVerificationViewController.self,
make: { presenter in
return RegistrationVerificationViewController(state: state, presenter: presenter)
},
update: { controller in
controller.updateState(state)
return nil
}
)
case .transferSelection:
return Controller(
type: RegistrationTransferChoiceViewController.self,
make: { presenter in
return RegistrationTransferChoiceViewController(presenter: presenter)
},
// No state to update.
update: nil
)
case .pinEntry(let state):
return Controller(
type: RegistrationPinViewController.self,
make: { presenter in
return RegistrationPinViewController(state: state, presenter: presenter)
},
update: { [weak self] oldController in
// TODO[Registration]: apply updates to state.
switch (oldController.state.operation, state.operation) {
case (.confirmingNewPin, .confirmingNewPin):
return nil
case (.creatingNewPin, .creatingNewPin):
return nil
case (.enteringExistingPin, .enteringExistingPin):
return nil
default:
guard let self else { return nil }
return RegistrationPinViewController(state: state, presenter: self)
}
}
)
case .captchaChallenge:
return Controller(
type: RegistrationCaptchaViewController.self,
make: { presenter in
return RegistrationCaptchaViewController(presenter: presenter)
},
// No state to update.
update: nil
)
case .setupProfile(let state):
return Controller(
type: RegistrationProfileViewController.self,
make: { presenter in
return RegistrationProfileViewController(state: state, presenter: presenter)
},
// No state to update.
update: nil
)
case .phoneNumberDiscoverability(let state):
return Controller(
type: RegistrationPhoneNumberDiscoverabilityViewController.self,
make: { presenter in
return RegistrationPhoneNumberDiscoverabilityViewController(state: state, presenter: presenter)
},
update: nil
)
case .reglockTimeout:
fatalError("Unimplemented")
case .showErrorSheet:
fatalError("Unimplemented")
case .appUpdateBanner:
present(UIAlertController.registrationAppUpdateBanner(), animated: true)
return nil
case .done:
Logger.info("Finished with registration!")
SignalApp.shared().showConversationSplitView()
return nil
}
}
@available(*, unavailable)
required init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
fatalError("init(nibName:bundle:) has not been implemented")
}
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
let superOrientations = super.supportedInterfaceOrientations
let onboardingOrientations: UIInterfaceOrientationMask = UIDevice.current.isIPad ? .all : .portrait
return superOrientations.intersection(onboardingOrientations)
}
}
extension RegistrationNavigationController: RegistrationSplashPresenter {
public func continueFromSplash() {
pushNextController(coordinator.continueFromSplash())
}
}
extension RegistrationNavigationController: RegistrationPermissionsPresenter {
func requestPermissions() {
pushNextController(coordinator.requestPermissions())
}
}
extension RegistrationNavigationController: RegistrationPhoneNumberPresenter {
func goToNextStep(withE164 e164: String) {
pushNextController(coordinator.submitE164(e164))
}
}
extension RegistrationNavigationController: RegistrationCaptchaPresenter {
func submitCaptcha(_ token: String) {
pushNextController(coordinator.submitCaptcha(token))
}
}
extension RegistrationNavigationController: RegistrationVerificationPresenter {
func returnToPhoneNumberEntry() {
// TODO[Registration]: figure out our story on going back.
}
func requestSMSCode() {
pushNextController(coordinator.requestSMSCode())
}
func requestVoiceCode() {
pushNextController(coordinator.requestVoiceCode())
}
func submitVerificationCode(_ code: String) {
pushNextController(coordinator.submitVerificationCode(code))
}
}
extension RegistrationNavigationController: RegistrationPinPresenter {
func cancelPinConfirmation() {
pushNextController(coordinator.resetUnconfirmedPINCode())
}
func askUserToConfirmPin(_ blob: RegistrationPinConfirmationBlob) {
pushNextController(coordinator.setPINCodeForConfirmation(blob))
}
func submitPinCode(_ code: String) {
pushNextController(coordinator.submitPINCode(code))
}
func submitWithSkippedPin() {
pushNextController(coordinator.skipPINCode())
}
}
extension RegistrationNavigationController: RegistrationTransferChoicePresenter {
func transferDevice() {
// TODO[Registration]: hand off to the device transfer flow.
}
func continueRegistration() {
pushNextController(coordinator.skipDeviceTransfer())
}
}
extension RegistrationNavigationController: RegistrationProfilePresenter {
func goToNextStep(
givenName: String,
familyName: String?,
avatarData: Data?,
isDiscoverableByPhoneNumber: Bool
) {
pushNextController(
coordinator.setProfileInfo(
givenName: givenName,
familyName: familyName,
avatarData: avatarData,
isDiscoverableByPhoneNumber: isDiscoverableByPhoneNumber
)
)
}
}
// MARK: RegistrationPhoneNumberDiscoverabilityPresenter
extension RegistrationNavigationController: RegistrationPhoneNumberDiscoverabilityPresenter {
var presentedAsModal: Bool { return false }
func setPhoneNumberDiscoverability(_ isDiscoverable: Bool) {
pushNextController(coordinator.setPhoneNumberDiscoverability(isDiscoverable))
}
}
private protocol AnyController {
var viewType: UIViewController.Type { get }
func makeViewController(_: RegistrationNavigationController) -> UIViewController
func updateViewController<T: UIViewController>(_: T) -> T?
}