Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coordinate alert presentation #5044

Merged
merged 1 commit into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ios/.swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ line_length:
ignores_interpolated_strings: true
warning: 120
error: 300
cyclomatic_complexity:
ignores_case_statements: true

type_name:
min_length: 4
Expand Down
38 changes: 27 additions & 11 deletions ios/MullvadVPN.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

20 changes: 18 additions & 2 deletions ios/MullvadVPN/Classes/AppRoutes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,17 @@ enum AppRouteGroup: AppRouteGroupProtocol {
*/
case changelog

/**
Alert group. Alert id should match the id of the alert being contained.
*/
case alert(_ alertId: String)

var isModal: Bool {
switch self {
case .primary:
return UIDevice.current.userInterfaceIdiom == .pad

case .selectLocation, .account, .settings, .changelog:
case .selectLocation, .account, .settings, .changelog, .alert:
return true
}
}
Expand All @@ -55,6 +60,9 @@ enum AppRouteGroup: AppRouteGroupProtocol {
return 0
case .settings, .account, .selectLocation, .changelog:
return 1
case .alert:
// Alerts should always be topmost.
return .max
}
}
}
Expand Down Expand Up @@ -83,14 +91,20 @@ enum AppRoute: AppRouteProtocol {
*/
case changelog

/**
Alert route. Alert id must be a unique string in order to produce a unique route
that distinguishes between different kinds of alerts.
*/
case alert(_ alertId: String)

/**
Routes that are part of primary horizontal navigation group.
*/
case tos, login, main, revoked, outOfTime, welcome

var isExclusive: Bool {
switch self {
case .selectLocation, .account, .settings, .changelog:
case .selectLocation, .account, .settings, .changelog, .alert:
return true
default:
return false
Expand All @@ -117,6 +131,8 @@ enum AppRoute: AppRouteProtocol {
return .account
case .settings:
return .settings
case let .alert(id):
return .alert(id)
}
}
}
60 changes: 29 additions & 31 deletions ios/MullvadVPN/Coordinators/AccountCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,12 @@ enum AddedMoreCreditOption: Equatable {
final class AccountCoordinator: Coordinator, Presentable, Presenting {
private let interactor: AccountInteractor
private var accountController: AccountViewController?
private let alertPresenter = AlertPresenter()

let navigationController: UINavigationController
var presentedViewController: UIViewController {
navigationController
}

var presentationContext: UIViewController {
navigationController
}

var didFinish: ((AccountCoordinator, AccountDismissReason) -> Void)?

init(
Expand All @@ -49,10 +44,7 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {

let accountController = AccountViewController(
interactor: interactor,
errorPresenter: PaymentAlertPresenter(
presentationController: presentationContext,
alertPresenter: alertPresenter
)
errorPresenter: PaymentAlertPresenter(alertContext: self)
)

accountController.actionHandler = handleViewControllerAction
Expand Down Expand Up @@ -141,19 +133,25 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
// MARK: - Alerts

private func logOut() {
let alertController = CustomAlertViewController(icon: .spinner)
let presentation = AlertPresentation(
id: "account-logout-alert",
icon: .spinner,
message: nil,
buttons: []
)

alertPresenter.enqueue(alertController, presentingController: presentationContext) {
self.interactor.logout {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in
guard let self else { return }
let alertPresenter = AlertPresenter(context: self)

alertController.dismiss(animated: true) {
self.didFinish?(self, .userLoggedOut)
}
}
interactor.logout {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in
guard let self else { return }

alertPresenter.dismissAlert(presentation: presentation, animated: true)
self.didFinish?(self, .userLoggedOut)
}
}

alertPresenter.showAlert(presentation: presentation, animated: true)
}

private func showAccountDeviceInfo() {
Expand All @@ -172,21 +170,21 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
comment: ""
)

let alertController = CustomAlertViewController(
let presentation = AlertPresentation(
id: "account-device-info-alert",
message: message,
icon: .info
)

alertController.addAction(
title: NSLocalizedString(
"DEVICE_INFO_DIALOG_OK_ACTION",
tableName: "Account",
value: "Got it!",
comment: ""
),
style: .default
buttons: [AlertAction(
title: NSLocalizedString(
"DEVICE_INFO_DIALOG_OK_ACTION",
tableName: "Account",
value: "Got it!",
comment: ""
),
style: .default
)]
)

alertPresenter.enqueue(alertController, presentingController: presentationContext)
let presenter = AlertPresenter(context: self)
presenter.showAlert(presentation: presentation, animated: true)
}
}
45 changes: 45 additions & 0 deletions ios/MullvadVPN/Coordinators/AlertCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// AlertCoordinator.swift
// MullvadVPN
//
// Created by Jon Petersson on 2023-08-23.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//

import MullvadLogging
import Routing
import UIKit

final class AlertCoordinator: Coordinator, Presentable {
private var alertController: AlertViewController?
private let presentation: AlertPresentation

var didFinish: (() -> Void)?

var presentedViewController: UIViewController {
return alertController!
}

init(presentation: AlertPresentation) {
self.presentation = presentation
}

func start() {
let alertController = AlertViewController(
header: presentation.header,
title: presentation.title,
message: presentation.message,
icon: presentation.icon
)

self.alertController = alertController

alertController.onDismiss = { [weak self] in
self?.didFinish?()
}

presentation.buttons.forEach { action in
alertController.addAction(title: action.title, style: action.style, handler: action.handler)
}
}
}
70 changes: 40 additions & 30 deletions ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ import UIKit
Application coordinator managing split view and two navigation contexts.
*/
final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewControllerDelegate,
UISplitViewControllerDelegate, ApplicationRouterDelegate,
NotificationManagerDelegate {
UISplitViewControllerDelegate, ApplicationRouterDelegate, NotificationManagerDelegate {
typealias RouteType = AppRoute

/**
Application router.
*/
private var router: ApplicationRouter<AppRoute>!
private(set) var router: ApplicationRouter<AppRoute>!

/**
Primary navigation container.
Expand Down Expand Up @@ -127,11 +126,11 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo

func applicationRouter(
_ router: ApplicationRouter<RouteType>,
route: AppRoute,
presentWithContext context: RoutePresentationContext<RouteType>,
animated: Bool,
completion: @escaping (Coordinator) -> Void
) {
switch route {
switch context.route {
case .account:
presentAccount(animated: animated, completion: completion)

Expand Down Expand Up @@ -161,6 +160,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo

case .welcome:
presentWelcome(animated: animated, completion: completion)

case .alert:
presentAlert(animated: animated, context: context, completion: completion)
}
}

Expand All @@ -169,15 +171,15 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
dismissWithContext context: RouteDismissalContext<RouteType>,
completion: @escaping () -> Void
) {
if context.isClosing {
let dismissedRoute = context.dismissedRoutes.first!
let dismissedRoute = context.dismissedRoutes.first!

if context.isClosing {
switch dismissedRoute.route.routeGroup {
case .primary:
endHorizontalFlow(animated: context.isAnimated, completion: completion)
context.dismissedRoutes.forEach { $0.coordinator.removeFromParent() }

case .selectLocation, .account, .settings, .changelog:
case .selectLocation, .account, .settings, .changelog, .alert:
guard let coordinator = dismissedRoute.coordinator as? Presentable else {
completion()
return assertionFailure("Expected presentable coordinator for \(dismissedRoute.route)")
Expand All @@ -186,13 +188,13 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
coordinator.dismiss(animated: context.isAnimated, completion: completion)
}
} else {
let dismissedRoute = context.dismissedRoutes.first!
assert(context.dismissedRoutes.count == 1)

if dismissedRoute.route == .outOfTime {
guard let coordinator = dismissedRoute.coordinator as? OutOfTimeCoordinator else {
switch dismissedRoute.route {
case .outOfTime, .welcome:
guard let coordinator = dismissedRoute.coordinator as? Poppable else {
completion()
return assertionFailure("Unhandled coordinator for \(dismissedRoute.route)")
return assertionFailure("Expected presentable coordinator for \(dismissedRoute.route)")
}

coordinator.popFromNavigationStack(
Expand All @@ -201,19 +203,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
)

coordinator.removeFromParent()
} else if dismissedRoute.route == .welcome {
guard let coordinator = dismissedRoute.coordinator as? WelcomeCoordinator else {
completion()
return assertionFailure("Unhandled coordinator for \(dismissedRoute.route)")
}

coordinator.popFromNavigationStack(
animated: context.isAnimated,
completion: completion
)

coordinator.removeFromParent()
} else {
default:
assertionFailure("Unhandled dismissal for \(dismissedRoute.route)")
completion()
}
Expand Down Expand Up @@ -579,11 +570,10 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
)

coordinator.didFinishPayment = { [weak self] _ in
guard let self else { return }
guard let self = self else { return }

if shouldDismissOutOfTime() {
router.dismiss(.outOfTime, animated: true)

continueFlow(animated: true)
}
}
Expand Down Expand Up @@ -628,10 +618,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
!(tunnelManager.deviceState.accountData?.isExpired ?? false)
}

private func presentSelectLocation(
animated: Bool,
completion: @escaping (Coordinator) -> Void
) {
private func presentSelectLocation(animated: Bool, completion: @escaping (Coordinator) -> Void) {
let coordinator = makeSelectLocationCoordinator(forModalPresentation: true)
coordinator.start()

Expand Down Expand Up @@ -664,6 +651,29 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
}
}

private func presentAlert(
animated: Bool,
context: RoutePresentationContext<RouteType>,
completion: @escaping (Coordinator) -> Void
) {
guard let metadata = context.metadata as? AlertMetadata else {
assertionFailure("Could not get AlertMetadata from RoutePresentationContext.")
return
}

let coordinator = AlertCoordinator(presentation: metadata.presentation)

coordinator.didFinish = { [weak self] in
self?.router.dismiss(context.route)
}

coordinator.start()

metadata.context.presentChild(coordinator, animated: animated) {
completion(coordinator)
}
}

private func makeTunnelCoordinator() -> TunnelCoordinator {
let tunnelCoordinator = TunnelCoordinator(tunnelManager: tunnelManager)

Expand Down
5 changes: 3 additions & 2 deletions ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Routing
import UIKit

final class ChangeLogCoordinator: Coordinator, Presentable {
private var alertController: CustomAlertViewController?
private var alertController: AlertViewController?
private let interactor: ChangeLogInteractor
var didFinish: ((ChangeLogCoordinator) -> Void)?

Expand All @@ -24,7 +24,7 @@ final class ChangeLogCoordinator: Coordinator, Presentable {
}

func start() {
let alertController = CustomAlertViewController(
let alertController = AlertViewController(
header: interactor.viewModel.header,
title: interactor.viewModel.title,
attributedMessage: interactor.viewModel.body
Expand All @@ -43,6 +43,7 @@ final class ChangeLogCoordinator: Coordinator, Presentable {
didFinish?(self)
}
)

self.alertController = alertController
}
}
Loading
Loading