Skip to content
This repository was archived by the owner on Aug 21, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 29 additions & 10 deletions Sources/InAppPurchase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<SKPayment, Swift.Error>) -> 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
Expand All @@ -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))
}
Expand Down
72 changes: 72 additions & 0 deletions Sources/Internal/Internal+Product.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
}
24 changes: 24 additions & 0 deletions Sources/Product.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
70 changes: 70 additions & 0 deletions Tests/InAppPurchaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down