summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorSteffen Ernst <steffen.ernst@mullvad.net>2025-04-09 10:33:39 +0200
committerSteffen Ernst <steffen.ernst@mullvad.net>2025-05-08 09:31:27 +0200
commit01660f7d4af6fc539ce938f44f26bdf55337b57a (patch)
tree4a15da846efe1f5fe9bf486d2c35727b1e2d1b31
parenta74a0c0aa3270bb2cf7861f94ad7dc590ed50d22 (diff)
downloadmullvadvpn-01660f7d4af6fc539ce938f44f26bdf55337b57a.tar.xz
mullvadvpn-01660f7d4af6fc539ce938f44f26bdf55337b57a.zip
Add storekit 2 functions to mullvad api
-rw-r--r--ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift17
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift19
-rw-r--r--ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift57
-rw-r--r--ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift16
-rw-r--r--ios/MullvadREST/MullvadAPI/APIRequest/APIRequestProxy.swift34
-rw-r--r--ios/MullvadREST/MullvadAPI/MullvadApiNetworkOperation.swift38
-rw-r--r--ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift31
-rw-r--r--ios/MullvadREST/Transport/APITransport.swift6
-rw-r--r--ios/MullvadRESTTests/MullvadApiTests.swift5
-rw-r--r--ios/MullvadRustRuntime/include/mullvad_rust_runtime.h44
-rw-r--r--ios/MullvadTypes/Storekit2.swift7
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/AppDelegate.swift5
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountInteractor.swift35
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountViewController.swift30
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift10
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift5
-rw-r--r--mullvad-api/src/lib.rs33
-rw-r--r--mullvad-api/src/rest.rs23
-rw-r--r--mullvad-ios/src/api_client/mod.rs1
-rw-r--r--mullvad-ios/src/api_client/storekit.rs161
21 files changed, 524 insertions, 57 deletions
diff --git a/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift b/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift
index 9880512524..218c43f195 100644
--- a/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift
+++ b/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift
@@ -52,4 +52,21 @@ struct APIProxyStub: APIQuerying {
) -> Cancellable {
AnyCancellable()
}
+
+ func initStorekitPayment(
+ accountNumber: String,
+ retryStrategy: MullvadREST.REST.RetryStrategy,
+ completionHandler: @escaping MullvadREST.ProxyCompletionHandler<String>
+ ) -> any MullvadTypes.Cancellable {
+ AnyCancellable()
+ }
+
+ func checkStorekitPayment(
+ accountNumber: String,
+ transaction: MullvadTypes.StorekitTransaction,
+ retryStrategy: MullvadREST.REST.RetryStrategy,
+ completionHandler: @escaping MullvadREST.ProxyCompletionHandler<Void>
+ ) -> any MullvadTypes.Cancellable {
+ AnyCancellable()
+ }
}
diff --git a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift
index d45745446e..e7b775124c 100644
--- a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift
+++ b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift
@@ -250,6 +250,25 @@ extension REST {
return executor.execute(retryStrategy: retryStrategy, completionHandler: completionHandler)
}
+
+ /// Not implemented. Use `MullvadAPIProxy` instead.
+ public func initStorekitPayment(
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping ProxyCompletionHandler<String>
+ ) -> any MullvadTypes.Cancellable {
+ AnyCancellable()
+ }
+
+ /// Not implemented. Use `MullvadAPIProxy` instead.
+ public func checkStorekitPayment(
+ accountNumber: String,
+ transaction: MullvadTypes.StorekitTransaction,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping ProxyCompletionHandler<Void>
+ ) -> any MullvadTypes.Cancellable {
+ AnyCancellable()
+ }
}
// MARK: - Response types
diff --git a/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift
index 5cf3ee0e37..898809ee1a 100644
--- a/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift
+++ b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift
@@ -40,6 +40,19 @@ public protocol APIQuerying: Sendable {
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.SubmitVoucherResponse>
) -> Cancellable
+
+ func initStorekitPayment(
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping @Sendable ProxyCompletionHandler<String>
+ ) -> Cancellable
+
+ func checkStorekitPayment(
+ accountNumber: String,
+ transaction: StorekitTransaction,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping @Sendable ProxyCompletionHandler<Void>
+ ) -> Cancellable
}
extension REST {
@@ -138,7 +151,49 @@ extension REST {
AnyCancellable()
}
- private func createNetworkOperation<Success: Any>(
+ public func initStorekitPayment(
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping ProxyCompletionHandler<String>
+ ) -> any MullvadTypes.Cancellable {
+ struct InitStorekitPaymentResponse: Codable {
+ let paymentToken: String
+ }
+
+ let responseHandler = rustResponseHandler(
+ decoding: InitStorekitPaymentResponse.self,
+ with: responseDecoder
+ )
+
+ return createNetworkOperation(
+ request:
+ .initStorekitPayment(retryStrategy: retryStrategy, accountNumber: accountNumber),
+ responseHandler: responseHandler,
+ completionHandler: { completionHandler($0.map { $0.paymentToken }) }
+ )
+ }
+
+ public func checkStorekitPayment(
+ accountNumber: String,
+ transaction: StorekitTransaction,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping ProxyCompletionHandler<Void>
+ ) -> any MullvadTypes.Cancellable {
+ let responseHandler = rustEmptyResponseHandler()
+
+ return createNetworkOperation(
+ request:
+ .checkStorekitPayment(
+ retryStrategy: retryStrategy,
+ accountNumber: accountNumber,
+ transaction: transaction
+ ),
+ responseHandler: responseHandler,
+ completionHandler: completionHandler
+ )
+ }
+
+ private func createNetworkOperation<Success>(
request: APIRequest,
responseHandler: RustResponseHandler<Success>,
completionHandler: @escaping @Sendable ProxyCompletionHandler<Success>
diff --git a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift
index ab1087fe5e..f2ecafdbf6 100644
--- a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift
+++ b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift
@@ -18,6 +18,12 @@ public enum APIRequest: Codable, Sendable {
case createAccount(_ retryStrategy: REST.RetryStrategy)
case getAccount(_ retryStrategy: REST.RetryStrategy, accountNumber: String)
case deleteAccount(_ retryStrategy: REST.RetryStrategy, accountNumber: String)
+ case initStorekitPayment(retryStrategy: REST.RetryStrategy, accountNumber: String)
+ case checkStorekitPayment(
+ retryStrategy: REST.RetryStrategy,
+ accountNumber: String,
+ transaction: StorekitTransaction
+ )
// Device Proxy
case getDevice(_ retryStrategy: REST.RetryStrategy, accountNumber: String, identifier: String)
@@ -55,6 +61,10 @@ public enum APIRequest: Codable, Sendable {
"rotate-device-key"
case .createDevice:
"create-device"
+ case .initStorekitPayment:
+ "init-storekit-payment"
+ case .checkStorekitPayment:
+ "check-storekit-payment"
}
}
@@ -70,8 +80,10 @@ public enum APIRequest: Codable, Sendable {
let .getDevice(strategy, _, _),
let .getDevices(strategy, _),
let .deleteDevice(strategy, _, _),
- let .rotateDeviceKey(strategy, _, _, _):
- return strategy
+ let .rotateDeviceKey(strategy, _, _, _),
+ let .initStorekitPayment(strategy, _),
+ let .checkStorekitPayment(strategy, _, _):
+ strategy
}
}
}
diff --git a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequestProxy.swift b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequestProxy.swift
index 8e2ac4fad2..d204165c0d 100644
--- a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequestProxy.swift
+++ b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequestProxy.swift
@@ -45,20 +45,32 @@ public final class APIRequestProxy: APIRequestProxyProtocol, @unchecked Sendable
completion(ProxyAPIResponse(data: nil, error: nil))
return
}
+ do {
+ let cancellable = try transport.sendRequest(proxyRequest.request) { [weak self] response in
+ guard let self else { return }
- let cancellable = transport.sendRequest(proxyRequest.request) { [weak self] response in
- guard let self else { return }
-
- // Use `dispatchQueue` to guarantee thread safe access to `proxiedRequests`
- dispatchQueue.async {
- _ = self.removeRequest(identifier: proxyRequest.id)
- completion(response)
+ // Use `dispatchQueue` to guarantee thread safe access to `proxiedRequests`
+ dispatchQueue.async {
+ _ = self.removeRequest(identifier: proxyRequest.id)
+ completion(response)
+ }
}
- }
- // Cancel old task, if there's one scheduled.
- let oldTask = self.addRequest(identifier: proxyRequest.id, task: cancellable)
- oldTask?.cancel()
+ // Cancel old task, if there's one scheduled.
+ let oldTask = self.addRequest(identifier: proxyRequest.id, task: cancellable)
+ oldTask?.cancel()
+ } catch {
+ completion(
+ ProxyAPIResponse(
+ data: nil,
+ error: APIError(
+ statusCode: 0,
+ errorDescription: error.localizedDescription,
+ serverResponseCode: nil
+ )
+ )
+ )
+ }
}
}
diff --git a/ios/MullvadREST/MullvadAPI/MullvadApiNetworkOperation.swift b/ios/MullvadREST/MullvadAPI/MullvadApiNetworkOperation.swift
index 12d9c0346d..643e24fcdb 100644
--- a/ios/MullvadREST/MullvadAPI/MullvadApiNetworkOperation.swift
+++ b/ios/MullvadREST/MullvadAPI/MullvadApiNetworkOperation.swift
@@ -70,28 +70,32 @@ extension REST {
}
let transport = transportProvider.makeTransport()
- networkTask = transport?.sendRequest(request) { [weak self] response in
- guard let self else { return }
+ do {
+ networkTask = try transport?.sendRequest(request) { [weak self] response in
+ guard let self else { return }
- if let apiError = response.error {
- finish(result: .failure(restError(apiError: apiError)))
- return
- }
+ if let apiError = response.error {
+ finish(result: .failure(restError(apiError: apiError)))
+ return
+ }
- let decodedResponse = responseHandler.handleResponse(response)
+ let decodedResponse = responseHandler.handleResponse(response)
- switch decodedResponse {
- case let .success(value):
- finish(result: .success(value))
- case let .decoding(block):
- do {
- finish(result: .success(try block()))
- } catch {
- finish(result: .failure(REST.Error.unhandledResponse(0, nil)))
+ switch decodedResponse {
+ case let .success(value):
+ finish(result: .success(value))
+ case let .decoding(block):
+ do {
+ finish(result: .success(try block()))
+ } catch {
+ finish(result: .failure(REST.Error.unhandledResponse(0, nil)))
+ }
+ case let .unhandledResponse(error):
+ finish(result: .failure(REST.Error.unhandledResponse(0, error)))
}
- case let .unhandledResponse(error):
- finish(result: .failure(REST.Error.unhandledResponse(0, error)))
}
+ } catch {
+ finish(result: .failure(error))
}
}
diff --git a/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift b/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift
index 3f29468d9b..20b15d2971 100644
--- a/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift
+++ b/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift
@@ -11,11 +11,14 @@ import MullvadTypes
public struct MullvadApiRequestFactory: Sendable {
public let apiContext: MullvadApiContext
+ private let encoder: JSONEncoder
- public init(apiContext: MullvadApiContext) {
+ public init(apiContext: MullvadApiContext, encoder: JSONEncoder) {
self.apiContext = apiContext
+ self.encoder = encoder
}
+ // swiftlint:disable:next function_body_length
public func makeRequest(_ request: APIRequest) -> REST.MullvadApiRequestHandler {
{ completion in
let completionPointer = MullvadApiCompletion { apiResponse in
@@ -111,11 +114,35 @@ public struct MullvadApiRequestFactory: Sendable {
accountNumber,
request.publicKey.rawValue.map { $0 }
))
+ case let .initStorekitPayment(
+ retryStrategy: retryStrategy,
+ accountNumber: accountNumber
+ ):
+ return MullvadApiCancellable(handle: mullvad_ios_init_storekit_payment(
+ apiContext.context,
+ rawCompletionPointer,
+ retryStrategy.toRustStrategy(),
+ accountNumber
+ ))
+ case let .checkStorekitPayment(
+ retryStrategy: retryStrategy,
+ accountNumber: accountNumber,
+ transaction: transaction
+ ):
+ let body = try encoder.encode(transaction)
+ return MullvadApiCancellable(handle: mullvad_ios_check_storekit_payment(
+ apiContext.context,
+ rawCompletionPointer,
+ retryStrategy.toRustStrategy(),
+ accountNumber,
+ body.map { $0 },
+ UInt(body.count)
+ ))
}
}
}
}
extension REST {
- public typealias MullvadApiRequestHandler = (((MullvadApiResponse) throws -> Void)?) -> MullvadApiCancellable
+ public typealias MullvadApiRequestHandler = (((MullvadApiResponse) throws -> Void)?) throws -> MullvadApiCancellable
}
diff --git a/ios/MullvadREST/Transport/APITransport.swift b/ios/MullvadREST/Transport/APITransport.swift
index d0bf7db48d..ed761f1479 100644
--- a/ios/MullvadREST/Transport/APITransport.swift
+++ b/ios/MullvadREST/Transport/APITransport.swift
@@ -12,7 +12,7 @@ import MullvadTypes
public protocol APITransportProtocol {
var name: String { get }
- func sendRequest(_ request: APIRequest, completion: @escaping @Sendable (ProxyAPIResponse) -> Void)
+ func sendRequest(_ request: APIRequest, completion: @escaping @Sendable (ProxyAPIResponse) -> Void) throws
-> Cancellable
}
@@ -30,10 +30,10 @@ public final class APITransport: APITransportProtocol {
public func sendRequest(
_ request: APIRequest,
completion: @escaping @Sendable (ProxyAPIResponse) -> Void
- ) -> Cancellable {
+ ) throws -> Cancellable {
let apiRequest = requestFactory.makeRequest(request)
- return apiRequest { response in
+ return try apiRequest { response in
let error: APIError? = if !response.success {
APIError(
statusCode: Int(response.statusCode),
diff --git a/ios/MullvadRESTTests/MullvadApiTests.swift b/ios/MullvadRESTTests/MullvadApiTests.swift
index 0290f11e66..506c7240f8 100644
--- a/ios/MullvadRESTTests/MullvadApiTests.swift
+++ b/ios/MullvadRESTTests/MullvadApiTests.swift
@@ -43,7 +43,10 @@ class MullvadApiTests: XCTestCase {
let proxy = REST.MullvadAPIProxy(
transportProvider: APITransportProvider(
- requestFactory: .init(apiContext: context)
+ requestFactory: .init(
+ apiContext: context,
+ encoder: JSONEncoder()
+ )
),
dispatchQueue: .main,
responseDecoder: REST.Coding.makeJSONDecoder()
diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
index fa24141e42..e053294fb8 100644
--- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
+++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
@@ -639,6 +639,50 @@ extern const void *swift_get_shadowsocks_bridges(const void *rawBridgeProvider);
struct SwiftShadowsocksLoaderWrapper init_swift_shadowsocks_loader_wrapper(const void *shadowsocks_loader);
/**
+ * # 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_ios_init_storekit_payment(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).
+ *
+ * `account_number` must be a pointer to a null terminated string.
+ *
+ * `body` must be a pointer to a contiguous memory segment
+ *
+ * `body_size` must be the size of the body
+ *
+ * This function is not safe to call multiple times with the same `CompletionCookie`.
+ */
+struct SwiftCancelHandle mullvad_ios_check_storekit_payment(struct SwiftApiContext api_context,
+ void *completion_cookie,
+ struct SwiftRetryStrategy retry_strategy,
+ const char *account_number,
+ const uint8_t *body,
+ uintptr_t body_size);
+
+/**
* Initializes a valid pointer to an instance of `EncryptedDnsProxyState`.
*
* # Safety
diff --git a/ios/MullvadTypes/Storekit2.swift b/ios/MullvadTypes/Storekit2.swift
new file mode 100644
index 0000000000..3407b76f3c
--- /dev/null
+++ b/ios/MullvadTypes/Storekit2.swift
@@ -0,0 +1,7 @@
+public struct StorekitTransaction: Codable, Sendable {
+ let transaction: String
+
+ public init(transaction: String) {
+ self.transaction = transaction
+ }
+}
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 9d291a7ae5..64f83215c5 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -1109,6 +1109,7 @@
F910A4312D4A1B41002FF3BB /* InAppPurchaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4302D4A1B3B002FF3BB /* InAppPurchaseCoordinator.swift */; };
F910A43A2D4A283D002FF3BB /* InAppPurchaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4392D4A2839002FF3BB /* InAppPurchaseViewController.swift */; };
F910A8572D523812002FF3BB /* TunnelSettingsV7.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A8562D523812002FF3BB /* TunnelSettingsV7.swift */; };
+ F924C5A42DA65F28001F4660 /* Storekit2.swift in Sources */ = {isa = PBXBuildFile; fileRef = F924C5A32DA65F28001F4660 /* Storekit2.swift */; };
F924C65F2DAE4554001F4660 /* ServerRelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F924C65E2DAE4554001F4660 /* ServerRelayTests.swift */; };
F924C4532D70692E001F4660 /* MullvadApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F924C4522D706929001F4660 /* MullvadApiTests.swift */; };
F998EFF82D359C4600D88D01 /* SKProduct+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */; };
@@ -2523,6 +2524,7 @@
F910A4392D4A2839002FF3BB /* InAppPurchaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseViewController.swift; sourceTree = "<group>"; };
F910A8562D523812002FF3BB /* TunnelSettingsV7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV7.swift; sourceTree = "<group>"; };
F924C65E2DAE4554001F4660 /* ServerRelayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerRelayTests.swift; sourceTree = "<group>"; };
+ F924C5A32DA65F28001F4660 /* Storekit2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storekit2.swift; sourceTree = "<group>"; };
F924C4522D706929001F4660 /* MullvadApiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiTests.swift; sourceTree = "<group>"; };
F998EFF92D3656B100D88D01 /* SKProduct+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Sorting.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -3044,6 +3046,7 @@
581DA2722A1E227D0046ED47 /* RESTTypes.swift */,
58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */,
58DFF7D92B02862E00F864E0 /* ShadowsocksCipherOptions.swift */,
+ F924C5A32DA65F28001F4660 /* Storekit2.swift */,
F0ADF1CC2CFDFF3100299F09 /* StringConversionError.swift */,
A91614D02B108D1B00F416EB /* TransportLayer.swift */,
58E511E028DDB7F100B0BCDE /* WrappingError.swift */,
@@ -6700,6 +6703,7 @@
7A307AD92A8CD8DA0017618B /* Duration.swift in Sources */,
58D2240A294C90210029F5F8 /* IPAddress+Codable.swift in Sources */,
58E45A5729F12C5100281ECF /* Result+Extensions.swift in Sources */,
+ F924C5A42DA65F28001F4660 /* Storekit2.swift in Sources */,
A90C48672C36BC2600DCB94C /* EphemeralPeerReceiver.swift in Sources */,
A9E031782ACB09930095D843 /* BackgroundTaskProvider.swift in Sources */,
58D2240B294C90210029F5F8 /* Cancellable.swift in Sources */,
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index 2b8b15042d..e056368264 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -175,7 +175,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
encryptedDNSTransport: encryptedDNSTransport
)
- let apiRequestFactory = MullvadApiRequestFactory(apiContext: apiContext)
+ let apiRequestFactory = MullvadApiRequestFactory(
+ apiContext: apiContext,
+ encoder: REST.Coding.makeJSONEncoder()
+ )
let apiTransportProvider = APITransportProvider(requestFactory: apiRequestFactory)
apiTransportMonitor = APITransportMonitor(
diff --git a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift
index 8a6f44df71..c11899906a 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift
@@ -49,10 +49,35 @@ final class AccountInteractor: Sendable {
await tunnelManager.unsetAccount()
}
- func sendStoreKitReceipt(_ transaction: VerificationResult<Transaction>, for accountNumber: String) async throws {
- _ = try await apiProxy.createApplePayment(
- accountNumber: accountNumber,
- receiptString: transaction.jwsRepresentation.data(using: .utf8)!
- ).execute()
+ // This function is for testing only
+ func getPaymentToken(for accountNumber: String) async -> Result<String, Error> {
+ await withCheckedContinuation { continuation in
+ _ = apiProxy
+ .initStorekitPayment(
+ accountNumber: accountNumber,
+ retryStrategy: .noRetry,
+ completionHandler: { result in
+ continuation.resume(returning: result)
+ }
+ )
+ }
+ }
+
+ // This function is for testing only
+ func sendStoreKitReceipt(
+ _ transaction: VerificationResult<Transaction>,
+ for accountNumber: String
+ ) async -> Result<Void, Error> {
+ await withCheckedContinuation { c in
+ _ = apiProxy
+ .checkStorekitPayment(
+ accountNumber: accountNumber,
+ transaction: StorekitTransaction(transaction: transaction.jwsRepresentation),
+ retryStrategy: .noRetry,
+ completionHandler: { result in
+ c.resume(returning: result)
+ }
+ )
+ }
}
}
diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
index 6f362e96db..eb3d2acd3b 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
@@ -225,6 +225,7 @@ class AccountViewController: UIViewController, @unchecked Sendable {
actionHandler?(.showRestorePurchases)
}
+ // This function is for testing only
@objc private func handleStoreKit2Purchase() {
guard let accountData = interactor.deviceState.accountData else {
return
@@ -234,13 +235,30 @@ class AccountViewController: UIViewController, @unchecked Sendable {
Task {
do {
- let product = try await Product.products(for: [storeKit2TestProduct]).first!
- let result = try await product.purchase()
+ let product = try await Product.products(
+ for: [
+ storeKit2TestProduct,
+ ]
+ ).first!
+ let token = switch await interactor
+ .getPaymentToken(for: accountData.number) {
+ case let .success(token):
+ UUID(uuidString: token)!
+ case let .failure(error):
+ throw error
+ }
+
+ let result = try await product.purchase(
+ options: [.appAccountToken(token)]
+ )
switch result {
case let .success(verification):
let transaction = try checkVerified(verification)
- await sendReceiptToAPI(accountNumber: accountData.identifier, receipt: verification)
+ await sendReceiptToAPI(
+ accountNumber: accountData.number,
+ receipt: verification
+ )
await transaction.finish()
case .userCancelled:
print("User cancelled the purchase")
@@ -303,10 +321,10 @@ class AccountViewController: UIViewController, @unchecked Sendable {
}
private func sendReceiptToAPI(accountNumber: String, receipt: VerificationResult<Transaction>) async {
- do {
- try await interactor.sendStoreKitReceipt(receipt, for: accountNumber)
+ switch await interactor.sendStoreKitReceipt(receipt, for: accountNumber) {
+ case .success:
print("Receipt sent successfully")
- } catch {
+ case let .failure(error):
print("Error sending receipt: \(error)")
errorPresenter.showAlertForStoreKitError(error, context: .purchase)
}
diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
index 391e637e23..3119af256f 100644
--- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
+++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
@@ -156,7 +156,10 @@ class TunnelManagerTests: XCTestCase {
relaySelector: relaySelector,
transportProvider: transportProvider,
apiTransportProvider: APITransportProvider(
- requestFactory: MullvadApiRequestFactory(apiContext: apiContext)
+ requestFactory: MullvadApiRequestFactory(
+ apiContext: apiContext,
+ encoder: REST.Coding.makeJSONEncoder()
+ )
)
)
SimulatorTunnelProvider.shared.delegate = simulatorTunnelProviderHost
@@ -227,7 +230,10 @@ class TunnelManagerTests: XCTestCase {
relaySelector: relaySelector,
transportProvider: transportProvider,
apiTransportProvider: APITransportProvider(
- requestFactory: MullvadApiRequestFactory(apiContext: apiContext)
+ requestFactory: MullvadApiRequestFactory(
+ apiContext: apiContext,
+ encoder: REST.Coding.makeJSONEncoder()
+ )
)
)
SimulatorTunnelProvider.shared.delegate = simulatorTunnelProviderHost
diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
index 90f1aa8d69..3f2df2cbdb 100644
--- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
@@ -68,7 +68,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
)
let apiTransportProvider = APITransportProvider(
- requestFactory: MullvadApiRequestFactory(apiContext: apiContext)
+ requestFactory: MullvadApiRequestFactory(
+ apiContext: apiContext,
+ encoder: REST.Coding.makeJSONEncoder()
+ )
)
adapter = WgAdapter(packetTunnelProvider: self)
diff --git a/mullvad-api/src/lib.rs b/mullvad-api/src/lib.rs
index fb44b5ba89..d203d4dd04 100644
--- a/mullvad-api/src/lib.rs
+++ b/mullvad-api/src/lib.rs
@@ -65,6 +65,10 @@ pub const API_IP_CACHE_FILENAME: &str = "api-ip-address.txt";
const ACCOUNTS_URL_PREFIX: &str = "accounts/v1";
const APP_URL_PREFIX: &str = "app/v1";
+
+#[cfg(target_os = "ios")]
+const APPLE_PAYMENT_URL_PREFIX: &str = "payments/apple/v2";
+
#[cfg(target_os = "android")]
const GOOGLE_PAYMENTS_URL_PREFIX: &str = "payments/google-play/v1";
@@ -599,6 +603,35 @@ impl AccountsProxy {
}
}
+ #[cfg(target_os = "ios")]
+ pub async fn init_storekit_payment(
+ &self,
+ account: AccountNumber,
+ ) -> Result<rest::Response<Incoming>, rest::Error> {
+ let request = self
+ .handle
+ .factory
+ .post(&format!("{APPLE_PAYMENT_URL_PREFIX}/init"))?
+ .expected_status(&[StatusCode::OK])
+ .account(account)?;
+ self.handle.service.request(request).await
+ }
+
+ #[cfg(target_os = "ios")]
+ pub async fn check_storekit_payment(
+ &self,
+ account: AccountNumber,
+ body: Vec<u8>,
+ ) -> Result<rest::Response<Incoming>, rest::Error> {
+ let request = self
+ .handle
+ .factory
+ .post_json_bytes(&format!("{APPLE_PAYMENT_URL_PREFIX}/check"), body)?
+ .expected_status(&[StatusCode::OK])
+ .account(account)?;
+ self.handle.service.request(request).await
+ }
+
#[cfg(target_os = "android")]
pub fn init_play_purchase(
&mut self,
diff --git a/mullvad-api/src/rest.rs b/mullvad-api/src/rest.rs
index 94c43392ad..14e37ec29e 100644
--- a/mullvad-api/src/rest.rs
+++ b/mullvad-api/src/rest.rs
@@ -606,6 +606,10 @@ impl RequestFactory {
self.json_request(Method::POST, path, body)
}
+ pub fn post_json_bytes(&self, path: &str, body: Vec<u8>) -> Result<Request<Full<Bytes>>> {
+ self.json_request_with_bytes(Method::POST, path, body)
+ }
+
pub fn put_json<S: serde::Serialize>(
&self,
path: &str,
@@ -618,17 +622,16 @@ impl RequestFactory {
self.default_timeout = timeout;
self
}
- fn json_request<S: serde::Serialize>(
+ fn json_request_with_bytes(
&self,
method: Method,
path: &str,
- body: &S,
+ body: Vec<u8>,
) -> Result<Request<Full<Bytes>>> {
let mut request = self.hyper_request(path, method)?;
- let json_body = serde_json::to_vec(&body)?;
- let body_length = json_body.len();
- *request.body_mut() = Full::new(Bytes::from(json_body));
+ let body_length = body.len();
+ *request.body_mut() = Full::new(Bytes::from(body));
let headers = request.headers_mut();
headers.insert(header::CONTENT_LENGTH, HeaderValue::from(body_length));
@@ -640,6 +643,16 @@ impl RequestFactory {
Ok(Request::new(request, self.token_store.clone()).timeout(self.default_timeout))
}
+ fn json_request<S: serde::Serialize>(
+ &self,
+ method: Method,
+ path: &str,
+ body: &S,
+ ) -> Result<Request<Full<Bytes>>> {
+ let json_body = serde_json::to_vec(&body)?;
+ self.json_request_with_bytes(method, path, json_body)
+ }
+
fn hyper_request<B: Default>(&self, path: &str, method: Method) -> Result<http::Request<B>> {
let uri = self.get_uri(path)?;
let request = http::request::Builder::new()
diff --git a/mullvad-ios/src/api_client/mod.rs b/mullvad-ios/src/api_client/mod.rs
index bf849bc45c..c65fe524a0 100644
--- a/mullvad-ios/src/api_client/mod.rs
+++ b/mullvad-ios/src/api_client/mod.rs
@@ -28,6 +28,7 @@ mod problem_report;
mod response;
mod retry_strategy;
mod shadowsocks_loader;
+mod storekit;
#[repr(C)]
pub struct SwiftApiContext(*const ApiContext);
diff --git a/mullvad-ios/src/api_client/storekit.rs b/mullvad-ios/src/api_client/storekit.rs
new file mode 100644
index 0000000000..c1ee0d26ca
--- /dev/null
+++ b/mullvad-ios/src/api_client/storekit.rs
@@ -0,0 +1,161 @@
+use std::os::raw::c_char;
+
+use mullvad_api::{
+ rest::{self, MullvadRestHandle},
+ AccountsProxy,
+};
+use mullvad_types::account::AccountNumber;
+
+use super::{
+ cancellation::{RequestCancelHandle, SwiftCancelHandle},
+ completion::{CompletionCookie, SwiftCompletionHandler},
+ do_request,
+ helpers::convert_c_string,
+ 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_ios_init_storekit_payment(
+ 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.rust_context();
+
+ // SAFETY: See SwiftRetryStrategy::into_rust.
+ let retry_strategy = unsafe { retry_strategy.into_rust() };
+
+ let completion = completion_handler.clone();
+
+ // SAFETY: See param documentation for `account_number`.
+ let account_number = unsafe { AccountNumber::from(convert_c_string(account_number)) };
+
+ let task = tokio_handle.spawn(async move {
+ match mullvad_ios_init_storekit_payment_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_ios_init_storekit_payment_inner(
+ rest_client: MullvadRestHandle,
+ retry_strategy: RetryStrategy,
+ account_number: AccountNumber,
+) -> Result<SwiftMullvadApiResponse, rest::Error> {
+ let account_proxy = AccountsProxy::new(rest_client);
+
+ let future_factory = || account_proxy.init_storekit_payment(account_number.clone());
+
+ do_request(retry_strategy, future_factory).await
+}
+
+/// # 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.
+///
+/// `body` must be a pointer to a contiguous memory segment
+///
+/// `body_size` must be the size of the body
+///
+/// This function is not safe to call multiple times with the same `CompletionCookie`.
+#[no_mangle]
+pub unsafe extern "C" fn mullvad_ios_check_storekit_payment(
+ api_context: SwiftApiContext,
+ completion_cookie: *mut libc::c_void,
+ retry_strategy: SwiftRetryStrategy,
+ account_number: *const c_char,
+ body: *const u8,
+ body_size: usize,
+) -> 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.rust_context();
+ // SAFETY: See SwiftRetryStrategy::into_rust.
+ let retry_strategy = unsafe { retry_strategy.into_rust() };
+
+ let completion = completion_handler.clone();
+
+ // SAFETY: See param documentation for `account_number`.
+ let account_number = unsafe { AccountNumber::from(convert_c_string(account_number)) };
+
+ // SAFETY: See param documentation for `body`.
+ let body = unsafe { std::slice::from_raw_parts(body, body_size) }.to_vec();
+ let task = tokio_handle.spawn(async move {
+ match mullvad_ios_check_storekit_payment_inner(
+ api_context.rest_handle(),
+ retry_strategy,
+ account_number,
+ body,
+ )
+ .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_ios_check_storekit_payment_inner(
+ rest_client: MullvadRestHandle,
+ retry_strategy: RetryStrategy,
+ account_number: AccountNumber,
+ body: Vec<u8>,
+) -> Result<SwiftMullvadApiResponse, rest::Error> {
+ let account_proxy = AccountsProxy::new(rest_client);
+
+ let future_factory =
+ || account_proxy.check_storekit_payment(account_number.clone(), body.clone());
+
+ do_request(retry_strategy, future_factory).await
+}