Skip to content

Commit

Permalink
Implement PromotionalOffer model
Browse files Browse the repository at this point in the history
  • Loading branch information
nik3212 committed Jan 15, 2024
1 parent d083950 commit e49a982
Show file tree
Hide file tree
Showing 15 changed files with 490 additions and 82 deletions.
20 changes: 13 additions & 7 deletions Sources/Flare/Classes/Flare.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,17 @@ extension Flare: IFlare {
try await iapProvider.fetch(productIDs: productIDs)
}

public func purchase(product: StoreProduct, completion: @escaping Closure<Result<StoreTransaction, IAPError>>) {
public func purchase(
product: StoreProduct,
promotionalOffer: PromotionalOffer?,
completion: @escaping Closure<Result<StoreTransaction, IAPError>>
) {
guard iapProvider.canMakePayments else {
completion(.failure(.paymentNotAllowed))
return
}

iapProvider.purchase(product: product) { result in
iapProvider.purchase(product: product, promotionalOffer: promotionalOffer) { result in
switch result {
case let .success(transaction):
completion(.success(transaction))
Expand All @@ -60,31 +64,33 @@ extension Flare: IFlare {
}
}

public func purchase(product: StoreProduct) async throws -> StoreTransaction {
public func purchase(product: StoreProduct, promotionalOffer: PromotionalOffer?) async throws -> StoreTransaction {
guard iapProvider.canMakePayments else { throw IAPError.paymentNotAllowed }
return try await iapProvider.purchase(product: product)
return try await iapProvider.purchase(product: product, promotionalOffer: promotionalOffer)
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
public func purchase(
product: StoreProduct,
options: Set<StoreKit.Product.PurchaseOption>,
promotionalOffer: PromotionalOffer?,
completion: @escaping SendableClosure<Result<StoreTransaction, IAPError>>
) {
guard iapProvider.canMakePayments else {
completion(.failure(.paymentNotAllowed))
return
}
iapProvider.purchase(product: product, options: options, completion: completion)
iapProvider.purchase(product: product, options: options, promotionalOffer: promotionalOffer, completion: completion)
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
public func purchase(
product: StoreProduct,
options: Set<StoreKit.Product.PurchaseOption>
options: Set<StoreKit.Product.PurchaseOption>,
promotionalOffer: PromotionalOffer?
) async throws -> StoreTransaction {
guard iapProvider.canMakePayments else { throw IAPError.paymentNotAllowed }
return try await iapProvider.purchase(product: product, options: options)
return try await iapProvider.purchase(product: product, options: options, promotionalOffer: promotionalOffer)
}

public func receipt(completion: @escaping Closure<Result<String, IAPError>>) {
Expand Down
101 changes: 97 additions & 4 deletions Sources/Flare/Classes/IFlare.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import Foundation
import StoreKit

// MARK: - IFlare

/// `Flare` creates and manages in-app purchases.
public protocol IFlare {
/// Retrieves localized information from the App Store about a specified list of products.
Expand All @@ -32,21 +34,28 @@ public protocol IFlare {
///
/// - Parameters:
/// - product: The product to be purchased.
/// - promotionalOffer: The promotional offer.
/// - completion: The closure to be executed once the purchase is complete.
func purchase(product: StoreProduct, completion: @escaping Closure<Result<StoreTransaction, IAPError>>)
func purchase(
product: StoreProduct,
promotionalOffer: PromotionalOffer?,
completion: @escaping Closure<Result<StoreTransaction, IAPError>>
)

/// Purchases a product.
///
/// - Note: The method automatically checks if the user can purchase a product.
/// If the user can't make a payment, the method returns an error
/// with the type `IAPError.paymentNotAllowed`.
///
/// - Parameter product: The product to be purchased.
/// - Parameters:
/// - product: The product to be purchased.
/// - promotionalOffer: The promotional offer.
///
/// - Throws: `IAPError.paymentNotAllowed` if user can't make payment.
///
/// - Returns: A payment transaction.
func purchase(product: StoreProduct) async throws -> StoreTransaction
func purchase(product: StoreProduct, promotionalOffer: PromotionalOffer?) async throws -> StoreTransaction

/// Purchases a product.
///
Expand All @@ -57,6 +66,7 @@ public protocol IFlare {
/// - Parameters:
/// - product: The product to be purchased.
/// - options: The optional settings for a product purchase.
/// - promotionalOffer: The promotional offer.
/// - completion: The closure to be executed once the purchase is complete.
///
/// - Throws: `IAPError.paymentNotAllowed` if user can't make payment.
Expand All @@ -66,6 +76,7 @@ public protocol IFlare {
func purchase(
product: StoreProduct,
options: Set<StoreKit.Product.PurchaseOption>,
promotionalOffer: PromotionalOffer?,
completion: @escaping SendableClosure<Result<StoreTransaction, IAPError>>
)

Expand All @@ -78,12 +89,17 @@ public protocol IFlare {
/// - Parameters:
/// - product: The product to be purchased.
/// - options: The optional settings for a product purchase.
/// - promotionalOffer: The promotional offer.
///
/// - Throws: `IAPError.paymentNotAllowed` if user can't make payment.
///
/// - Returns: A payment transaction.
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func purchase(product: StoreProduct, options: Set<StoreKit.Product.PurchaseOption>) async throws -> StoreTransaction
func purchase(
product: StoreProduct,
options: Set<StoreKit.Product.PurchaseOption>,
promotionalOffer: PromotionalOffer?
) async throws -> StoreTransaction

/// Refreshes the receipt, representing the user's transactions with your app.
///
Expand Down Expand Up @@ -129,3 +145,80 @@ public protocol IFlare {
func beginRefundRequest(productID: String) async throws -> RefundRequestStatus
#endif
}

public extension IFlare {
/// Performs a purchase of a product.
///
/// - Note: The method automatically checks if the user can purchase a product.
/// If the user can't make a payment, the method returns an error
/// with the type `IAPError.paymentNotAllowed`.
///
/// - Parameters:
/// - product: The product to be purchased.
/// - completion: The closure to be executed once the purchase is complete.
func purchase(
product: StoreProduct,
completion: @escaping Closure<Result<StoreTransaction, IAPError>>
) {
purchase(product: product, promotionalOffer: nil, completion: completion)
}

/// Purchases a product.
///
/// - Note: The method automatically checks if the user can purchase a product.
/// If the user can't make a payment, the method returns an error
/// with the type `IAPError.paymentNotAllowed`.
///
/// - Parameter product: The product to be purchased.
///
/// - Throws: `IAPError.paymentNotAllowed` if user can't make payment.
///
/// - Returns: A payment transaction.
func purchase(product: StoreProduct) async throws -> StoreTransaction {
try await purchase(product: product, promotionalOffer: nil)
}

/// Purchases a product.
///
/// - Note: The method automatically checks if the user can purchase a product.
/// If the user can't make a payment, the method returns an error
/// with the type `IAPError.paymentNotAllowed`.
///
/// - Parameters:
/// - product: The product to be purchased.
/// - options: The optional settings for a product purchase.
/// - completion: The closure to be executed once the purchase is complete.
///
/// - Throws: `IAPError.paymentNotAllowed` if user can't make payment.
///
/// - Returns: A payment transaction.
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func purchase(
product: StoreProduct,
options: Set<StoreKit.Product.PurchaseOption>,
completion: @escaping SendableClosure<Result<StoreTransaction, IAPError>>
) {
purchase(product: product, options: options, promotionalOffer: nil, completion: completion)
}

/// Purchases a product.
///
/// - Note: The method automatically checks if the user can purchase a product.
/// If the user can't make a payment, the method returns an error
/// with the type `IAPError.paymentNotAllowed`.
///
/// - Parameters:
/// - product: The product to be purchased.
/// - options: The optional settings for a product purchase.
///
/// - Throws: `IAPError.paymentNotAllowed` if user can't make payment.
///
/// - Returns: A payment transaction.
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func purchase(
product: StoreProduct,
options: Set<StoreKit.Product.PurchaseOption>
) async throws -> StoreTransaction {
try await purchase(product: product, options: options, promotionalOffer: nil)
}
}
4 changes: 4 additions & 0 deletions Sources/Flare/Classes/Models/IAPError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public enum IAPError: Swift.Error {
///
/// - Note: This is only available for StoreKit 2 transactions.
case paymentDefferred
/// The decoding signature is failed.
///
/// - Note: This is only available for StoreKit 2 transactions.
case failedToDecodeSignature(signature: String)
/// The unknown error occurred.
case unknown
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ protocol ISKProduct {

/// <#Description#>
var discounts: [StoreProductDiscount] { get }

var subscriptionGroupIdentifier: String? { get }
}
10 changes: 5 additions & 5 deletions Sources/Flare/Classes/Models/Internal/SK1StoreProduct.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,9 @@ extension SK1StoreProduct: ISKProduct {
}

var productCategory: ProductCategory? {
guard #available(iOS 11.2, macOS 10.13.2, tvOS 11.2, watchOS 6.2, *) else {
return .nonSubscription
}
return subscriptionPeriod == nil ? .nonSubscription : .subscription
subscriptionPeriod == nil ? .nonSubscription : .subscription
}

@available(iOS 11.2, macOS 10.13.2, tvOS 11.2, watchOS 6.2, *)
var subscriptionPeriod: SubscriptionPeriod? {
guard let subscriptionPeriod = product.subscriptionPeriod, subscriptionPeriod.numberOfUnits > 0 else {
return nil
Expand All @@ -77,4 +73,8 @@ extension SK1StoreProduct: ISKProduct {
var discounts: [StoreProductDiscount] {
product.discounts.compactMap { StoreProductDiscount(skProductDiscount: $0) }
}

var subscriptionGroupIdentifier: String? {
product.subscriptionGroupIdentifier
}
}
4 changes: 4 additions & 0 deletions Sources/Flare/Classes/Models/Internal/SK2StoreProduct.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,8 @@ extension SK2StoreProduct: ISKProduct {
StoreProductDiscount(discount: $0, currencyCode: self.currencyCode)
} ?? []
}

var subscriptionGroupIdentifier: String? {
product.subscription?.subscriptionGroupID
}
}
90 changes: 90 additions & 0 deletions Sources/Flare/Classes/Models/PromotionalOffer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// Flare
// Copyright © 2024 Space Code. All rights reserved.
//

import Foundation
import StoreKit

// MARK: - PromotionalOffer

public final class PromotionalOffer: NSObject, Sendable {
// MARK: Properties

public let discount: StoreProductDiscount
public let signedData: SignedData

// MARK: Initialization

public init(discount: StoreProductDiscount, signedData: SignedData) {
self.discount = discount
self.signedData = signedData
}
}

// MARK: PromotionalOffer.SignedData

public extension PromotionalOffer {
final class SignedData: NSObject, Sendable {
// MARK: Properties

public let identifier: String
public let keyIdentifier: String
public let nonce: UUID
public let signature: String
public let timestamp: Int

public init(identifier: String, keyIdentifier: String, nonce: UUID, signature: String, timestamp: Int) {
self.identifier = identifier
self.keyIdentifier = keyIdentifier
self.nonce = nonce
self.signature = signature
self.timestamp = timestamp
}
}
}

// MARK: - Convenience Initializators

extension PromotionalOffer.SignedData {
convenience init(paymentDiscount: SKPaymentDiscount) {
self.init(
identifier: paymentDiscount.identifier,
keyIdentifier: paymentDiscount.keyIdentifier,
nonce: paymentDiscount.nonce,
signature: paymentDiscount.signature,
timestamp: paymentDiscount.timestamp.intValue
)
}
}

// MARK: - Helpers

extension PromotionalOffer.SignedData {
var skPromotionalOffer: SKPaymentDiscount {
SKPaymentDiscount(
identifier: identifier,
keyIdentifier: keyIdentifier,
nonce: nonce,
signature: signature,
timestamp: .init(integerLiteral: timestamp)
)
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
var promotionalOffer: Product.PurchaseOption {
get throws {
guard let data = Data(base64Encoded: signature) else {
throw IAPError.failedToDecodeSignature(signature: signature)
}

return .promotionalOffer(
offerID: identifier,
keyID: keyIdentifier,
nonce: nonce,
signature: data,
timestamp: timestamp
)
}
}
}
Loading

0 comments on commit e49a982

Please sign in to comment.