summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2025-05-08 09:32:52 +0200
committerBug Magnet <marco.nikic@mullvad.net>2025-05-08 09:32:52 +0200
commitcaf05bc110aeb3f6c0b4db9723629818e7e81d99 (patch)
tree4a15da846efe1f5fe9bf486d2c35727b1e2d1b31 /ios
parenta74a0c0aa3270bb2cf7861f94ad7dc590ed50d22 (diff)
parent01660f7d4af6fc539ce938f44f26bdf55337b57a (diff)
downloadmullvadvpn-caf05bc110aeb3f6c0b4db9723629818e7e81d99.tar.xz
mullvadvpn-caf05bc110aeb3f6c0b4db9723629818e7e81d99.zip
Merge branch 'implement-storekit2-api-calls-using-mullvad-api-ios-1158'
Diffstat (limited to 'ios')
-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
17 files changed, 311 insertions, 52 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)