summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-04-08 16:51:28 +0200
committerJon Petersson <jon.petersson@mullvad.net>2025-04-08 16:51:28 +0200
commit9a6938a4a32ecc162cc5afcf15fa636de2f9b9fe (patch)
treeb19fdb5f1a4b299de54d323965be32a3756270d6
parent5a53a0479d33d9cdab1f3859706fb2ff776ee56a (diff)
parent4ae7d50075a6e82a0d1edabf26ce13d9357479cb (diff)
downloadmullvadvpn-9a6938a4a32ecc162cc5afcf15fa636de2f9b9fe.tar.xz
mullvadvpn-9a6938a4a32ecc162cc5afcf15fa636de2f9b9fe.zip
Merge branch 'use-mullvad-api-instead-of-urlsession-in-accounts-proxy-ios-982'
-rw-r--r--ios/MullvadMockData/MullvadREST/AccountsProxy+Stubs.swift25
-rw-r--r--ios/MullvadMockData/MullvadTypes/NewAccountDataMock.swift23
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift31
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTAccountsProxy.swift49
-rw-r--r--ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift6
-rw-r--r--ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift31
-rw-r--r--ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAccountProxy.swift125
-rw-r--r--ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift16
-rw-r--r--ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift20
-rw-r--r--ios/MullvadRustRuntime/include/mullvad_rust_runtime.h70
-rw-r--r--ios/MullvadTypes/NewAccountData.swift35
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj12
-rw-r--r--ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift2
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift5
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift3
-rw-r--r--ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift14
-rw-r--r--ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift10
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift3
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift8
-rw-r--r--ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift6
-rw-r--r--mullvad-api/src/lib.rs28
-rw-r--r--mullvad-ios/src/api_client/account.rs196
-rw-r--r--mullvad-ios/src/api_client/api.rs20
-rw-r--r--mullvad-ios/src/api_client/completion.rs10
-rw-r--r--mullvad-ios/src/api_client/mod.rs29
-rw-r--r--mullvad-ios/src/api_client/response.rs14
26 files changed, 658 insertions, 133 deletions
diff --git a/ios/MullvadMockData/MullvadREST/AccountsProxy+Stubs.swift b/ios/MullvadMockData/MullvadREST/AccountsProxy+Stubs.swift
index 0b7f62adee..ebb69f1b03 100644
--- a/ios/MullvadMockData/MullvadREST/AccountsProxy+Stubs.swift
+++ b/ios/MullvadMockData/MullvadREST/AccountsProxy+Stubs.swift
@@ -13,25 +13,28 @@ import MullvadTypes
struct AccountProxyStubError: Error {}
struct AccountsProxyStub: RESTAccountHandling {
- var createAccountResult: Result<REST.NewAccountData, Error> = .failure(AccountProxyStubError())
+ var createAccountResult: Result<NewAccountData, Error> = .failure(AccountProxyStubError())
var deleteAccountResult: Result<Void, Error> = .failure(AccountProxyStubError())
func createAccount(
retryStrategy: REST.RetryStrategy,
- completion: @escaping ProxyCompletionHandler<REST.NewAccountData>
+ completion: @escaping ProxyCompletionHandler<NewAccountData>
) -> Cancellable {
completion(createAccountResult)
return AnyCancellable()
}
- func getAccountData(accountNumber: String) -> any RESTRequestExecutor<Account> {
- RESTRequestExecutorStub<Account>(success: {
- Account(
- id: accountNumber,
- expiry: Calendar.current.date(byAdding: .day, value: 38, to: Date())!,
- maxDevices: 1,
- canAddDevices: true
- )
- })
+ func getAccountData(
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping ProxyCompletionHandler<Account>
+ ) -> Cancellable {
+ completion(.success(Account(
+ id: accountNumber,
+ expiry: Calendar.current.date(byAdding: .day, value: 38, to: Date())!,
+ maxDevices: 1,
+ canAddDevices: true
+ )))
+ return AnyCancellable()
}
func deleteAccount(
diff --git a/ios/MullvadMockData/MullvadTypes/NewAccountDataMock.swift b/ios/MullvadMockData/MullvadTypes/NewAccountDataMock.swift
new file mode 100644
index 0000000000..f50b990f65
--- /dev/null
+++ b/ios/MullvadMockData/MullvadTypes/NewAccountDataMock.swift
@@ -0,0 +1,23 @@
+//
+// NewAccountDataMock.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2025-04-08.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadTypes
+
+extension NewAccountData {
+ public static func mockValue() -> NewAccountData {
+ return NewAccountData(
+ id: UUID().uuidString,
+ expiry: Date().addingTimeInterval(3600),
+ maxPorts: 2,
+ canAddPorts: false,
+ maxDevices: 5,
+ canAddDevices: false,
+ number: "1234567890123456"
+ )
+ }
+}
diff --git a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift
index 0d75b25d74..d45745446e 100644
--- a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift
+++ b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift
@@ -12,37 +12,6 @@ import MullvadTypes
import Operations
import WireGuardKitTypes
-public protocol APIQuerying: Sendable {
- func getAddressList(
- retryStrategy: REST.RetryStrategy,
- completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
- ) -> Cancellable
-
- func getRelays(
- etag: String?,
- retryStrategy: REST.RetryStrategy,
- completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.ServerRelaysCacheResponse>
- ) -> Cancellable
-
- func createApplePayment(
- accountNumber: String,
- receiptString: Data
- ) -> any RESTRequestExecutor<REST.CreateApplePaymentResponse>
-
- func sendProblemReport(
- _ body: REST.ProblemReportRequest,
- retryStrategy: REST.RetryStrategy,
- completionHandler: @escaping @Sendable ProxyCompletionHandler<Void>
- ) -> Cancellable
-
- func submitVoucher(
- voucherCode: String,
- accountNumber: String,
- retryStrategy: REST.RetryStrategy,
- completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.SubmitVoucherResponse>
- ) -> Cancellable
-}
-
extension REST {
public final class APIProxy: Proxy<AuthProxyConfiguration>, APIQuerying, @unchecked Sendable {
public init(configuration: AuthProxyConfiguration) {
diff --git a/ios/MullvadREST/ApiHandlers/RESTAccountsProxy.swift b/ios/MullvadREST/ApiHandlers/RESTAccountsProxy.swift
index 79e9e6dd2a..9bcc9c70b0 100644
--- a/ios/MullvadREST/ApiHandlers/RESTAccountsProxy.swift
+++ b/ios/MullvadREST/ApiHandlers/RESTAccountsProxy.swift
@@ -9,21 +9,6 @@
import Foundation
import MullvadTypes
-public protocol RESTAccountHandling: Sendable {
- func createAccount(
- retryStrategy: REST.RetryStrategy,
- completion: @escaping @Sendable ProxyCompletionHandler<REST.NewAccountData>
- ) -> Cancellable
-
- func getAccountData(accountNumber: String) -> any RESTRequestExecutor<Account>
-
- func deleteAccount(
- accountNumber: String,
- retryStrategy: REST.RetryStrategy,
- completion: @escaping ProxyCompletionHandler<Void>
- ) -> Cancellable
-}
-
extension REST {
public final class AccountsProxy: Proxy<AuthProxyConfiguration>, RESTAccountHandling, @unchecked Sendable {
public init(configuration: AuthProxyConfiguration) {
@@ -64,7 +49,11 @@ extension REST {
return executor.execute(retryStrategy: retryStrategy, completionHandler: completion)
}
- public func getAccountData(accountNumber: String) -> any RESTRequestExecutor<Account> {
+ public func getAccountData(
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping ProxyCompletionHandler<Account>
+ ) -> Cancellable {
let requestHandler = AnyRequestHandler(
createURLRequest: { endpoint, authorization in
var requestBuilder = try self.requestFactory.createRequestBuilder(
@@ -85,11 +74,13 @@ extension REST {
with: responseDecoder
)
- return makeRequestExecutor(
+ let executor = makeRequestExecutor(
name: "get-my-account",
requestHandler: requestHandler,
responseHandler: responseHandler
)
+
+ return executor.execute(retryStrategy: retryStrategy, completionHandler: completion)
}
public func deleteAccount(
@@ -134,28 +125,4 @@ extension REST {
return executor.execute(retryStrategy: retryStrategy, completionHandler: completion)
}
}
-
- public struct NewAccountData: Decodable, Sendable {
- public let id: String
- public let expiry: Date
- public let maxPorts: Int
- public let canAddPorts: Bool
- public let maxDevices: Int
- public let canAddDevices: Bool
- public let number: String
- }
-}
-
-extension REST.NewAccountData {
- public static func mockValue() -> REST.NewAccountData {
- return REST.NewAccountData(
- id: UUID().uuidString,
- expiry: Date().addingTimeInterval(3600),
- maxPorts: 2,
- canAddPorts: false,
- maxDevices: 5,
- canAddDevices: false,
- number: "1234567890123456"
- )
- }
}
diff --git a/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift b/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift
index c6d6fab25e..47a157dd8a 100644
--- a/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift
+++ b/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift
@@ -138,7 +138,11 @@ extension REST {
public let wireguard: ServerWireguardTunnels
public let bridge: ServerBridges
- public init(locations: [String: ServerLocation], wireguard: ServerWireguardTunnels, bridge: ServerBridges) {
+ public init(
+ locations: [String: ServerLocation],
+ wireguard: ServerWireguardTunnels,
+ bridge: ServerBridges
+ ) {
self.locations = locations
self.wireguard = wireguard
self.bridge = bridge
diff --git a/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift
index be478ecb59..5d2555f07f 100644
--- a/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift
+++ b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift
@@ -11,6 +11,37 @@ import MullvadTypes
import Operations
import WireGuardKitTypes
+public protocol APIQuerying: Sendable {
+ func getAddressList(
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
+ ) -> Cancellable
+
+ func getRelays(
+ etag: String?,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.ServerRelaysCacheResponse>
+ ) -> Cancellable
+
+ func createApplePayment(
+ accountNumber: String,
+ receiptString: Data
+ ) -> any RESTRequestExecutor<REST.CreateApplePaymentResponse>
+
+ func sendProblemReport(
+ _ body: REST.ProblemReportRequest,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping @Sendable ProxyCompletionHandler<Void>
+ ) -> Cancellable
+
+ func submitVoucher(
+ voucherCode: String,
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.SubmitVoucherResponse>
+ ) -> Cancellable
+}
+
extension REST {
public final class MullvadAPIProxy: APIQuerying, @unchecked Sendable {
let transportProvider: APITransportProviderProtocol
diff --git a/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAccountProxy.swift b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAccountProxy.swift
new file mode 100644
index 0000000000..7ee228b8a5
--- /dev/null
+++ b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAccountProxy.swift
@@ -0,0 +1,125 @@
+//
+// MullvadAccountProxy.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2025-03-31.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadRustRuntime
+import MullvadTypes
+import Operations
+import WireGuardKitTypes
+
+public protocol RESTAccountHandling: Sendable {
+ func createAccount(
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping @Sendable ProxyCompletionHandler<NewAccountData>
+ ) -> Cancellable
+
+ func getAccountData(
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping @Sendable ProxyCompletionHandler<Account>
+ ) -> Cancellable
+
+ func deleteAccount(
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping ProxyCompletionHandler<Void>
+ ) -> Cancellable
+}
+
+extension REST {
+ public final class MullvadAccountProxy: RESTAccountHandling, @unchecked Sendable {
+ let transportProvider: APITransportProviderProtocol
+ let dispatchQueue: DispatchQueue
+ let operationQueue = AsyncOperationQueue()
+ let responseDecoder: JSONDecoder
+
+ public init(
+ transportProvider: APITransportProviderProtocol,
+ dispatchQueue: DispatchQueue,
+ responseDecoder: JSONDecoder
+ ) {
+ self.transportProvider = transportProvider
+ self.dispatchQueue = dispatchQueue
+ self.responseDecoder = responseDecoder
+ }
+
+ public func createAccount(
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping ProxyCompletionHandler<NewAccountData>
+ ) -> Cancellable {
+ let responseHandler = rustResponseHandler(
+ decoding: NewAccountData.self,
+ with: responseDecoder
+ )
+
+ return createNetworkOperation(
+ request: .createAccount(retryStrategy),
+ responseHandler: responseHandler,
+ completionHandler: completion
+ )
+ }
+
+ public func getAccountData(
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping ProxyCompletionHandler<Account>
+ ) -> Cancellable {
+ let responseHandler = rustResponseHandler(
+ decoding: Account.self,
+ with: responseDecoder
+ )
+
+ return createNetworkOperation(
+ request: .getAccount(retryStrategy, accountNumber: accountNumber),
+ responseHandler: responseHandler,
+ completionHandler: completion
+ )
+ }
+
+ public func deleteAccount(
+ accountNumber: String,
+ retryStrategy: RetryStrategy,
+ completion: @escaping ProxyCompletionHandler<Void>
+ ) -> Cancellable {
+ let request = APIRequest.deleteAccount(retryStrategy, accountNumber: accountNumber)
+
+ let networkOperation = MullvadApiNetworkOperation(
+ name: request.name,
+ dispatchQueue: dispatchQueue,
+ request: request,
+ transportProvider: transportProvider,
+ responseDecoder: responseDecoder,
+ responseHandler: rustEmptyResponseHandler(),
+ completionHandler: completion
+ )
+
+ operationQueue.addOperation(networkOperation)
+
+ return networkOperation
+ }
+
+ private func createNetworkOperation<Success: Decodable>(
+ request: APIRequest,
+ responseHandler: RustResponseHandler<Success>,
+ completionHandler: @escaping @Sendable ProxyCompletionHandler<Success>
+ ) -> MullvadApiNetworkOperation<Success> {
+ let networkOperation = MullvadApiNetworkOperation(
+ name: request.name,
+ dispatchQueue: dispatchQueue,
+ request: request,
+ transportProvider: transportProvider,
+ responseDecoder: responseDecoder,
+ responseHandler: responseHandler,
+ completionHandler: completionHandler
+ )
+
+ operationQueue.addOperation(networkOperation)
+
+ return networkOperation
+ }
+ }
+}
diff --git a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift
index ea51e22508..68e7e41663 100644
--- a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift
+++ b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift
@@ -9,6 +9,9 @@
public enum APIRequest: Codable, Sendable {
case getAddressList(_ retryStrategy: REST.RetryStrategy)
case getRelayList(_ retryStrategy: REST.RetryStrategy, etag: String?)
+ case createAccount(_ retryStrategy: REST.RetryStrategy)
+ case getAccount(_ retryStrategy: REST.RetryStrategy, accountNumber: String)
+ case deleteAccount(_ retryStrategy: REST.RetryStrategy, accountNumber: String)
var name: String {
switch self {
@@ -16,12 +19,23 @@ public enum APIRequest: Codable, Sendable {
"get-address-list"
case .getRelayList:
"get-relay-list"
+ case .createAccount:
+ "create-account"
+ case .getAccount:
+ "get-account"
+ case .deleteAccount:
+ "delete-account"
}
}
var retryStrategy: REST.RetryStrategy {
switch self {
- case let .getAddressList(strategy), let .getRelayList(strategy, _):
+ case
+ let .getAddressList(strategy),
+ let .getRelayList(strategy, _),
+ let .createAccount(strategy),
+ let .getAccount(strategy, _),
+ let .deleteAccount(strategy, _):
strategy
}
}
diff --git a/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift b/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift
index fd64408e0e..298d3ac31f 100644
--- a/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift
+++ b/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift
@@ -39,6 +39,26 @@ public struct MullvadApiRequestFactory: Sendable {
retryStrategy.toRustStrategy(),
etag
))
+ case let .getAccount(retryStrategy, accountNumber: accountNumber):
+ MullvadApiCancellable(handle: mullvad_api_get_account(
+ apiContext.context,
+ rawCompletionPointer,
+ retryStrategy.toRustStrategy(),
+ accountNumber
+ ))
+ case let .createAccount(retryStrategy):
+ MullvadApiCancellable(handle: mullvad_api_create_account(
+ apiContext.context,
+ rawCompletionPointer,
+ retryStrategy.toRustStrategy()
+ ))
+ case let .deleteAccount(retryStrategy, accountNumber: accountNumber):
+ MullvadApiCancellable(handle: mullvad_api_delete_account(
+ apiContext.context,
+ rawCompletionPointer,
+ retryStrategy.toRustStrategy(),
+ accountNumber
+ ))
}
}
}
diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
index 8a199559b5..12ce0fd4c0 100644
--- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
+++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
@@ -51,7 +51,7 @@ typedef struct SwiftMullvadApiResponse {
} SwiftMullvadApiResponse;
typedef struct CompletionCookie {
- void *_0;
+ void *inner;
} CompletionCookie;
typedef struct ProxyHandle {
@@ -104,9 +104,63 @@ struct SwiftApiContext mullvad_api_init_new(const uint8_t *host,
* `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created
* by calling `mullvad_api_init_new`.
*
- * `completion_cookie` must be pointing to a valid instance of `CompletionCookie`. `CompletionCookie` is
- * safe because the pointer in `MullvadApiCompletion` is valid for the lifetime of the process where this
- * type is intended to be used.
+ * This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift
+ * object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish`
+ * when completion finishes (in completion.finish).
+ *
+ * `account_number` must be a pointer to a null terminated string.
+ *
+ * This function is not safe to call multiple times with the same `CompletionCookie`.
+ */
+struct SwiftCancelHandle mullvad_api_get_account(struct SwiftApiContext api_context,
+ void *completion_cookie,
+ struct SwiftRetryStrategy retry_strategy,
+ const char *account_number);
+
+/**
+ * # Safety
+ *
+ * `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created
+ * by calling `mullvad_api_init_new`.
+ *
+ * This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift
+ * object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish`
+ * when completion finishes (in completion.finish).
+ *
+ * This function is not safe to call multiple times with the same `CompletionCookie`.
+ */
+struct SwiftCancelHandle mullvad_api_create_account(struct SwiftApiContext api_context,
+ void *completion_cookie,
+ struct SwiftRetryStrategy retry_strategy);
+
+/**
+ * # Safety
+ *
+ * `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created
+ * by calling `mullvad_api_init_new`.
+ *
+ * This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift
+ * object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish`
+ * when completion finishes (in completion.finish).
+ *
+ * `account_number` must be a pointer to a null terminated string.
+ *
+ * This function is not safe to call multiple times with the same `CompletionCookie`.
+ */
+struct SwiftCancelHandle mullvad_api_delete_account(struct SwiftApiContext api_context,
+ void *completion_cookie,
+ struct SwiftRetryStrategy retry_strategy,
+ const char *account_number);
+
+/**
+ * # Safety
+ *
+ * `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created
+ * by calling `mullvad_api_init_new`.
+ *
+ * This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift
+ * object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish`
+ * when completion finishes (in completion.finish).
*
* This function is not safe to call multiple times with the same `CompletionCookie`.
*/
@@ -120,9 +174,9 @@ struct SwiftCancelHandle mullvad_api_get_addresses(struct SwiftApiContext api_co
* `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created
* by calling `mullvad_api_init_new`.
*
- * `completion_cookie` must be pointing to a valid instance of `CompletionCookie`. `CompletionCookie` is
- * safe because the pointer in `MullvadApiCompletion` is valid for the lifetime of the process where this
- * type is intended to be used.
+ * This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift
+ * object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish`
+ * when completion finishes (in completion.finish).
*
* `etag` must be a pointer to a null terminated string.
*
@@ -131,7 +185,7 @@ struct SwiftCancelHandle mullvad_api_get_addresses(struct SwiftApiContext api_co
struct SwiftCancelHandle mullvad_api_get_relays(struct SwiftApiContext api_context,
void *completion_cookie,
struct SwiftRetryStrategy retry_strategy,
- const uint8_t *etag);
+ const char *etag);
/**
* Called by the Swift side to signal that a Mullvad API call should be cancelled.
diff --git a/ios/MullvadTypes/NewAccountData.swift b/ios/MullvadTypes/NewAccountData.swift
new file mode 100644
index 0000000000..c405f40394
--- /dev/null
+++ b/ios/MullvadTypes/NewAccountData.swift
@@ -0,0 +1,35 @@
+//
+// NewAccountData.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2025-04-08.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+public struct NewAccountData: Decodable, Sendable {
+ public let id: String
+ public let expiry: Date
+ public let maxPorts: Int
+ public let canAddPorts: Bool
+ public let maxDevices: Int
+ public let canAddDevices: Bool
+ public let number: String
+
+ public init(
+ id: String,
+ expiry: Date,
+ maxPorts: Int,
+ canAddPorts: Bool,
+ maxDevices: Int,
+ canAddDevices: Bool,
+ number: String
+ ) {
+ self.id = id
+ self.expiry = expiry
+ self.maxPorts = maxPorts
+ self.canAddPorts = canAddPorts
+ self.maxDevices = maxDevices
+ self.canAddDevices = canAddDevices
+ self.number = number
+ }
+}
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index b9c42d131a..050bd50e1c 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -645,8 +645,11 @@
7AB2B6702BA1EB8C00B03E3B /* ListCustomListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */; };
7AB2B6712BA1EB8C00B03E3B /* ListCustomListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB2B66F2BA1EB8C00B03E3B /* ListCustomListCoordinator.swift */; };
7AB3BEB52BD7A6CB00E34384 /* LocationViewControllerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB3BEB42BD7A6CB00E34384 /* LocationViewControllerWrapper.swift */; };
+ 7AB401852DA53D5300522E17 /* NewAccountData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB401842DA53D4E00522E17 /* NewAccountData.swift */; };
+ 7AB401872DA53DA300522E17 /* NewAccountDataMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB401862DA53D9B00522E17 /* NewAccountDataMock.swift */; };
7AB4CCB92B69097E006037F5 /* IPOverrideTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4CCB82B69097E006037F5 /* IPOverrideTests.swift */; };
7AB4CCBB2B691BBB006037F5 /* IPOverrideInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4CCBA2B691BBB006037F5 /* IPOverrideInteractor.swift */; };
+ 7AB73F6E2D9AAD0A00DA5E1D /* MullvadAccountProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB73F6D2D9AAD0400DA5E1D /* MullvadAccountProxy.swift */; };
7AB931242D43C2CA005FCEBA /* MullvadApiContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB931232D43C2C2005FCEBA /* MullvadApiContext.swift */; };
7AB931262D43D22F005FCEBA /* MullvadApiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB931252D43D222005FCEBA /* MullvadApiResponse.swift */; };
7AB9312F2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB9312D2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift */; };
@@ -2166,8 +2169,11 @@
7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCustomListViewController.swift; sourceTree = "<group>"; };
7AB2B66F2BA1EB8C00B03E3B /* ListCustomListCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCustomListCoordinator.swift; sourceTree = "<group>"; };
7AB3BEB42BD7A6CB00E34384 /* LocationViewControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationViewControllerWrapper.swift; sourceTree = "<group>"; };
+ 7AB401842DA53D4E00522E17 /* NewAccountData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewAccountData.swift; sourceTree = "<group>"; };
+ 7AB401862DA53D9B00522E17 /* NewAccountDataMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewAccountDataMock.swift; sourceTree = "<group>"; };
7AB4CCB82B69097E006037F5 /* IPOverrideTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideTests.swift; sourceTree = "<group>"; };
7AB4CCBA2B691BBB006037F5 /* IPOverrideInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideInteractor.swift; sourceTree = "<group>"; };
+ 7AB73F6D2D9AAD0400DA5E1D /* MullvadAccountProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadAccountProxy.swift; sourceTree = "<group>"; };
7AB931232D43C2C2005FCEBA /* MullvadApiContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiContext.swift; sourceTree = "<group>"; };
7AB931252D43D222005FCEBA /* MullvadApiResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiResponse.swift; sourceTree = "<group>"; };
7AB9312D2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiNetworkOperation.swift; sourceTree = "<group>"; };
@@ -2989,6 +2995,7 @@
5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */,
58D223D7294C8E5E0029F5F8 /* MullvadTypes.h */,
7A7AD14D2BF21DCE00B30B3C /* NameInputFormatter.swift */,
+ 7AB401842DA53D4E00522E17 /* NewAccountData.swift */,
A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */,
58CC40EE24A601900019D96E /* ObserverList.swift */,
58CAFA01298530DC00BE19F7 /* Promise.swift */,
@@ -4177,6 +4184,7 @@
7A2C0E8A2D8B13DB003D8048 /* APIHandlers */ = {
isa = PBXGroup;
children = (
+ 7AB73F6D2D9AAD0400DA5E1D /* MullvadAccountProxy.swift */,
7A2C0E8B2D8B13E8003D8048 /* MullvadAPIProxy.swift */,
);
path = APIHandlers;
@@ -4644,6 +4652,7 @@
children = (
449EB9FE2B95FF2500DFA4EB /* AccountMock.swift */,
449EB9FC2B95F8AD00DFA4EB /* DeviceMock.swift */,
+ 7AB401862DA53D9B00522E17 /* NewAccountDataMock.swift */,
);
path = MullvadTypes;
sourceTree = "<group>";
@@ -5757,6 +5766,7 @@
A90763BE2B2857D50045ADF0 /* Socks5HandshakeNegotiation.swift in Sources */,
A90763B02B2857D50045ADF0 /* Socks5ConnectCommand.swift in Sources */,
06799ADD28F98E4800ACD94E /* RESTError.swift in Sources */,
+ 7AB73F6E2D9AAD0A00DA5E1D /* MullvadAccountProxy.swift in Sources */,
A90763B92B2857D50045ADF0 /* Socks5ForwardingProxy.swift in Sources */,
A970C89D2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift in Sources */,
A90763B32B2857D50045ADF0 /* Socks5Authentication.swift in Sources */,
@@ -6653,6 +6663,7 @@
58D22412294C90210029F5F8 /* RelayConstraint.swift in Sources */,
7A7AD14F2BF21EF200B30B3C /* NameInputFormatter.swift in Sources */,
58D22413294C90210029F5F8 /* RelayConstraints.swift in Sources */,
+ 7AB401852DA53D5300522E17 /* NewAccountData.swift in Sources */,
A97275562CE36CAE00029F15 /* DaitaV2Parameters.swift in Sources */,
7AF9BE8C2A321D1F00DBFEDB /* RelayFilter.swift in Sources */,
58D22414294C90210029F5F8 /* RelayLocation.swift in Sources */,
@@ -6834,6 +6845,7 @@
7A52F96A2C1735AE00B133B9 /* RelaySelectorStub.swift in Sources */,
7AF84F462D12C5B000C72690 /* SelectedRelaysStub+Stubs.swift in Sources */,
F0FA160C2D7F2BF2007E2546 /* ServerRelaysResponse+Stubs.swift in Sources */,
+ 7AB401872DA53DA300522E17 /* NewAccountDataMock.swift in Sources */,
F03A69F72C2AD2D6000E2E7E /* TimeInterval+Timeout.swift in Sources */,
F0ACE32F2BE4EA8B006D5333 /* MockProxyFactory.swift in Sources */,
);
diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
index 58dac8181d..e811732a35 100644
--- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
@@ -839,4 +839,4 @@ extension DeviceState {
var splitViewMode: UISplitViewController.DisplayMode {
isLoggedIn ? UISplitViewController.DisplayMode.oneBesideSecondary : .secondaryOnly
}
-}
+} // swiftlint:disable:this file_length
diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift
index 3ad770653a..5c4dfd8836 100644
--- a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift
+++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift
@@ -225,10 +225,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver, @unchec
completionHandler: @escaping @Sendable (StorePaymentManagerError?) -> Void
) {
let accountOperation = ResultBlockOperation<Account>(dispatchQueue: .main) { finish in
- self.accountsProxy.getAccountData(accountNumber: accountNumber).execute(
- retryStrategy: .default,
- completionHandler: finish
- )
+ self.accountsProxy.getAccountData(accountNumber: accountNumber, retryStrategy: .default, completion: finish)
}
accountOperation.addObserver(BackgroundObserver(
diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
index ef45960320..07467a5ca8 100644
--- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
@@ -267,7 +267,8 @@ class SetAccountOperation: ResultOperation<StoredAccountData?>, @unchecked Senda
) {
logger.debug("Request account data...")
- let task = accountsProxy.getAccountData(accountNumber: accountNumber).execute(
+ let task = accountsProxy.getAccountData(
+ accountNumber: accountNumber,
retryStrategy: .default
) { [self] result in
dispatchQueue.async { [self] in
diff --git a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift
index 9af9799702..0e06dbfb6f 100644
--- a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift
@@ -36,13 +36,15 @@ class UpdateAccountDataOperation: ResultOperation<Void>, @unchecked Sendable {
return
}
- task = accountsProxy.getAccountData(accountNumber: accountData.number).execute(
- retryStrategy: .default
- ) { result in
- self.dispatchQueue.async {
- self.didReceiveAccountData(result: result)
+ task = accountsProxy.getAccountData(
+ accountNumber: accountData.number,
+ retryStrategy: .default,
+ completion: { result in
+ self.dispatchQueue.async {
+ self.didReceiveAccountData(result: result)
+ }
}
- }
+ )
}
override func operationDidCancel() {
diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift
index cf76e17fe3..fe348f0a7d 100644
--- a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift
+++ b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift
@@ -57,15 +57,19 @@ final class RedeemVoucherInteractor: @unchecked Sendable {
}
private func verifyVoucherAsAccount(code: String) {
- let executer = accountsProxy.getAccountData(accountNumber: code)
- tasks.append(executer.execute { [weak self] result in
+ let task = accountsProxy.getAccountData(
+ accountNumber: code,
+ retryStrategy: .noRetry
+ ) { [weak self] result in
guard let self,
case .success = result else {
return
}
showLogoutDialog?()
preferredAccountNumber = code
- })
+ }
+
+ tasks.append(task)
}
}
diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift
index 9066ed59e9..c3e5349314 100644
--- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift
+++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift
@@ -25,7 +25,8 @@ enum VPNSettingsInfoButtonItem: CustomStringConvertible {
"VPN_SETTINGS_LOCAL_NETWORK_SHARING",
tableName: "LocalNetworkSharing",
value: """
- This feature allows access to other devices on the local network, such as for sharing, printing, streaming, etc.
+ This feature allows access to other devices on the local network, such as for sharing, printing, \
+ streaming, etc.
Attention: toggling “Local network sharing” requires restarting the VPN connection.
""",
comment: ""
diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
index 12e1b933e9..b44927f931 100644
--- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
+++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
@@ -84,7 +84,7 @@ class TunnelManagerTests: XCTestCase {
}
func testLogInStartsKeyRotations() async throws {
- accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue())
+ accountProxy.createAccountResult = .success(NewAccountData.mockValue())
let tunnelManager = TunnelManager(
backgroundTaskProvider: application,
@@ -102,7 +102,7 @@ class TunnelManagerTests: XCTestCase {
}
func testLogOutStopsKeyRotations() async throws {
- accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue())
+ accountProxy.createAccountResult = .success(NewAccountData.mockValue())
let tunnelManager = TunnelManager(
backgroundTaskProvider: application,
@@ -125,7 +125,7 @@ class TunnelManagerTests: XCTestCase {
let blockedExpectation = expectation(description: "Relay constraints aren't satisfied!")
let connectedExpectation = expectation(description: "Connected!")
- accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue())
+ accountProxy.createAccountResult = .success(NewAccountData.mockValue())
let relaySelector = RelaySelectorStub { _ in
try RelaySelectorStub.unsatisfied().selectRelays(
@@ -196,7 +196,7 @@ class TunnelManagerTests: XCTestCase {
var connectedExpectation = expectation(description: "Connected!")
let disconnectedExpectation = expectation(description: "Disconnected!")
- accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue())
+ accountProxy.createAccountResult = .success(NewAccountData.mockValue())
let relaySelector = RelaySelectorStub { _ in
try RelaySelectorStub.nonFallible().selectRelays(
diff --git a/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift
index e8738bb1d6..5110a8808e 100644
--- a/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift
+++ b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift
@@ -25,7 +25,11 @@ struct DeviceCheckRemoteService: DeviceCheckRemoteServiceProtocol {
accountNumber: String,
completion: @escaping @Sendable (Result<Account, Error>) -> Void
) -> Cancellable {
- accountsProxy.getAccountData(accountNumber: accountNumber).execute(completionHandler: completion)
+ accountsProxy.getAccountData(
+ accountNumber: accountNumber,
+ retryStrategy: .noRetry,
+ completion: completion
+ )
}
func getDevice(
diff --git a/mullvad-api/src/lib.rs b/mullvad-api/src/lib.rs
index 1ced489080..2d2814d099 100644
--- a/mullvad-api/src/lib.rs
+++ b/mullvad-api/src/lib.rs
@@ -506,15 +506,24 @@ impl AccountsProxy {
&self,
account: AccountNumber,
) -> impl Future<Output = Result<AccountData, rest::Error>> + use<> {
+ let request = self.get_data_response(account);
+
+ async move { request.await?.deserialize().await }
+ }
+
+ pub fn get_data_response(
+ &self,
+ account: AccountNumber,
+ ) -> impl Future<Output = Result<rest::Response<Incoming>, rest::Error>> {
let service = self.handle.service.clone();
let factory = self.handle.factory.clone();
+
async move {
let request = factory
.get(&format!("{ACCOUNTS_URL_PREFIX}/accounts/me"))?
.expected_status(&[StatusCode::OK])
.account(account)?;
- let response = service.request(request).await?;
- response.deserialize().await
+ service.request(request).await
}
}
@@ -526,6 +535,17 @@ impl AccountsProxy {
number: AccountNumber,
}
+ let request = self.create_account_response();
+
+ async move {
+ let account: AccountCreationResponse = request.await?.deserialize().await?;
+ Ok(account.number)
+ }
+ }
+
+ pub fn create_account_response(
+ &self,
+ ) -> impl Future<Output = Result<rest::Response<Incoming>, rest::Error>> {
let service = self.handle.service.clone();
let factory = self.handle.factory.clone();
@@ -533,9 +553,7 @@ impl AccountsProxy {
let request = factory
.post(&format!("{ACCOUNTS_URL_PREFIX}/accounts"))?
.expected_status(&[StatusCode::CREATED]);
- let response = service.request(request).await?;
- let account: AccountCreationResponse = response.deserialize().await?;
- Ok(account.number)
+ service.request(request).await
}
}
diff --git a/mullvad-ios/src/api_client/account.rs b/mullvad-ios/src/api_client/account.rs
new file mode 100644
index 0000000000..ff3b0294cf
--- /dev/null
+++ b/mullvad-ios/src/api_client/account.rs
@@ -0,0 +1,196 @@
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+use mullvad_api::{
+ rest::{self, MullvadRestHandle},
+ AccountsProxy,
+};
+
+use super::{
+ cancellation::{RequestCancelHandle, SwiftCancelHandle},
+ completion::{CompletionCookie, SwiftCompletionHandler},
+ do_request, do_request_with_empty_body,
+ response::SwiftMullvadApiResponse,
+ retry_strategy::{RetryStrategy, SwiftRetryStrategy},
+ SwiftApiContext,
+};
+
+/// # Safety
+///
+/// `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created
+/// by calling `mullvad_api_init_new`.
+///
+/// This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift
+/// object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish`
+/// when completion finishes (in completion.finish).
+///
+/// `account_number` must be a pointer to a null terminated string.
+///
+/// This function is not safe to call multiple times with the same `CompletionCookie`.
+#[no_mangle]
+pub unsafe extern "C" fn mullvad_api_get_account(
+ api_context: SwiftApiContext,
+ completion_cookie: *mut libc::c_void,
+ retry_strategy: SwiftRetryStrategy,
+ account_number: *const c_char,
+) -> SwiftCancelHandle {
+ let completion_handler = SwiftCompletionHandler::new(CompletionCookie::new(completion_cookie));
+
+ let Ok(tokio_handle) = crate::mullvad_ios_runtime() else {
+ completion_handler.finish(SwiftMullvadApiResponse::no_tokio_runtime());
+ return SwiftCancelHandle::empty();
+ };
+
+ let api_context = api_context.into_rust_context();
+ let retry_strategy = unsafe { retry_strategy.into_rust() };
+ // SAFETY: See param documentation for `account_number`.
+ let account_number = unsafe { CStr::from_ptr(account_number.cast()) }
+ .to_str()
+ .unwrap();
+ let account_number = String::from(account_number);
+
+ let completion = completion_handler.clone();
+ let task = tokio_handle.clone().spawn(async move {
+ match mullvad_api_get_account_inner(
+ api_context.rest_handle(),
+ retry_strategy,
+ account_number,
+ )
+ .await
+ {
+ Ok(response) => completion.finish(response),
+ Err(err) => {
+ log::error!("{err:?}");
+ completion.finish(SwiftMullvadApiResponse::rest_error(err));
+ }
+ }
+ });
+
+ RequestCancelHandle::new(task, completion_handler.clone()).into_swift()
+}
+
+/// # Safety
+///
+/// `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created
+/// by calling `mullvad_api_init_new`.
+///
+/// This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift
+/// object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish`
+/// when completion finishes (in completion.finish).
+///
+/// This function is not safe to call multiple times with the same `CompletionCookie`.
+#[no_mangle]
+pub unsafe extern "C" fn mullvad_api_create_account(
+ api_context: SwiftApiContext,
+ completion_cookie: *mut libc::c_void,
+ retry_strategy: SwiftRetryStrategy,
+) -> SwiftCancelHandle {
+ let completion_handler = SwiftCompletionHandler::new(CompletionCookie::new(completion_cookie));
+
+ let Ok(tokio_handle) = crate::mullvad_ios_runtime() else {
+ completion_handler.finish(SwiftMullvadApiResponse::no_tokio_runtime());
+ return SwiftCancelHandle::empty();
+ };
+
+ let api_context = api_context.into_rust_context();
+ let retry_strategy = unsafe { retry_strategy.into_rust() };
+
+ let completion = completion_handler.clone();
+ let task = tokio_handle.clone().spawn(async move {
+ match mullvad_api_create_account_inner(api_context.rest_handle(), retry_strategy).await {
+ Ok(response) => completion.finish(response),
+ Err(err) => {
+ log::error!("{err:?}");
+ completion.finish(SwiftMullvadApiResponse::rest_error(err));
+ }
+ }
+ });
+
+ RequestCancelHandle::new(task, completion_handler.clone()).into_swift()
+}
+
+/// # Safety
+///
+/// `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created
+/// by calling `mullvad_api_init_new`.
+///
+/// This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift
+/// object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish`
+/// when completion finishes (in completion.finish).
+///
+/// `account_number` must be a pointer to a null terminated string.
+///
+/// This function is not safe to call multiple times with the same `CompletionCookie`.
+#[no_mangle]
+pub unsafe extern "C" fn mullvad_api_delete_account(
+ api_context: SwiftApiContext,
+ completion_cookie: *mut libc::c_void,
+ retry_strategy: SwiftRetryStrategy,
+ account_number: *const c_char,
+) -> SwiftCancelHandle {
+ let completion_handler = SwiftCompletionHandler::new(CompletionCookie::new(completion_cookie));
+
+ let Ok(tokio_handle) = crate::mullvad_ios_runtime() else {
+ completion_handler.finish(SwiftMullvadApiResponse::no_tokio_runtime());
+ return SwiftCancelHandle::empty();
+ };
+
+ let api_context = api_context.into_rust_context();
+ let retry_strategy = unsafe { retry_strategy.into_rust() };
+ // SAFETY: See param documentation for `account_number`.
+ let account_number = unsafe { CStr::from_ptr(account_number.cast()) }
+ .to_str()
+ .unwrap();
+ let account_number = String::from(account_number);
+
+ let completion = completion_handler.clone();
+ let task = tokio_handle.clone().spawn(async move {
+ match mullvad_api_delete_account_inner(
+ api_context.rest_handle(),
+ retry_strategy,
+ account_number,
+ )
+ .await
+ {
+ Ok(response) => completion.finish(response),
+ Err(err) => {
+ log::error!("{err:?}");
+ completion.finish(SwiftMullvadApiResponse::rest_error(err));
+ }
+ }
+ });
+
+ RequestCancelHandle::new(task, completion_handler.clone()).into_swift()
+}
+
+async fn mullvad_api_get_account_inner(
+ rest_client: MullvadRestHandle,
+ retry_strategy: RetryStrategy,
+ account_number: String,
+) -> Result<SwiftMullvadApiResponse, rest::Error> {
+ let api = AccountsProxy::new(rest_client);
+ let future_factory = || api.get_data_response(account_number.clone());
+
+ do_request(retry_strategy, future_factory).await
+}
+
+async fn mullvad_api_create_account_inner(
+ rest_client: MullvadRestHandle,
+ retry_strategy: RetryStrategy,
+) -> Result<SwiftMullvadApiResponse, rest::Error> {
+ let api = AccountsProxy::new(rest_client);
+ let future_factory = || api.create_account_response();
+
+ do_request(retry_strategy, future_factory).await
+}
+
+async fn mullvad_api_delete_account_inner(
+ rest_client: MullvadRestHandle,
+ retry_strategy: RetryStrategy,
+ account_number: String,
+) -> Result<SwiftMullvadApiResponse, rest::Error> {
+ let api = AccountsProxy::new(rest_client);
+ let future_factory = || api.delete_account(account_number.clone());
+
+ do_request_with_empty_body(retry_strategy, future_factory).await
+}
diff --git a/mullvad-ios/src/api_client/api.rs b/mullvad-ios/src/api_client/api.rs
index c918dda61f..c6c14bb14c 100644
--- a/mullvad-ios/src/api_client/api.rs
+++ b/mullvad-ios/src/api_client/api.rs
@@ -1,4 +1,5 @@
use std::ffi::CStr;
+use std::os::raw::c_char;
use mullvad_api::{
rest::{self, MullvadRestHandle},
@@ -19,9 +20,9 @@ use super::{
/// `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created
/// by calling `mullvad_api_init_new`.
///
-/// `completion_cookie` must be pointing to a valid instance of `CompletionCookie`. `CompletionCookie` is
-/// safe because the pointer in `MullvadApiCompletion` is valid for the lifetime of the process where this
-/// type is intended to be used.
+/// This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift
+/// object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish`
+/// when completion finishes (in completion.finish).
///
/// This function is not safe to call multiple times with the same `CompletionCookie`.
#[no_mangle]
@@ -30,7 +31,7 @@ pub unsafe extern "C" fn mullvad_api_get_addresses(
completion_cookie: *mut libc::c_void,
retry_strategy: SwiftRetryStrategy,
) -> SwiftCancelHandle {
- let completion_handler = SwiftCompletionHandler::new(CompletionCookie(completion_cookie));
+ let completion_handler = SwiftCompletionHandler::new(CompletionCookie::new(completion_cookie));
let Ok(tokio_handle) = crate::mullvad_ios_runtime() else {
completion_handler.finish(SwiftMullvadApiResponse::no_tokio_runtime());
@@ -59,9 +60,9 @@ pub unsafe extern "C" fn mullvad_api_get_addresses(
/// `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created
/// by calling `mullvad_api_init_new`.
///
-/// `completion_cookie` must be pointing to a valid instance of `CompletionCookie`. `CompletionCookie` is
-/// safe because the pointer in `MullvadApiCompletion` is valid for the lifetime of the process where this
-/// type is intended to be used.
+/// This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift
+/// object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish`
+/// when completion finishes (in completion.finish).
///
/// `etag` must be a pointer to a null terminated string.
///
@@ -71,9 +72,9 @@ pub unsafe extern "C" fn mullvad_api_get_relays(
api_context: SwiftApiContext,
completion_cookie: *mut libc::c_void,
retry_strategy: SwiftRetryStrategy,
- etag: *const u8,
+ etag: *const c_char,
) -> SwiftCancelHandle {
- let completion_handler = SwiftCompletionHandler::new(CompletionCookie(completion_cookie));
+ let completion_handler = SwiftCompletionHandler::new(CompletionCookie::new(completion_cookie));
let Ok(tokio_handle) = crate::mullvad_ios_runtime() else {
completion_handler.finish(SwiftMullvadApiResponse::no_tokio_runtime());
@@ -85,6 +86,7 @@ pub unsafe extern "C" fn mullvad_api_get_relays(
let mut maybe_etag: Option<String> = None;
if !etag.is_null() {
+ // SAFETY: See param documentation for `etag`.
let unwrapped_tag = unsafe { CStr::from_ptr(etag.cast()) }.to_str().unwrap();
maybe_etag = Some(String::from(unwrapped_tag));
}
diff --git a/mullvad-ios/src/api_client/completion.rs b/mullvad-ios/src/api_client/completion.rs
index 11a05acf8e..db15a6b8b6 100644
--- a/mullvad-ios/src/api_client/completion.rs
+++ b/mullvad-ios/src/api_client/completion.rs
@@ -20,8 +20,16 @@ extern "C" {
}
#[repr(C)]
-pub struct CompletionCookie(pub *mut std::ffi::c_void);
+pub struct CompletionCookie {
+ inner: *mut std::ffi::c_void,
+}
unsafe impl Send for CompletionCookie {}
+impl CompletionCookie {
+ /// `inner` must be pointing to a valid instance of Swift object `MullvadApiCompletion`.
+ pub unsafe fn new(inner: *mut std::ffi::c_void) -> Self {
+ Self { inner }
+ }
+}
#[derive(Clone)]
pub struct SwiftCompletionHandler {
diff --git a/mullvad-ios/src/api_client/mod.rs b/mullvad-ios/src/api_client/mod.rs
index 98b41c103f..fd9fde7aa7 100644
--- a/mullvad-ios/src/api_client/mod.rs
+++ b/mullvad-ios/src/api_client/mod.rs
@@ -9,6 +9,7 @@ use response::SwiftMullvadApiResponse;
use retry_strategy::RetryStrategy;
use talpid_future::retry::retry_future;
+mod account;
mod api;
mod cancellation;
mod completion;
@@ -93,12 +94,34 @@ where
F: Fn() -> T,
T: Future<Output = Result<rest::Response<hyper::body::Incoming>, rest::Error>>,
{
+ let response = retry_request(retry_strategy, future_factory).await?;
+ SwiftMullvadApiResponse::with_body(response).await
+}
+
+async fn do_request_with_empty_body<F, T>(
+ retry_strategy: RetryStrategy,
+ future_factory: F,
+) -> Result<SwiftMullvadApiResponse, rest::Error>
+where
+ F: Fn() -> T,
+ T: Future<Output = Result<(), rest::Error>>,
+{
+ retry_request(retry_strategy, future_factory).await?;
+ Ok(SwiftMullvadApiResponse::ok())
+}
+
+async fn retry_request<F, T, U>(
+ retry_strategy: RetryStrategy,
+ future_factory: F,
+) -> Result<U, rest::Error>
+where
+ F: Fn() -> T,
+ T: Future<Output = Result<U, rest::Error>>,
+{
let should_retry = |result: &Result<_, rest::Error>| match result {
Err(err) => err.is_network_error(),
Ok(_) => false,
};
- let response = retry_future(future_factory, should_retry, retry_strategy.delays()).await?;
-
- SwiftMullvadApiResponse::with_body(response).await
+ retry_future(future_factory, should_retry, retry_strategy.delays()).await
}
diff --git a/mullvad-ios/src/api_client/response.rs b/mullvad-ios/src/api_client/response.rs
index ac6c0feecc..3b017e37c6 100644
--- a/mullvad-ios/src/api_client/response.rs
+++ b/mullvad-ios/src/api_client/response.rs
@@ -5,7 +5,7 @@ use std::{
use mullvad_api::{
rest::{self, Response},
- RelayListProxy,
+ RelayListProxy, StatusCode,
};
#[repr(C)]
@@ -49,6 +49,18 @@ impl SwiftMullvadApiResponse {
})
}
+ pub fn ok() -> Self {
+ Self {
+ success: true,
+ error_description: null_mut(),
+ body: null_mut(),
+ body_size: 0,
+ etag: null_mut(),
+ status_code: StatusCode::NO_CONTENT.as_u16(),
+ server_response_code: null_mut(),
+ }
+ }
+
pub fn rest_error(err: mullvad_api::rest::Error) -> Self {
if err.is_aborted() {
return Self::cancelled();