Skip to content

Commit

Permalink
Merge branch 'implement-account-deletion-button-and-logic-ios-229'
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrej Mihajlov committed Aug 3, 2023
2 parents bb0d75f + e5b91ab commit 4ee496e
Show file tree
Hide file tree
Showing 20 changed files with 1,052 additions and 117 deletions.
1 change: 1 addition & 0 deletions ios/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Line wrap the file at 100 chars. Th
## [Unreleased]
### Added
- Allow redeeming vouchers in account view.
- Allow deleting account in account view.

## [2023.3 - 2023-07-15]
### Added
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<Void>
) -> 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(accountNumber, forHTTPHeaderField: "Mullvad-Account-Number")

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

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

switch statusCode {
case let statusCode where statusCode.isSuccess:
return .success(())
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(_ value: String, forHTTPHeaderField: 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 @@ -438,12 +438,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 @@ -1213,12 +1219,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 @@ -1501,6 +1513,7 @@
5823FA5726CE4A4100283BF8 /* TunnelManager */ = {
isa = PBXGroup;
children = (
F0C6FA802A66E23300F521F0 /* DeleteAccountOperation.swift */,
588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */,
58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */,
F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */,
Expand Down Expand Up @@ -1547,9 +1560,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 @@ -1931,18 +1945,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 @@ -2344,6 +2359,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 @@ -3199,6 +3225,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 @@ -3247,6 +3274,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 @@ -3303,6 +3331,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 @@ -3344,10 +3374,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
76 changes: 49 additions & 27 deletions ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ class AutomaticKeyboardResponder {
weak var targetView: UIView?
private let handler: (UIView, CGFloat) -> Void

private var showsKeyboard = false
private var lastKeyboardRect: CGRect?

private let logger = Logger(label: "AutomaticKeyboardResponder")
Expand All @@ -28,58 +27,74 @@ class AutomaticKeyboardResponder {
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillChangeFrame(_:)),
name: UIWindow.keyboardWillChangeFrameNotification,
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow(_:)),
name: UIWindow.keyboardWillShowNotification,
name: UIResponder.keyboardWillShowNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardDidHide(_:)),
name: UIWindow.keyboardDidHideNotification,
selector: #selector(keyboardWillHide(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
}

func updateContentInsets() {
guard let keyboardRect = lastKeyboardRect else { return }

adjustContentInsets(keyboardRect: keyboardRect)
adjustContentInsets(convertedKeyboardFrameEnd: keyboardRect)
}

// MARK: - Keyboard notifications

@objc private func keyboardWillShow(_ notification: Notification) {
showsKeyboard = true

addPresentationControllerObserver()
handleKeyboardNotification(notification)
}

@objc private func keyboardDidHide(_ notification: Notification) {
showsKeyboard = false
@objc private func keyboardWillHide(_ notification: Notification) {
presentationFrameObserver = nil
}

@objc private func keyboardWillChangeFrame(_ notification: Notification) {
guard showsKeyboard else { return }

handleKeyboardNotification(notification)
}

// MARK: - Private

private func handleKeyboardNotification(_ notification: Notification) {
guard let keyboardFrameValue = notification
.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? NSValue else { return }
guard let userInfo = notification.userInfo,
let targetView else { return }
// In iOS 16.1 and later, the keyboard notification object is the screen the keyboard appears on.
if #available(iOS 16.1, *) {
guard let screen = notification.object as? UIScreen,
// Get the keyboard’s frame at the end of its animation.
let keyboardFrameEnd = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }

// Use that screen to get the coordinate space to convert from.
let fromCoordinateSpace = screen.coordinateSpace

// Get your view's coordinate space.
let toCoordinateSpace: UICoordinateSpace = targetView

// Convert the keyboard's frame from the screen's coordinate space to your view's coordinate space.
let convertedKeyboardFrameEnd = fromCoordinateSpace.convert(keyboardFrameEnd, to: toCoordinateSpace)

lastKeyboardRect = keyboardFrameValue.cgRectValue
lastKeyboardRect = convertedKeyboardFrameEnd

adjustContentInsets(keyboardRect: keyboardFrameValue.cgRectValue)
adjustContentInsets(convertedKeyboardFrameEnd: convertedKeyboardFrameEnd)
} else {
guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
else { return }
let keyboardFrameEnd = keyboardValue.cgRectValue
let convertedKeyboardFrameEnd = targetView.convert(keyboardFrameEnd, from: targetView.window)
lastKeyboardRect = convertedKeyboardFrameEnd

adjustContentInsets(convertedKeyboardFrameEnd: convertedKeyboardFrameEnd)
}
}

private func addPresentationControllerObserver() {
Expand All @@ -100,7 +115,7 @@ class AutomaticKeyboardResponder {
guard let self,
let keyboardFrameValue = lastKeyboardRect else { return }

adjustContentInsets(keyboardRect: keyboardFrameValue)
adjustContentInsets(convertedKeyboardFrameEnd: keyboardFrameValue)
}
)
}
Expand All @@ -112,7 +127,6 @@ class AutomaticKeyboardResponder {
responder = responder?.next
return responder
}

return iterator.first { $0 is UIViewController } as? UIViewController
}

Expand Down Expand Up @@ -152,16 +166,24 @@ class AutomaticKeyboardResponder {
return presentationStyle == .formSheet
}

private func adjustContentInsets(keyboardRect: CGRect) {
guard let targetView, let superview = targetView.superview else { return }
private func adjustContentInsets(convertedKeyboardFrameEnd: CGRect) {
guard let targetView else { return }

// Get the safe area insets when the keyboard is offscreen.
var bottomOffset = targetView.safeAreaInsets.bottom

// Compute the target view frame within screen coordinates
let screenRect = superview.convert(targetView.frame, to: nil)
// Get the intersection between the keyboard's frame and the view's bounds to work with the
// part of the keyboard that overlaps your view.
let viewIntersection = targetView.bounds.intersection(convertedKeyboardFrameEnd)

// Find the intersection between the keyboard and the view
let intersection = keyboardRect.intersection(screenRect)
// Check whether the keyboard intersects your view before adjusting your offset.
if !viewIntersection.isEmpty {
// Adjust the offset by the difference between the view's height and the height of the
// intersection rectangle.
bottomOffset = targetView.bounds.maxY - viewIntersection.minY
}

handler(targetView, intersection.height)
handler(targetView, bottomOffset)
}
}

Expand Down
Loading

0 comments on commit 4ee496e

Please sign in to comment.