From eefb707c3d1be9166b12565a736b610fdec0fe14 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Fri, 26 Nov 2021 11:51:15 +0900 Subject: [PATCH 1/2] feat: add `InAppPurchase#purchase(productIdentifier:, paymentBuildWith:, handler:)` ref: #35 --- Sources/InAppPurchase.swift | 39 ++++++++++++++----- Tests/InAppPurchaseTests.swift | 70 ++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 10 deletions(-) diff --git a/Sources/InAppPurchase.swift b/Sources/InAppPurchase.swift index 2442f82..981b42f 100644 --- a/Sources/InAppPurchase.swift +++ b/Sources/InAppPurchase.swift @@ -152,6 +152,20 @@ extension InAppPurchase: InAppPurchaseProvidable { } public func purchase(productIdentifier: String, handler: InAppPurchase.PurchaseHandler? = nil) { + purchase( + productIdentifier: productIdentifier, + paymentBuildWith: { (product, completion) in + completion(.success(SKPayment(product: product))) + }, + handler: handler + ) + } + + public func purchase( + productIdentifier: String, + paymentBuildWith paymentBuilder: @escaping ((_ product: SKProduct, _ completion: @escaping ((_ result: Result) -> Void)) -> Void), + handler: InAppPurchase.PurchaseHandler? = nil + ) { // Fetch product from App Store let requestId = UUID().uuidString productProvider.fetch(productIdentifiers: [productIdentifier], requestId: requestId) { [weak self] (result) in @@ -162,19 +176,24 @@ extension InAppPurchase: InAppPurchaseProvidable { return } - // Add payment to App Store queue - let payment = SKPayment(product: product) - self?.paymentProvider.add(payment: payment, handler: { (_, result) in + paymentBuilder(product) { result in switch result { - case .success(let transaction): - InAppPurchase.handle( - transaction: transaction, - handler: handler - ) + case .success(let payment): + self?.paymentProvider.add(payment: payment, handler: { (_, result) in + switch result { + case .success(let transaction): + InAppPurchase.handle( + transaction: transaction, + handler: handler + ) + case .failure(let error): + handler?(.failure(error)) + } + }) case .failure(let error): - handler?(.failure(error)) + handler?(.failure(.init(code: .with(error: error), transaction: nil))) } - }) + } case .failure(let error): handler?(.failure(error)) } diff --git a/Tests/InAppPurchaseTests.swift b/Tests/InAppPurchaseTests.swift index 782dd92..018d3d8 100644 --- a/Tests/InAppPurchaseTests.swift +++ b/Tests/InAppPurchaseTests.swift @@ -461,6 +461,76 @@ class InAppPurchaseTests: XCTestCase { wait(for: [expectation1, expectation2], timeout: 1) } + func testPurchaseWithPaymentBuilder() { + let expectation1 = self.expectation() + let product = StubProduct(productIdentifier: "PRODUCT_001") + let productProvider = StubProductProvider(result: .success([product])) + let paymentProvider = StubPaymentProvider(addPaymentHandler: { (payment, handler) in + XCTAssertEqual(payment.productIdentifier, "PRODUCT_001") + XCTAssertEqual(payment.quantity, 99) + + let queue = StubPaymentQueue() + let originalTransaction = StubPaymentTransaction(transactionIdentifier: "ORIGINAL_TRANSACTION_001", transactionState: .purchased, payment: payment) + let transaction = StubPaymentTransaction(transactionIdentifier: "TRANSACTION_001", transactionState: .purchased, original: originalTransaction, payment: payment) + handler(queue, .success(transaction)) + + expectation1.fulfill() + }) + + let expectation2 = self.expectation() + let iap = InAppPurchase(product: productProvider, payment: paymentProvider) + iap.purchase( + productIdentifier: "PRODUCT_001", + paymentBuildWith: { (product, completion) in + let payment = SKMutablePayment(product: product) + payment.quantity = 99 + completion(.success(payment)) + }, + handler: { (result) in + switch result { + case .success(let state): + XCTAssertEqual(state.state, .purchased) + XCTAssertEqual(state.transaction.transactionIdentifier, "TRANSACTION_001") + XCTAssertEqual(state.transaction.originalTransactionIdentifier, "ORIGINAL_TRANSACTION_001") + case .failure: + XCTFail() + } + expectation2.fulfill() + }) + wait(for: [expectation1, expectation2], timeout: 1) + } + + func testPurchaseWithPaymentBuilderWhereFailureBuildPayment() { + let product = StubProduct(productIdentifier: "PRODUCT_001") + let productProvider = StubProductProvider(result: .success([product])) + let paymentProvider = StubPaymentProvider() + + let expectation = self.expectation() + let iap = InAppPurchase(product: productProvider, payment: paymentProvider) + iap.purchase( + productIdentifier: "PRODUCT_001", + paymentBuildWith: { (product, handler) in + handler(.failure(InAppPurchase.Error(code: .paymentNotAllowed, transaction: nil))) + }, + handler: { (result) in + switch result { + case .failure(let error): + switch error.code { + case .with(let error): + let error = error as? InAppPurchase.Error + XCTAssertNotNil(error) + XCTAssertEqual(error!.code, .paymentNotAllowed) + default: + XCTFail() + } + default: + XCTFail() + } + expectation.fulfill() + }) + wait(for: [expectation], timeout: 1) + } + func testConvertWhereSuccess() { let expectation = self.expectation() let purchaseHandler: InAppPurchase.PurchaseHandler = { result in From dec45a14622ea886e7ade06bdc4523844008a211 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Mon, 29 Nov 2021 17:54:58 +0900 Subject: [PATCH 2/2] feat: add support SKProductDiscount --- Sources/Internal/Internal+Product.swift | 72 +++++++++++++++++++++++++ Sources/Product.swift | 24 +++++++++ 2 files changed, 96 insertions(+) diff --git a/Sources/Internal/Internal+Product.swift b/Sources/Internal/Internal+Product.swift index 22006e0..3c0124f 100644 --- a/Sources/Internal/Internal+Product.swift +++ b/Sources/Internal/Internal+Product.swift @@ -49,6 +49,23 @@ extension Internal.Product: Product { } return Internal.ProductSubscriptionPeriod(subscriptionPeriod) } + var discounts: [ProductDiscount] { + guard #available(iOS 12.2, *) else { + return [] + } + return skProduct.discounts.map { Internal.ProductDiscount($0) } + } +} + +extension Internal { + @available(iOS 12.2, *) + struct ProductDiscount { + private let skProductDiscount: SKProductDiscount + + init(_ skProductDiscount: SKProductDiscount) { + self.skProductDiscount = skProductDiscount + } + } } extension Internal { @@ -80,3 +97,58 @@ extension PeriodUnit { } } } + +@available(iOS 12.2, *) +extension Internal.ProductDiscount: ProductDiscount { + var offerIdentifier: String? { + return skProductDiscount.identifier + } + + var type: ProductDiscountType? { + return ProductDiscountType(skProductDiscount.type) + } + + var price: Decimal { + return skProductDiscount.price as Decimal + } + + var priceLocale: Locale { + return skProductDiscount.priceLocale + } + + var paymentMode: ProductDiscountPaymentMode? { + return ProductDiscountPaymentMode(skProductDiscount.paymentMode) + } + + var numberOfPeriods: Int { + return skProductDiscount.numberOfPeriods + } + + var subscriptionPeriod: ProductSubscriptionPeriod? { + let period: SKProductSubscriptionPeriod = skProductDiscount.subscriptionPeriod + return Internal.ProductSubscriptionPeriod(period) + } +} + +@available(iOS 12.2, *) +extension ProductDiscountType { + init?(_ type: SKProductDiscount.`Type`) { + switch type { + case .introductory: self = .introductory + case .subscription: self = .subscription + @unknown default: return nil + } + } +} + +@available(iOS 11.2, *) +extension ProductDiscountPaymentMode { + init?(_ paymentMode: SKProductDiscount.PaymentMode) { + switch paymentMode { + case .freeTrial: self = .freeTrial + case .payAsYouGo: self = .payAsYouGo + case .payUpFront: self = .payUpFront + @unknown default: return nil + } + } +} diff --git a/Sources/Product.swift b/Sources/Product.swift index 5805ee5..03decab 100644 --- a/Sources/Product.swift +++ b/Sources/Product.swift @@ -18,6 +18,7 @@ public protocol Product { var downloadContentLengths: [NSNumber] { get } var downloadContentVersion: String { get } var subscriptionPeriod: ProductSubscriptionPeriod? { get } + var discounts: [ProductDiscount] { get } } public protocol ProductSubscriptionPeriod { @@ -31,3 +32,26 @@ public enum PeriodUnit { case month case year } + +public protocol ProductDiscount { + var offerIdentifier: String? { get } + var type: ProductDiscountType? { get } + var price: Decimal { get } + var priceLocale: Locale { get } + var paymentMode: ProductDiscountPaymentMode? { get } + var numberOfPeriods: Int { get } + var subscriptionPeriod: ProductSubscriptionPeriod? { get } +} + +public enum ProductDiscountType { + case introductory + case subscription + case unsupported +} + +public enum ProductDiscountPaymentMode { + case payAsYouGo + case payUpFront + case freeTrial + case unsupported +}