Skip to content

Commit

Permalink
implement account deletion
Browse files Browse the repository at this point in the history
  • Loading branch information
mojganii committed Jul 20, 2023
1 parent 5cd7172 commit 58b0b04
Show file tree
Hide file tree
Showing 17 changed files with 868 additions and 23 deletions.
2 changes: 2 additions & 0 deletions ios/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Line wrap the file at 100 chars. Th
* **Security**: in case of vulnerabilities.

## [Unreleased]
- Allow deleting account in account view.


## [2023.3 - 2023-07-15]
Expand All @@ -33,6 +34,7 @@ Line wrap the file at 100 chars. Th
not match the one stored on device.
- Allow redeeming vouchers in account view.
- Add WireGuard port selection to settings.
- Add account deletion ability to account view.

## [2023.2 - 2023-04-03]
### Changed
Expand Down
42 changes: 42 additions & 0 deletions ios/MullvadREST/RESTAccountsProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,48 @@ extension REST {
completionHandler: completion
)
}

public func deleteAccount(
accountNumber: String,
retryStrategy: RetryStrategy,
completion: @escaping CompletionHandler<Bool>
) -> Cancellable {
let requestHandler = AnyRequestHandler(createURLRequest: { endpoint, authorization in
var requestBuilder = try self.requestFactory.createRequestBuilder(
endpoint: endpoint,
method: .delete,
pathTemplate: "accounts/me"
)
requestBuilder.setAuthorization(authorization)
requestBuilder.addValue("Mullvad-Account-Number", value: accountNumber)

return requestBuilder.getRequest()
}, authorizationProvider: createAuthorizationProvider(accountNumber: accountNumber))

let responseHandler = AnyResponseHandler { response, data -> ResponseHandlerResult<Bool> in
let statusCode = HTTPStatus(rawValue: response.statusCode)

switch statusCode {
case let statusCode where statusCode.isSuccess:
return .success(true)
default:
return .unhandledResponse(
try? self.responseDecoder.decode(
ServerErrorResponse.self,
from: data
)
)
}
}

return addOperation(
name: "delete-my-account",
retryStrategy: retryStrategy,
requestHandler: requestHandler,
responseHandler: responseHandler,
completionHandler: completion
)
}
}

public struct NewAccountData: Decodable {
Expand Down
7 changes: 7 additions & 0 deletions ios/MullvadREST/RESTRequestFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ extension REST {
)
}

mutating func addValue(_ forHTTPHeaderField: String, value: String) {
restRequest.urlRequest.addValue(
value,
forHTTPHeaderField: forHTTPHeaderField
)
}

