Skip to content

Commit

Permalink
Paywalls: refactored PurchaseHandler extracting protocol
Browse files Browse the repository at this point in the history
As suggested by @aboedo in #3164 (comment)
  • Loading branch information
NachoSoto committed Sep 14, 2023
1 parent dbc7710 commit 9e2113a
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 133 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// TrialOrIntroEligibilityChecker+TestData.swift
//
// Created by Nacho Soto on 9/12/23.

import Foundation
import RevenueCat

#if DEBUG

@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
extension TrialOrIntroEligibilityChecker {

/// Creates a mock `TrialOrIntroEligibilityChecker` with a constant result.
static func producing(eligibility: @autoclosure @escaping () -> IntroEligibilityStatus) -> Self {
return .init { packages in
return Dictionary(
uniqueKeysWithValues: Set(packages)
.map { package in
let result = package.storeProduct.hasIntroDiscount
? eligibility()
: .noIntroOfferExists

return (package, result)
}
)
}
}

/// Creates a copy of this `TrialOrIntroEligibilityChecker` with a delay.
func with(delay seconds: TimeInterval) -> Self {
return .init { [checker = self.checker] in
await Task.sleep(seconds: seconds)

return await checker($0)
}
}

}

#endif
83 changes: 0 additions & 83 deletions RevenueCatUI/Data/TestData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -519,80 +519,6 @@ internal enum TestData {
}
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
extension TrialOrIntroEligibilityChecker {

/// Creates a mock `TrialOrIntroEligibilityChecker` with a constant result.
static func producing(eligibility: @autoclosure @escaping () -> IntroEligibilityStatus) -> Self {
return .init { packages in
return Dictionary(
uniqueKeysWithValues: Set(packages)
.map { package in
let result = package.storeProduct.hasIntroDiscount
? eligibility()
: .noIntroOfferExists

return (package, result)
}
)
}
}

/// Creates a copy of this `TrialOrIntroEligibilityChecker` with a delay.
func with(delay seconds: TimeInterval) -> Self {
return .init { [checker = self.checker] in
await Task.sleep(seconds: seconds)

return await checker($0)
}
}

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
extension PurchaseHandler {

static func mock() -> Self {
return self.init { _ in
return (
transaction: nil,
customerInfo: TestData.customerInfo,
userCancelled: false
)
} restorePurchases: {
return TestData.customerInfo
} trackEvent: { event in
Logger.debug("Tracking event: \(event)")
}
}

static func cancelling() -> Self {
return .mock()
.map { block in {
var result = try await block($0)
result.userCancelled = true
return result
}
} restore: { $0 }
}

/// Creates a copy of this `PurchaseHandler` with a delay.
func with(delay seconds: TimeInterval) -> Self {
return self.map { purchaseBlock in {
await Task.sleep(seconds: seconds)

return try await purchaseBlock($0)
}
} restore: { restoreBlock in {
await Task.sleep(seconds: seconds)

return try await restoreBlock()
}
}
}

}

// MARK: -

extension PaywallColor: ExpressibleByStringLiteral {
Expand All @@ -614,13 +540,4 @@ extension PackageType {

}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
private extension Task where Success == Never, Failure == Never {

static func sleep(seconds: TimeInterval) async {
try? await Self.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
}

}

#endif
86 changes: 86 additions & 0 deletions RevenueCatUI/Purchasing/MockPurchases.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// MockPurchasesType.swift
//
// Created by Nacho Soto on 9/12/23.

import RevenueCat

#if DEBUG

/// An implementation of `PaywallPurchasesType` that allows creating custom blocks.
@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
final class MockPurchases: PaywallPurchasesType {

typealias PurchaseBlock = @Sendable (Package) async throws -> PurchaseResultData
typealias RestoreBlock = @Sendable () async throws -> CustomerInfo
typealias TrackEventBlock = @Sendable (PaywallEvent) async -> Void

private let purchaseBlock: PurchaseBlock
private let restoreBlock: RestoreBlock
private let trackEventBlock: TrackEventBlock

init(
purchase: @escaping PurchaseBlock,
restorePurchases: @escaping RestoreBlock,
trackEvent: @escaping TrackEventBlock
) {
self.purchaseBlock = purchase
self.restoreBlock = restorePurchases
self.trackEventBlock = trackEvent
}

func purchase(package: Package) async throws -> PurchaseResultData {
return try await self.purchaseBlock(package)
}

func restorePurchases() async throws -> CustomerInfo {
return try await self.restoreBlock()
}

func track(paywallEvent: PaywallEvent) async {
await self.trackEventBlock(paywallEvent)
}

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
extension PaywallPurchasesType {

/// Creates a copy of this `PaywallPurchasesType` wrapping `purchase` and `restore`.
func map(
purchase: @escaping (@escaping MockPurchases.PurchaseBlock) -> MockPurchases.PurchaseBlock,
restore: @escaping (@escaping MockPurchases.RestoreBlock) -> MockPurchases.RestoreBlock
) -> PaywallPurchasesType {
return MockPurchases { package in
try await purchase(self.purchase(package:))(package)
} restorePurchases: {
try await restore(self.restorePurchases)()
} trackEvent: { event in
await self.track(paywallEvent: event)
}
}

/// Creates a copy of this `PaywallPurchasesType` wrapping `trackEvent`.
func map(
trackEvent: @escaping (@escaping MockPurchases.TrackEventBlock) -> MockPurchases.TrackEventBlock
) -> PaywallPurchasesType {
return MockPurchases { package in
try await self.purchase(package: package)
} restorePurchases: {
try await self.restorePurchases()
} trackEvent: { event in
await trackEvent(self.track(paywallEvent:))(event)
}
}

}

#endif
31 changes: 31 additions & 0 deletions RevenueCatUI/Purchasing/PaywallPurchasesType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// PaywallPurchasesType.swift
//
// Created by Nacho Soto on 9/12/23.

import RevenueCat

/// A simplified protocol for the subset of `PurchasesType` needed for `RevenueCatUI`.
@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
protocol PaywallPurchasesType: Sendable {

@Sendable
func purchase(package: Package) async throws -> PurchaseResultData

@Sendable
func restorePurchases() async throws -> CustomerInfo

@Sendable
func track(paywallEvent: PaywallEvent) async

}

extension Purchases: PaywallPurchasesType {}
74 changes: 74 additions & 0 deletions RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// PurchaseHandler+TestData.swift
//
// Created by Nacho Soto on 9/12/23.

import Foundation
import RevenueCat

#if DEBUG

@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
extension PurchaseHandler {

static func mock() -> Self {
return self.init(
purchases: MockPurchases { _ in
return (
transaction: nil,
customerInfo: TestData.customerInfo,
userCancelled: false
)
} restorePurchases: {
return TestData.customerInfo
} trackEvent: { event in
Logger.debug("Tracking event: \(event)")
}
)
}

static func cancelling() -> Self {
return .mock()
.map { block in {
var result = try await block($0)
result.userCancelled = true
return result
}
} restore: { $0 }
}

/// Creates a copy of this `PurchaseHandler` with a delay.
func with(delay seconds: TimeInterval) -> Self {
return self.map { purchaseBlock in {
await Task.sleep(seconds: seconds)

return try await purchaseBlock($0)
}
} restore: { restoreBlock in {
await Task.sleep(seconds: seconds)

return try await restoreBlock()
}
}
}

}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension Task where Success == Never, Failure == Never {

static func sleep(seconds: TimeInterval) async {
try? await Self.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
}

}

#endif
Loading

0 comments on commit 9e2113a

Please sign in to comment.