From 6aa1a644688c53728bc06a572c44a9edec914dbc Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 20 Jun 2023 15:26:42 -0700 Subject: [PATCH] `getPromotionalOffer`: return `ErrorCode.ineligibleError` if receipt is not found Fixes #2580. Promotional offers require a receipt. If there is none (like in sandbox), it means no purchases were found, which implies that the user is not eligible. --- .../Purchases/PurchasesOrchestrator.swift | 13 +++++++--- .../StoreKitIntegrationTests.swift | 11 ++++++++ .../PurchasesOrchestratorTests.swift | 26 +++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift index 4dae2deab1..82b7ee2097 100644 --- a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift +++ b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift @@ -234,10 +234,15 @@ final class PurchasesOrchestrator { return } - receiptFetcher.receiptData(refreshPolicy: .onlyIfEmpty) { receiptData, receiptURL in - guard let receiptData = receiptData, - !receiptData.isEmpty else { - completion(.failure(ErrorUtils.missingReceiptFileError(receiptURL))) + self.receiptFetcher.receiptData(refreshPolicy: .onlyIfEmpty) { receiptData, receiptURL in + guard let receiptData = receiptData, !receiptData.isEmpty else { + let underlyingError = ErrorUtils.missingReceiptFileError(receiptURL) + + // Promotional offers require existing purchases. + // If no receipt is found, this is most likely in sandbox with no purchases, + // so producing an "ineligible" error is better. + completion(.failure(ErrorUtils.ineligibleError(error: underlyingError))) + return } diff --git a/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift index f123c9a71a..386e9a3a18 100644 --- a/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift @@ -416,6 +416,17 @@ class StoreKit1IntegrationTests: BaseStoreKitIntegrationTests { try await subscribe() } + func testGetPromotionalOfferWithNoPurchasesReturnsIneligible() async throws { + let product = try await self.monthlyPackage.storeProduct + let discount = try XCTUnwrap(product.discounts.onlyElement) + + do { + _ = try await Purchases.shared.promotionalOffer(forProductDiscount: discount, product: product) + } catch { + expect(error).to(matchError(ErrorCode.ineligibleError)) + } + } + func testUserHasNoEligibleOffersByDefault() async throws { let (_, created) = try await Purchases.shared.logIn(UUID().uuidString) expect(created) == true diff --git a/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift b/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift index 6f6fd6547d..6088f177eb 100644 --- a/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift +++ b/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift @@ -371,6 +371,32 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase { expect(self.offerings.invokedPostOfferParameters?.offerIdentifier) == storeProductDiscount.offerIdentifier } + func testGetPromotionalOfferFailsWithIneligibleIfNoReceiptIsFound() async throws { + self.receiptFetcher.shouldReturnReceipt = false + + let product = try await self.fetchSk1Product() + let storeProductDiscount = MockStoreProductDiscount(offerIdentifier: "offerid1", + currencyCode: product.priceLocale.currencyCode, + price: 11.1, + localizedPriceString: "$11.10", + paymentMode: .payAsYouGo, + subscriptionPeriod: .init(value: 1, unit: .month), + numberOfPeriods: 2, + type: .promotional) + + do { + _ = try await Async.call { completion in + self.orchestrator.promotionalOffer(forProductDiscount: storeProductDiscount, + product: StoreProduct(sk1Product: product), + completion: completion) + } + } catch { + expect(error).to(matchError(ErrorCode.ineligibleError)) + } + + expect(self.offerings.invokedPostOffer) == false + } + func testGetSK1PromotionalOfferFailsWithIneligibleDiscount() async throws { self.customerInfoManager.stubbedCachedCustomerInfoResult = mockCustomerInfo self.offerings.stubbedPostOfferCompletionResult = .failure(