func getRequest() -> REST.Request {
restRequest
}
Expand Down
36 changes: 34 additions & 2 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -436,12 +436,18 @@
F07C0A072A52DA64009825CA /* SetupAccountCompletedCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07C0A062A52DA64009825CA /* SetupAccountCompletedCoordinator.swift */; };
F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; };
F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */; };
F0C6FA812A66E23300F521F0 /* DeleteAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C6FA802A66E23300F521F0 /* DeleteAccountOperation.swift */; };
F0E3618B2A4ADD2F00AEEF2B /* WelcomeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E3618A2A4ADD2F00AEEF2B /* WelcomeContentView.swift */; };
F0E8CC032A4C753B007ED3B4 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC022A4C753B007ED3B4 /* WelcomeViewController.swift */; };
F0E8CC052A4CC88F007ED3B4 /* WelcomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC042A4CC88F007ED3B4 /* WelcomeCoordinator.swift */; };
F0E8CC0A2A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC092A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift */; };
F0E8CC0C2A4EE672007ED3B4 /* SetupAccountCompletedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC0B2A4EE672007ED3B4 /* SetupAccountCompletedViewController.swift */; };
F0E8E4BB2A56C9F100ED26A3 /* WelcomeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4BA2A56C9F100ED26A3 /* WelcomeInteractor.swift */; };
F0E8E4C12A602CCB00ED26A3 /* AccountDeletionContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C02A602CCB00ED26A3 /* AccountDeletionContentView.swift */; };
F0E8E4C32A602E0D00ED26A3 /* AccountDeletionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C22A602E0D00ED26A3 /* AccountDeletionViewModel.swift */; };
F0E8E4C52A60499100ED26A3 /* AccountDeletionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C42A60499100ED26A3 /* AccountDeletionViewController.swift */; };
F0E8E4C72A604CBE00ED26A3 /* AccountDeletionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C62A604CBE00ED26A3 /* AccountDeletionCoordinator.swift */; };
F0E8E4C92A604E7400ED26A3 /* AccountDeletionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C82A604E7400ED26A3 /* AccountDeletionInteractor.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -1207,12 +1213,18 @@
F07C0A062A52DA64009825CA /* SetupAccountCompletedCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedCoordinator.swift; sourceTree = "<group>"; };
F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisteredDeviceInAppNotificationProvider.swift; sourceTree = "<group>"; };
F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationProviderIdentifier.swift; sourceTree = "<group>"; };
F0C6FA802A66E23300F521F0 /* DeleteAccountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountOperation.swift; sourceTree = "<group>"; };
F0E3618A2A4ADD2F00AEEF2B /* WelcomeContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeContentView.swift; sourceTree = "<group>"; };
F0E8CC022A4C753B007ED3B4 /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; };
F0E8CC042A4CC88F007ED3B4 /* WelcomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeCoordinator.swift; sourceTree = "<group>"; };
F0E8CC092A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedContentView.swift; sourceTree = "<group>"; };
F0E8CC0B2A4EE672007ED3B4 /* SetupAccountCompletedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedViewController.swift; sourceTree = "<group>"; };
F0E8E4BA2A56C9F100ED26A3 /* WelcomeInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeInteractor.swift; sourceTree = "<group>"; };
F0E8E4C02A602CCB00ED26A3 /* AccountDeletionContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionContentView.swift; sourceTree = "<group>"; };
F0E8E4C22A602E0D00ED26A3 /* AccountDeletionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionViewModel.swift; sourceTree = "<group>"; };
F0E8E4C42A60499100ED26A3 /* AccountDeletionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionViewController.swift; sourceTree = "<group>"; };
F0E8E4C62A604CBE00ED26A3 /* AccountDeletionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionCoordinator.swift; sourceTree = "<group>"; };
F0E8E4C82A604E7400ED26A3 /* AccountDeletionInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionInteractor.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -1496,6 +1508,7 @@
5823FA5726CE4A4100283BF8 /* TunnelManager */ = {
isa = PBXGroup;
children = (
F0C6FA802A66E23300F521F0 /* DeleteAccountOperation.swift */,
588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */,
58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */,
F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */,
Expand Down Expand Up @@ -1542,9 +1555,10 @@
583FE01629C196E8006E85F9 /* View controllers */ = {
isa = PBXGroup;
children = (
F0E8E4B92A55593300ED26A3 /* CreationAccount */,
583FE02029C1A0B1006E85F9 /* Account */,
F0E8E4BF2A602C7D00ED26A3 /* AccountDeletion */,
5878F4FA29CDA2D4003D4BE2 /* ChangeLog */,
F0E8E4B92A55593300ED26A3 /* CreationAccount */,
583FE01D29C197C1006E85F9 /* DeviceList */,
583FE02529C1AD0E006E85F9 /* Launch */,
583FE02129C1A0F4006E85F9 /* Login */,
Expand Down Expand Up @@ -1927,18 +1941,19 @@
isa = PBXGroup;
children = (
7AF0419D29E957EB00D492DD /* AccountCoordinator.swift */,
F0E8E4C62A604CBE00ED26A3 /* AccountDeletionCoordinator.swift */,
F07C0A042A52D4C3009825CA /* AccountRedeemingVoucherCoordinator.swift */,
58BBB39629717E0C00C8DB7C /* ApplicationCoordinator.swift */,
5893C6FB29C311E9009090D1 /* ApplicationRouter.swift */,
5878F50129CDB989003D4BE2 /* ChangeLogCoordinator.swift */,
F07C0A062A52DA64009825CA /* SetupAccountCompletedCoordinator.swift */,
58CAF9F92983E0C600BE19F7 /* LoginCoordinator.swift */,
583FE00D29C0D586006E85F9 /* OutOfTimeCoordinator.swift */,
5847D58C29B7740F008C3808 /* RevokedCoordinator.swift */,
586891CC29D452E4002A8278 /* SafariCoordinator.swift */,
587C92FD2986E28100FB9664 /* SelectLocationCoordinator.swift */,
58C3F4FA296C3AD500D72515 /* SettingsCoordinator.swift */,
F041CD552A38B0B7001B703B /* SettingsRedeemVoucherCoordinator.swift */,
F07C0A062A52DA64009825CA /* SetupAccountCompletedCoordinator.swift */,
587C92FF2986E2B600FB9664 /* TermsOfServiceCoordinator.swift */,
58F185A9298A3E3E00075977 /* TunnelCoordinator.swift */,
F0E8CC042A4CC88F007ED3B4 /* WelcomeCoordinator.swift */,
Expand Down Expand Up @@ -2327,6 +2342,17 @@
path = CreationAccount;
sourceTree = "<group>";
};
F0E8E4BF2A602C7D00ED26A3 /* AccountDeletion */ = {
isa = PBXGroup;
children = (
F0E8E4C02A602CCB00ED26A3 /* AccountDeletionContentView.swift */,
F0E8E4C82A604E7400ED26A3 /* AccountDeletionInteractor.swift */,
F0E8E4C42A60499100ED26A3 /* AccountDeletionViewController.swift */,
F0E8E4C22A602E0D00ED26A3 /* AccountDeletionViewModel.swift */,
);
path = AccountDeletion;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXHeadersBuildPhase section */
Expand Down Expand Up @@ -3180,6 +3206,7 @@
5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */,
58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */,
F0E8CC052A4CC88F007ED3B4 /* WelcomeCoordinator.swift in Sources */,
F0E8E4C92A604E7400ED26A3 /* AccountDeletionInteractor.swift in Sources */,
58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */,
5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */,
587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */,
Expand Down Expand Up @@ -3229,6 +3256,7 @@
58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */,
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */,
068CE57029278F5300A068BB /* MigrationFromV1ToV2.swift in Sources */,
F0E8E4C52A60499100ED26A3 /* AccountDeletionViewController.swift in Sources */,
F028A54B2A3370FA00C0CAA3 /* RedeemVoucherContentView.swift in Sources */,
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */,
5846227326E22A160035F7C2 /* StorePaymentObserver.swift in Sources */,
Expand Down Expand Up @@ -3285,6 +3313,8 @@
582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */,
58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */,
5864AF0829C78849005B0CD9 /* CellFactoryProtocol.swift in Sources */,
F0C6FA812A66E23300F521F0 /* DeleteAccountOperation.swift in Sources */,
F0E8E4C72A604CBE00ED26A3 /* AccountDeletionCoordinator.swift in Sources */,
F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */,
587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */,
58CAF9F82983D36800BE19F7 /* Coordinator.swift in Sources */,
Expand Down Expand Up @@ -3326,10 +3356,12 @@
586891CD29D452E4002A8278 /* SafariCoordinator.swift in Sources */,
58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */,
58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */,
F0E8E4C12A602CCB00ED26A3 /* AccountDeletionContentView.swift in Sources */,
58B26E1E2943514300D5980C /* InAppNotificationDescriptor.swift in Sources */,
58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */,
06410E04292D0F7100AFC18C /* SettingsParser.swift in Sources */,
5878A27D2909657C0096FC88 /* RevokedDeviceInteractor.swift in Sources */,
F0E8E4C32A602E0D00ED26A3 /* AccountDeletionViewModel.swift in Sources */,
58677710290975E9006F721F /* SettingsInteractorFactory.swift in Sources */,
58B26E282943527300D5980C /* SystemNotificationProvider.swift in Sources */,
58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */,
Expand Down
31 changes: 31 additions & 0 deletions ios/MullvadVPN/Coordinators/App/AccountCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import UIKit
enum AccountDismissReason: Equatable {
case none
case userLoggedOut
case accountDeletion
}

enum AddedMoreCreditOption: Equatable {
Expand Down Expand Up @@ -70,6 +71,8 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
logOut()
case .navigateToVoucher:
navigateToRedeemVoucher()
case .navigateToDeleteAccount:
navigateToDeleteAccount()
}
}

Expand Down Expand Up @@ -99,6 +102,34 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
)
}

private func navigateToDeleteAccount() {
let coordinator = AccountDeletionCoordinator(
navigationController: CustomNavigationController(),
interactor: AccountDeletionInteractor(tunnelManager: interactor.tunnelManager)
)

coordinator.start()
coordinator.didCancel = { accountDeletionCoordinator in
accountDeletionCoordinator.dismiss(animated: true)
}

coordinator.didFinish = { accountDeletionCoordinator in
accountDeletionCoordinator.dismiss(animated: true) {
self.didFinish?(self, .userLoggedOut)
}
}

presentChild(
coordinator,
animated: true,
configuration: ModalPresentationConfiguration(
preferredContentSize: UIMetrics.AccountDeletion.preferredContentSize,
modalPresentationStyle: .custom,
transitioningDelegate: FormSheetTransitioningDelegate()
)
)
}

// MARK: - Alerts

private func logOut() {
Expand Down
47 changes: 47 additions & 0 deletions ios/MullvadVPN/Coordinators/App/AccountDeletionCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// AccountDeletionCoordinator.swift
// MullvadVPN
//
// Created by Mojgan on 2023-07-13.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//

import Foundation
import UIKit

final class AccountDeletionCoordinator: Coordinator, Presentable {
private let navigationController: UINavigationController
private let interactor: AccountDeletionInteractor

var didCancel: ((AccountDeletionCoordinator) -> Void)?
var didFinish: ((AccountDeletionCoordinator) -> Void)?

var presentedViewController: UIViewController {
navigationController
}

init(
navigationController: UINavigationController,
interactor: AccountDeletionInteractor
) {
self.navigationController = navigationController
self.interactor = interactor
}

func start() {
navigationController.navigationBar.isHidden = true
let viewController = AccountDeletionViewController(interactor: interactor)
viewController.delegate = self
navigationController.pushViewController(viewController, animated: true)
}
}

extension AccountDeletionCoordinator: AccountDeletionViewControllerDelegate {
func deleteAccountDidSucceeded(controller: AccountDeletionViewController) {
didFinish?(self)
}

func deleteAccountDidCancel(controller: AccountDeletionViewController) {
didCancel?(self)
}
}
57 changes: 57 additions & 0 deletions ios/MullvadVPN/TunnelManager/DeleteAccountOperation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// DeleteAccountOperation.swift
// MullvadVPN
//
// Created by Mojgan on 2023-07-18.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadLogging
import MullvadREST
import MullvadTypes
import Operations

class DeleteAccountOperation: ResultOperation<Bool> {
private let logger = Logger(label: "\(DeleteAccountOperation.self)")

private let accountNumber: String
private let accountsProxy: REST.AccountsProxy
private var task: Cancellable?

init(
dispatchQueue: DispatchQueue,
accountsProxy: REST.AccountsProxy,
accountNumber: String
) {
self.accountNumber = accountNumber
self.accountsProxy = accountsProxy
super.init(dispatchQueue: dispatchQueue)
}

override func main() {
task = accountsProxy.deleteAccount(
accountNumber: accountNumber,
retryStrategy: .default,
completion: { [weak self] result in
self?.dispatchQueue.async {
switch result {
case let .success(isDeleted):
self?.finish(result: .success(isDeleted))
case let .failure(error):
self?.logger.error(
error: error,
message: "Failed to delete account."
)
self?.finish(result: .failure(error))
}
}
}
)
}

override func operationDidCancel() {
task?.cancel()
task = nil
}
}
Loading

0 comments on commit 58b0b04

Please sign in to comment.