Skip to content

Commit

Permalink
Paywalls: send events to Purchases
Browse files Browse the repository at this point in the history
  • Loading branch information
NachoSoto committed Sep 7, 2023
1 parent de2c8db commit 8ac7996
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 14 deletions.
5 changes: 5 additions & 0 deletions RevenueCatUI/Data/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ enum Strings {
case displaying_paywall
case not_displaying_paywall

case attempted_to_track_event_with_missing_data

}

extension Strings: CustomStringConvertible {
Expand All @@ -51,6 +53,9 @@ extension Strings: CustomStringConvertible {

case .not_displaying_paywall:
return "Condition not met: will not display paywall"

case .attempted_to_track_event_with_missing_data:
return "Attempted to track event with missing data"
}
}

Expand Down
21 changes: 12 additions & 9 deletions RevenueCatUI/Data/TestData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -547,19 +547,19 @@ extension PurchaseHandler {
)
} restorePurchases: {
return TestData.customerInfo
} trackEvent: { event in
Logger.debug("Tracking event: \(event)")
}
}

static func cancelling() -> Self {
return self.init { _ in
return (
transaction: nil,
customerInfo: TestData.customerInfo,
userCancelled: true
)
} restorePurchases: {
return TestData.customerInfo
}
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.
Expand All @@ -576,8 +576,11 @@ extension PurchaseHandler {
}
}
}

}

// MARK: -

extension PaywallColor: ExpressibleByStringLiteral {

/// Creates a `PaywallColor` with a string literal
Expand Down
49 changes: 49 additions & 0 deletions RevenueCatUI/PaywallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ struct LoadedOfferingPaywallView: View {
private let mode: PaywallViewMode
private let fonts: PaywallFontProvider

@State
private var session: (lastPaywall: DisplayedPaywall, id: PaywallEvent.SessionID)
@StateObject
private var introEligibility: IntroEligibilityViewModel
@ObservedObject
Expand All @@ -191,6 +193,9 @@ struct LoadedOfferingPaywallView: View {
@Environment(\.locale)
private var locale

@Environment(\.colorScheme)
private var colorScheme

init(
offering: Offering,
activelySubscribedProductIdentifiers: Set<String>,
Expand All @@ -211,6 +216,10 @@ struct LoadedOfferingPaywallView: View {
wrappedValue: .init(introEligibilityChecker: introEligibility)
)
self._purchaseHandler = .init(initialValue: purchaseHandler)
self._session = .init(initialValue: (
lastPaywall: .init(offering: offering, paywall: paywall),
id: .init()
))
}

var body: some View {
Expand All @@ -227,6 +236,8 @@ struct LoadedOfferingPaywallView: View {
.preference(key: PurchasedCustomerInfoPreferenceKey.self,
value: self.purchaseHandler.purchasedCustomerInfo)
.disabled(self.purchaseHandler.actionInProgress)
.onAppear { self.purchaseHandler.trackPaywallView(self.eventData) }
.onDisappear { self.purchaseHandler.trackPaywallClose() }

switch self.mode {
case .fullScreen:
Expand All @@ -239,6 +250,44 @@ struct LoadedOfferingPaywallView: View {
}
}

private var eventData: PaywallEvent.Data {
self.updateSessionIfNeeded()

return .init(
offering: self.offering,
paywall: self.paywall,
sessionID: self.session.id,
displayMode: self.mode,
locale: .current,
darkMode: self.colorScheme == .dark
)
}

private func updateSessionIfNeeded() {
let newPaywall: DisplayedPaywall = .init(offering: self.offering, paywall: self.paywall)
guard self.session.lastPaywall != newPaywall else { return }

self.session.lastPaywall = newPaywall
self.session.id = .init()
}

}

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

struct DisplayedPaywall: Equatable {
var offeringIdentifier: String
var paywallTemplate: String
var revision: Int

init(offering: Offering, paywall: PaywallData) {
self.offeringIdentifier = offering.identifier
self.paywallTemplate = paywall.templateName
self.revision = paywall.revision
}
}

}

// MARK: -
Expand Down
67 changes: 63 additions & 4 deletions RevenueCatUI/Purchasing/PurchaseHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ import RevenueCat
import StoreKit
import SwiftUI

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
final class PurchaseHandler: ObservableObject {

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

/// Whether a purchase or restore is currently in progress
@Published
Expand All @@ -40,20 +42,26 @@ final class PurchaseHandler: ObservableObject {
@Published
fileprivate(set) var restored: Bool = false

private var eventData: PaywallEvent.Data?

convenience init(purchases: Purchases = .shared) {
self.init { package in
return try await purchases.purchase(package: package)
} restorePurchases: {
return try await purchases.restorePurchases()
} trackEvent: { event in
await purchases.track(paywallEvent: event)
}
}

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

static func `default`() -> Self? {
Expand All @@ -74,7 +82,9 @@ extension PurchaseHandler {

let result = try await self.purchaseBlock(package)

if !result.userCancelled {
if result.userCancelled {
self.trackCancelledPurchase()
} else {
withAnimation(Constants.defaultAnimation) {
self.purchased = true
self.purchasedCustomerInfo = result.customerInfo
Expand All @@ -96,13 +106,62 @@ extension PurchaseHandler {
return result
}

func trackPaywallView(_ eventData: PaywallEvent.Data) {
self.eventData = eventData
self.trackEvent(PaywallEvent.view)
}

func trackPaywallClose() {
self.trackEvent(PaywallEvent.close)
}

fileprivate func trackCancelledPurchase() {
self.trackEvent(PaywallEvent.cancel)
}

}

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

/// Creates a copy of this `PurchaseHandler` wrapping the purchase and restore blocks.
func map(
purchase: @escaping (@escaping PurchaseBlock) -> PurchaseBlock,
restore: @escaping (@escaping RestoreBlock) -> RestoreBlock
) -> Self {
return .init(purchase: purchase(self.purchaseBlock),
restorePurchases: restore(self.restoreBlock))
restorePurchases: restore(self.restoreBlock),
trackEvent: self.trackEventBlock)
}

func map(
trackEvent: @escaping (@escaping TrackEventBlock) -> TrackEventBlock
) -> Self {
return .init(
purchase: self.purchaseBlock,
restorePurchases: self.restoreBlock,
trackEvent: trackEvent(self.trackEventBlock)
)
}

}

// MARK: - Private

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

func trackEvent(_ eventCreator: (PaywallEvent.Data) -> PaywallEvent) {
guard let data = self.eventData else {
Logger.warning(Strings.attempted_to_track_event_with_missing_data)
return
}

let event = eventCreator(data)

Task.detached(priority: .background) { [block = self.trackEventBlock] in
await block(event)
}
}

}
Expand Down
3 changes: 3 additions & 0 deletions RevenueCatUI/Views/LoadingPaywallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ private extension LoadingPaywallView {
},
restorePurchases: {
fatalError("Should not be able to purchase")
},
trackEvent: { _ in
// Ignoring events from loading paywall view
}
)

Expand Down
3 changes: 2 additions & 1 deletion Sources/Paywalls/Events/PaywallEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ extension PaywallEvent {
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension PaywallEvent {

var data: Data {
/// - Returns: The underlying ``PaywallEvent/Data-swift.struct``
public var data: Data {
switch self {
case let .view(data): return data
case let .cancel(data): return data
Expand Down
Loading

0 comments on commit 8ac7996

Please sign in to comment.