Skip to content

Commit

Permalink
Implement passing parameters to a view
Browse files Browse the repository at this point in the history
- Implement `onInAppPurchaseCompletion` to handle the completion of a purchase.
- Implement `inAppPurchaseOptions` to pass additional parameters for a purchase.
  • Loading branch information
ns-vasilev committed Mar 8, 2024
1 parent 4b4d141 commit d38ffbf
Show file tree
Hide file tree
Showing 24 changed files with 223 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ final class IAPProvider: IIAPProvider {
completion: { (result: Result<[StoreProduct], Error>) in
switch result {
case let .success(products):
completion(.success(products))
if products.isEmpty {
completion(.failure(.invalid(productIDs: Array(productIDs))))
} else {
completion(.success(products))
}
case let .failure(error):
completion(.failure(.with(error: error)))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// Flare
// Copyright © 2024 Space Code. All rights reserved.
//

import Flare
import SwiftUI

// MARK: - PurchaseCompletionKey

struct PurchaseCompletionKey: EnvironmentKey {
static var defaultValue: ((StoreProduct, Result<Void, Error>) -> Void)?
}

extension EnvironmentValues {
var purchaseCompletion: ((StoreProduct, Result<Void, Error>) -> Void)? {
get { self[PurchaseCompletionKey.self] }
set { self[PurchaseCompletionKey.self] = newValue }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// Flare
// Copyright © 2024 Space Code. All rights reserved.
//

import Flare
import StoreKit
import SwiftUI

// MARK: - PurchaseOptionKey

struct PurchaseOptionKey: EnvironmentKey {
static var defaultValue: ((StoreProduct) -> PurchaseOptions)?
}

extension EnvironmentValues {
var purchaseOptions: ((StoreProduct) -> PurchaseOptions)? {
get { self[PurchaseOptionKey.self] }
set { self[PurchaseOptionKey.self] = newValue }
}
}
24 changes: 24 additions & 0 deletions Sources/FlareUI/Classes/Core/Models/PurchaseOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// Flare
// Copyright © 2024 Space Code. All rights reserved.
//

import StoreKit

struct PurchaseOptions {
// MARK: Properties

private var _options: Any?

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
var options: Set<Product.PurchaseOption>? {
_options as? Set<Product.PurchaseOption>
}

// MARK: Initialization

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
init(options: Set<Product.PurchaseOption>) {
self._options = options
}
}
1 change: 1 addition & 0 deletions Sources/FlareUI/Classes/Generated/Colors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ internal enum Asset {
internal enum Colors {
internal static let dynamicBackground = ColorAsset(name: "Colors/dynamic_background")
internal static let gray = ColorAsset(name: "Colors/gray")
internal static let systemBackground = ColorAsset(name: "Colors/system_background")
}
}
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
Expand Down
10 changes: 0 additions & 10 deletions Sources/FlareUI/Classes/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,6 @@ internal enum L10n {
internal static func price(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "product.subscription.price", String(describing: p1), String(describing: p2), fallback: "%@/%@")
}
internal enum Duration {
/// Day
internal static let day = L10n.tr("Localizable", "product.subscription.duration.day", fallback: "Day")
/// Month
internal static let month = L10n.tr("Localizable", "product.subscription.duration.month", fallback: "Month")
/// Week
internal static let week = L10n.tr("Localizable", "product.subscription.duration.week", fallback: "Week")
/// Year
internal static let year = L10n.tr("Localizable", "product.subscription.duration.year", fallback: "Year")
}
}
}
internal enum StoreButton {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ enum Palette {
Color(Asset.Colors.dynamicBackground.color)
}

static var systemBackground: Color {
Color(Asset.Colors.systemBackground.color)
}

static var systemGray: Color {
#if os(macOS)
Color(NSColor.systemGray)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// Flare
// Copyright © 2024 Space Code. All rights reserved.
//

import Flare
import SwiftUI

public extension View {
func onInAppPurchaseCompletion(completion: ((StoreProduct, Result<Void, Error>) -> Void)?) -> some View {
environment(\.purchaseCompletion, completion)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// Flare
// Copyright © 2024 Space Code. All rights reserved.
//

import Flare
import StoreKit
import SwiftUI

public extension View {
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func inAppPurchaseOptions(_ options: ((StoreProduct) -> Set<Product.PurchaseOption>)?) -> some View {
environment(\.purchaseOptions) { PurchaseOptions(options: options?($0) ?? []) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ struct PaywallViewModifier: ViewModifier {
/// - paywallType: The paywall type.
/// - presented: The binding to control the presentation state of the paywall.
/// - presentationAssembly: The presentation assembly.
init(paywallType: PaywallType, presented: Binding<Bool>, presentationAssembly: IPresentationAssembly) {
init(
paywallType: PaywallType,
presented: Binding<Bool>,
presentationAssembly: IPresentationAssembly
) {
self.paywallType = paywallType
self.presented = presented
self.presentationAssembly = presentationAssembly
Expand All @@ -35,11 +39,14 @@ struct PaywallViewModifier: ViewModifier {
func body(content: Content) -> some View {
content
.sheet(isPresented: presented) {
PaywallView(
paywallType: paywallType,
productsAssembly: presentationAssembly.productsViewAssembly,
subscriptionsAssembly: presentationAssembly.subscritpionsViewAssembly
)
ZStack {
Palette.systemBackground.edgesIgnoringSafeArea(.all)
PaywallView(
paywallType: paywallType,
productsAssembly: presentationAssembly.productsViewAssembly,
subscriptionsAssembly: presentationAssembly.subscritpionsViewAssembly
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ struct PaywallView: View {

// MARK: Initialization

init(paywallType: PaywallType, productsAssembly: IProductsViewAssembly, subscriptionsAssembly: ISubscriptionAssembly) {
init(
paywallType: PaywallType,
productsAssembly: IProductsViewAssembly,
subscriptionsAssembly: ISubscriptionAssembly
) {
self.paywallType = paywallType
self.productsAssembly = productsAssembly
self.subscriptionsAssembly = subscriptionsAssembly
Expand All @@ -25,11 +29,15 @@ struct PaywallView: View {
// MARK: View

var body: some View {
switch paywallType {
case let .subscriptions(type):
return AnyView(subscriptionsAssembly.assembly(type: type))
case let .products(productIDs):
return AnyView(productsAssembly.assemble(ids: productIDs))
VStack(alignment: .leading) {
Group {
switch paywallType {
case let .subscriptions(type):
AnyView(subscriptionsAssembly.assembly(type: type))
case let .products(productIDs):
AnyView(productsAssembly.assemble(ids: productIDs))
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@

import Flare
import Foundation
import StoreKit
import SwiftUI

// MARK: - IProductPresenter

protocol IProductPresenter {
func viewDidLoad()
func purchase() async throws
func purchase(options: PurchaseOptions?) async throws -> Bool
}

// MARK: - ProductPresenter
Expand Down Expand Up @@ -58,8 +59,8 @@ extension ProductPresenter: IProductPresenter {
}

@MainActor
func purchase() async throws {
guard !isInProgress else { return }
func purchase(options: PurchaseOptions?) async throws -> Bool {
guard !isInProgress else { return false }

defer { isInProgress = false }
isInProgress = true
Expand All @@ -69,12 +70,23 @@ extension ProductPresenter: IProductPresenter {
}

do {
let transaction = try await iap.purchase(product: product)
let transaction = try await purchase(product: product, options: options)
// TODO: Don't finish transaction
await iap.finish(transaction: transaction)
return true
} catch {
if let error = error as? IAPError, error != .paymentCancelled {
throw error
}
return false
}
}

private func purchase(product: StoreProduct, options: PurchaseOptions?) async throws -> StoreTransaction {
if #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *), let options = options?.options {
return try await iap.purchase(product: product, options: options)
} else {
return try await iap.purchase(product: product)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// Copyright © 2024 Space Code. All rights reserved.
//

import Flare
import StoreKit
import SwiftUI

// MARK: - ProductView
Expand All @@ -11,6 +13,9 @@ struct ProductView: View, IViewWrapper {
// MARK: Properties

@Environment(\.productViewStyle) var productViewStyle
@Environment(\.purchaseCompletion) var purchaseCompletion
@Environment(\.purchaseOptions) var purchaseOptions

@State private var error: Error?

private let viewModel: ProductViewModel
Expand Down Expand Up @@ -49,11 +54,19 @@ struct ProductView: View, IViewWrapper {
}

private func purchase() {
guard case let .product(storeProduct) = viewModel.state else { return }

Task { @MainActor in
do {
try await viewModel.presenter.purchase()
let options = purchaseOptions?(storeProduct)
let result = try await viewModel.presenter.purchase(options: options)

if result {
purchaseCompletion?(storeProduct, .success(()))
}
} catch {
self.error = error
purchaseCompletion?(storeProduct, .failure(error))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ struct ProductsView: View, IViewWrapper {
ForEach(Array(products), id: \.self) { product in
viewModel.productAssembly.assemble(storeProduct: product)
}
.padding()
.padding(.horizontal)
Spacer()
storeButtonView
}
case .error:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ final class StoreButtonPresenter: IPresenter {
extension StoreButtonPresenter: IStoreButtonPresenter {
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func restore() async throws {
try await iap.restore()
do {
try await iap.restore()
} catch {
if let error = error as? IAPError, error != .paymentCancelled {
throw error
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "255",
"green" : "255",
"red" : "255"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0",
"green" : "0",
"red" : "0"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 13 additions & 1 deletion Tests/FlareUITests/UnitTests/Mocks/ProductPresenterMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,17 @@ import Foundation
final class ProductPresenterMock: IProductPresenter {
func viewDidLoad() {}

func purchase() async throws {}
var invokedPurchase = false
var invokedPurchaseCount = 0
var invokedPurchaseParameters: (options: PurchaseOptions?, Void)?
var invokedPurchaseParametersList = [(options: PurchaseOptions?, Void)]()
var stubbedPurchase: Bool = true

func purchase(options: PurchaseOptions?) async throws -> Bool {
invokedPurchase = true
invokedPurchaseCount += 1
invokedPurchaseParameters = (options, ())
invokedPurchaseParametersList.append((options, ()))
return stubbedPurchase
}
}
Loading

0 comments on commit d38ffbf

Please sign in to comment.