Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Implement Promotional Offers #16

Merged
merged 17 commits into from
Jan 18, 2024
Merged
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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
## [Unreleased]

## Added
- Implement Support for Promotional Offers
- Added in Pull Request [#16](https://github.com/space-code/flare/pull/16).

- Add additional badges to `README.md`
- Added in Pull Request [#15](https://github.com/space-code/flare/pull/15).

Expand Down Expand Up @@ -55,4 +58,4 @@ Released on 2023-01-20.

#### Added
- Initial release of Flare.
- Added by [Nikita Vasilev](https://github.com/nik3212).
- Added by [Nikita Vasilev](https://github.com/nik3212).
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ Flare is a framework written in Swift that makes it easy for you to work with in
## Features
- [x] Support Consumable & Non-Consumable Purchases
- [x] Support Subscription Purchase
- [x] Refresh Receipt
- [x] Complete Unit Test Coverage
- [x] Support Promotional & Introductory Offers
- [x] iOS, tvOS, watchOS, macOS, and visionOS compatible
- [x] Complete Unit & Integration Test Coverage

## Documentation
Check out [flare documentation](https://space-code.github.io/flare/documentation/flare/).
Expand Down
70 changes: 70 additions & 0 deletions Sources/Flare/Classes/DI/FlareDependencies.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// Flare
// Copyright © 2024 Space Code. All rights reserved.
//

import Concurrency
import Foundation
import StoreKit

/// The package's dependencies.
final class FlareDependencies: IFlareDependencies {
// MARK: Internal

lazy var iapProvider: IIAPProvider = IAPProvider(
paymentQueue: SKPaymentQueue.default(),
productProvider: productProvider,
purchaseProvider: purchaseProvider,
receiptRefreshProvider: receiptRefreshProvider,
refundProvider: refundProvider,
eligibilityProvider: eligibilityProvider,
redeemCodeProvider: redeemCodeProvider
)

lazy var configurationProvider: IConfigurationProvider = ConfigurationProvider()

// MARK: Private

private var productProvider: IProductProvider {
ProductProvider(
dispatchQueueFactory: DispatchQueueFactory()
)
}

private var purchaseProvider: IPurchaseProvider {
PurchaseProvider(
paymentProvider: paymentProvider,
configurationProvider: configurationProvider
)
}

private var paymentProvider: IPaymentProvider {
PaymentProvider(
paymentQueue: SKPaymentQueue.default(),
dispatchQueueFactory: DispatchQueueFactory()
)
}

private var receiptRefreshProvider: IReceiptRefreshProvider {
ReceiptRefreshProvider(
dispatchQueueFactory: DispatchQueueFactory(),
receiptRefreshRequestFactory: ReceiptRefreshRequestFactory()
)
}

private var refundProvider: IRefundProvider {
RefundProvider(
systemInfoProvider: SystemInfoProvider()
)
}

private var eligibilityProvider: IEligibilityProvider {
EligibilityProvider()
}

private var redeemCodeProvider: IRedeemCodeProvider {
RedeemCodeProvider(systemInfoProvider: systemInfoProvider)
}

private lazy var systemInfoProvider: ISystemInfoProvider = SystemInfoProvider()
}
14 changes: 14 additions & 0 deletions Sources/Flare/Classes/DI/IFlareDependencies.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// Flare
// Copyright © 2024 Space Code. All rights reserved.
//

import Foundation

/// The package's dependencies.
protocol IFlareDependencies {
/// The IAP provider.
var iapProvider: IIAPProvider { get }
/// The configuration provider.
var configurationProvider: IConfigurationProvider { get }
}
78 changes: 61 additions & 17 deletions Sources/Flare/Classes/Flare.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,41 @@

/// The class creates and manages in-app purchases.
public final class Flare {
// MARK: Properties

/// The in-app purchase provider.
private let iapProvider: IIAPProvider

/// The configuration provider.
private let configurationProvider: IConfigurationProvider

/// The singleton instance.
private static let flare: Flare = .init()

/// Returns a shared `Flare` object.
public static var shared: IFlare { flare }

// MARK: Initialization

/// Creates a new `Flare` instance.
///
/// - Parameter iapProvider: The in-app purchase provider.
init(iapProvider: IIAPProvider = IAPProvider()) {
self.iapProvider = iapProvider
/// - Parameters:
/// - dependencies: The package's dependencies.
/// - configurationProvider: The configuration provider.
init(dependencies: IFlareDependencies = FlareDependencies()) {
iapProvider = dependencies.iapProvider
configurationProvider = dependencies.configurationProvider
}

// MARK: Public

/// Returns a default `Flare` object.
public static let `default`: IFlare = Flare()

// MARK: Private

/// The in-app purchase provider.
private let iapProvider: IIAPProvider
/// Configures the Flare package with the provided configuration.
///
/// - Parameters:
/// - configuration: The configuration object containing settings for Flare.
public static func configure(with configuration: Configuration) {
flare.configurationProvider.configure(with: configuration)
}

Check warning on line 50 in Sources/Flare/Classes/Flare.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Flare/Classes/Flare.swift#L48-L50

Added lines #L48 - L50 were not covered by tests
}

// MARK: IFlare
Expand All @@ -44,13 +61,17 @@
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 +81,33 @@
}
}

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 Expand Up @@ -114,6 +137,11 @@
iapProvider.removeTransactionObserver()
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
public func checkEligibility(productIDs: Set<String>) async throws -> [String: SubscriptionEligibility] {
try await iapProvider.checkEligibility(productIDs: productIDs)
}

#if os(iOS) || VISION_OS
@available(iOS 15.0, *)
@available(macOS, unavailable)
Expand All @@ -122,5 +150,21 @@
public func beginRefundRequest(productID: String) async throws -> RefundRequestStatus {
try await iapProvider.beginRefundRequest(productID: productID)
}

@available(iOS 14.0, *)
@available(macOS, unavailable)
@available(watchOS, unavailable)
@available(tvOS, unavailable)
public func presentCodeRedemptionSheet() {
iapProvider.presentCodeRedemptionSheet()
}

@available(iOS 16.0, *)
@available(macOS, unavailable)
@available(watchOS, unavailable)
@available(tvOS, unavailable)
public func presentOfferCodeRedeemSheet() async throws {
try await iapProvider.presentOfferCodeRedeemSheet()
}
#endif
}
24 changes: 24 additions & 0 deletions Sources/Flare/Classes/Foundation/UserDefaults/IUserDefaults.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// Flare
// Copyright © 2024 Space Code. All rights reserved.
//

import Foundation

/// Protocol for managing `UserDefaults` operations.
protocol IUserDefaults {
/// Sets a `Codable` value in `UserDefaults` for a given key.
///
/// - Parameters:
/// - key: The key to associate with the Codable value.
///
/// - codable: The Codable value to be stored.
func set<T: Codable>(key: String, codable: T)

/// Retrieves a `Codable` value from `UserDefaults` for a given key.
///
/// - Parameter key: The key associated with the desired Codable value.
///
/// - Returns: The Codable value stored for the given key, or nil if not found.
func get<T: Codable>(key: String) -> T?
}
19 changes: 19 additions & 0 deletions Sources/Flare/Classes/Foundation/UserDefaults/UserDefaults.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// Flare
// Copyright © 2024 Space Code. All rights reserved.
//

import Foundation

extension UserDefaults: IUserDefaults {
func set<T: Codable>(key: String, codable: T) {
guard let value = try? JSONEncoder().encode(codable) else { return }
set(value, forKey: key)
}

Check warning on line 12 in Sources/Flare/Classes/Foundation/UserDefaults/UserDefaults.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Flare/Classes/Foundation/UserDefaults/UserDefaults.swift#L9-L12

Added lines #L9 - L12 were not covered by tests

func get<T: Codable>(key: String) -> T? {
let data = object(forKey: key) as? Data
guard let data = data, let value = try? JSONDecoder().decode(T.self, from: data) else { return nil }
return value

Check warning on line 17 in Sources/Flare/Classes/Foundation/UserDefaults/UserDefaults.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Flare/Classes/Foundation/UserDefaults/UserDefaults.swift#L17

Added line #L17 was not covered by tests
}
}
6 changes: 6 additions & 0 deletions Sources/Flare/Classes/Helpers/PaymentQueue/PaymentQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,10 @@ public protocol PaymentQueue: AnyObject {
/// Remove a finished (i.e. failed or completed) transaction from the queue.
/// Attempting to finish a purchasing transaction will throw an exception.
func finishTransaction(_ transaction: SKPaymentTransaction)

#if os(iOS) || VISION_OS
// Call this method to have StoreKit present a sheet enabling the user to redeem codes provided by your app.
@available(iOS 14.0, *)
func presentCodeRedemptionSheet()
#endif
}
Loading
Loading