From 3ac7d75235f3b88a8dcaf8f3d45b4c8acb2c25ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Thu, 29 Feb 2024 07:16:47 +0000 Subject: [PATCH] Add UI error message when IAP succeeds but receipt verification fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should not be visible to end-users on normal circumstances, but we should regardless show an error message if something goes wrong with the IAP receipt verification, to prompt them to contact support. Testing ------- PASS Device: iPhone simulator iOS: 17.2 Damus: This commit damus-api: d3801376fa204433661be6de8b7974f12b0ad25f Setup: - Local Testing server - Xcode StoreKit environment Steps: 1. Set MOCK_VERIFY to false on the server (that means receipt verification will fail on Xcode environment) 2. Try to make IAP purchase. Error should appear on UI 3. Set MOCK_VERIFY to true on the server and restart StoreKit environment (Receipt validation will always work) 5. Try to make IAP purchase. Onboarding flow should go as normal with no error messages. PASS Signed-off-by: Daniel D’Aquino --- damus/Models/Purple/DamusPurple.swift | 60 +++++++++++------------- damus/Models/Purple/StoreObserver.swift | 4 +- damus/Views/Purple/DamusPurpleView.swift | 6 +++ 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/damus/Models/Purple/DamusPurple.swift b/damus/Models/Purple/DamusPurple.swift index c5a2ba9c..679ca8c7 100644 --- a/damus/Models/Purple/DamusPurple.swift +++ b/damus/Models/Purple/DamusPurple.swift @@ -120,7 +120,7 @@ class DamusPurple: StoreObserverDelegate { // During testing I found that the purchase initiated via `purchase` was not emitted via the listener `StoreKit.Transaction.updates` until the app was restarted. self.storekit_manager.record_purchased_product(StoreKitManager.PurchasedProduct(tx: tx, product: product)) // Send the receipt to the server - await self.send_receipt() + try await self.send_receipt() default: // Any time we get a non-verified result, it means that the purchase was not successful, and thus we should throw an error. throw PurpleError.iap_purchase_error(result: result) @@ -161,42 +161,37 @@ class DamusPurple: StoreObserverDelegate { return account_uuid_info.account_uuid } - func send_receipt() async { + func send_receipt() async throws { // Get the receipt if it's available. if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) { - do { - let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) - let receipt_base64_string = receiptData.base64EncodedString() - let account_uuid = try await self.get_maybe_cached_uuid_for_account() - let json_text: [String: String] = ["receipt": receipt_base64_string, "account_uuid": account_uuid.uuidString] - let json_data = try JSONSerialization.data(withJSONObject: json_text) - - let url = environment.api_base_url().appendingPathComponent("accounts/\(keypair.pubkey.hex())/apple-iap/app-store-receipt") - - Log.info("Sending in-app purchase receipt to Damus Purple server", for: .damus_purple) - - let (data, response) = try await make_nip98_authenticated_request( - method: .post, - url: url, - payload: json_data, - payload_type: .json, - auth_keypair: self.keypair - ) - - if let httpResponse = response as? HTTPURLResponse { - switch httpResponse.statusCode { - case 200: - Log.info("Sent in-app purchase receipt to Damus Purple server successfully", for: .damus_purple) - default: - Log.error("Error in sending in-app purchase receipt to Damus Purple. HTTP status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown") - } + let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) + let receipt_base64_string = receiptData.base64EncodedString() + let account_uuid = try await self.get_maybe_cached_uuid_for_account() + let json_text: [String: String] = ["receipt": receipt_base64_string, "account_uuid": account_uuid.uuidString] + let json_data = try JSONSerialization.data(withJSONObject: json_text) + + let url = environment.api_base_url().appendingPathComponent("accounts/\(keypair.pubkey.hex())/apple-iap/app-store-receipt") + + Log.info("Sending in-app purchase receipt to Damus Purple server", for: .damus_purple) + + let (data, response) = try await make_nip98_authenticated_request( + method: .post, + url: url, + payload: json_data, + payload_type: .json, + auth_keypair: self.keypair + ) + + if let httpResponse = response as? HTTPURLResponse { + switch httpResponse.statusCode { + case 200: + Log.info("Sent in-app purchase receipt to Damus Purple server successfully", for: .damus_purple) + default: + Log.error("Error in sending in-app purchase receipt to Damus Purple. HTTP status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown") + throw DamusPurple.PurpleError.iap_receipt_verification_error(status: httpResponse.statusCode, response: data) } - - } - catch { - Log.error("Couldn't read receipt data with error: %s", for: .damus_purple, error.localizedDescription) } } } @@ -453,6 +448,7 @@ extension DamusPurple { case http_response_error(status_code: Int, response: Data) case error_processing_response case iap_purchase_error(result: Product.PurchaseResult) + case iap_receipt_verification_error(status: Int, response: Data) case translation_no_response case checkout_npub_verification_error } diff --git a/damus/Models/Purple/StoreObserver.swift b/damus/Models/Purple/StoreObserver.swift index 4a898b8f..a78a585e 100644 --- a/damus/Models/Purple/StoreObserver.swift +++ b/damus/Models/Purple/StoreObserver.swift @@ -23,11 +23,11 @@ class StoreObserver: NSObject, SKPaymentTransactionObserver { //Handle transaction states here. Task { - await self.delegate?.send_receipt() + try await self.delegate?.send_receipt() } } } protocol StoreObserverDelegate { - func send_receipt() async + func send_receipt() async throws } diff --git a/damus/Views/Purple/DamusPurpleView.swift b/damus/Views/Purple/DamusPurpleView.swift index 11644033..32364faf 100644 --- a/damus/Views/Purple/DamusPurpleView.swift +++ b/damus/Views/Purple/DamusPurpleView.swift @@ -144,6 +144,12 @@ struct DamusPurpleView: View, DamusPurpleStoreKitManagerDelegate { // If account is no longer active or was purchased via IAP, then show IAP purchase/manage options if let account_uuid { DamusPurpleView.IAPProductStateView(products: products, purchased: purchased, account_uuid: account_uuid, subscribe: subscribe) + if let iap_error { + Text(String(format: NSLocalizedString("There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: %@", comment: "In-app purchase error message for the user"), iap_error)) + .foregroundStyle(.red) + .multilineTextAlignment(.center) + .padding(.horizontal) + } } else { ProgressView()