Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,56 +46,53 @@
}
],
"settings" : {
"_askToBuyEnabled" : false,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to rever this file

"_billingGracePeriodEnabled" : false,
"_billingIssuesEnabled" : false,
"_disableDialogs" : false,
"_failTransactionsEnabled" : false,
"_locale" : "en_US",
"_renewalBillingIssuesEnabled" : false,
"_storefront" : "ESP",
"_storeKitErrors" : [
{
"current" : null,
"enabled" : false,
"name" : "Load Products"
},
{
"current" : null,
"enabled" : false,
"name" : "Purchase"
},
{
"current" : null,
"enabled" : false,
"name" : "Verification"
},
{
"current" : null,
"enabled" : false,
"name" : "App Store Sync"
},
{
"current" : null,
"enabled" : false,
"name" : "Subscription Status"
},
{
"current" : null,
"enabled" : false,
"name" : "App Transaction"
},
{
"current" : null,
"enabled" : false,
"name" : "Manage Subscriptions Sheet"
},
{
"current" : null,
"enabled" : false,
"name" : "Refund Request Sheet"
},
{
"current" : null,
"enabled" : false,
"name" : "Offer Code Redeem Sheet"
}
]
],
"_timeRate" : 0
},
"subscriptionGroups" : [
{
Expand Down Expand Up @@ -134,7 +131,7 @@
"locale" : "en_US"
}
],
"productID" : "maestro.weekly.tests.01",
"productID" : "com.revenuecat.monthly_4.99.1_week_intro",
"recurringSubscriptionPeriod" : "P1W",
"referenceName" : "Product 1 [Weekly]",
"subscriptionGroupID" : "F7DE1690",
Expand Down
7 changes: 7 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,8 @@
887A60772C1D037000E1A461 /* TemplateViewConfiguration+Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A5FD92C1D037000E1A461 /* TemplateViewConfiguration+Images.swift */; };
887A60792C1D037000E1A461 /* UserInterfaceIdiom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A5FDB2C1D037000E1A461 /* UserInterfaceIdiom.swift */; };
887A607A2C1D037000E1A461 /* Variables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A5FDC2C1D037000E1A461 /* Variables.swift */; };
57B1D7B22F2A1F1800A77E21 /* PaywallSourceEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57B1D7B12F2A1F1800A77E21 /* PaywallSourceEnvironment.swift */; };
57B1D7B42F2A213800A77E21 /* PaywallSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57B1D7B32F2A213800A77E21 /* PaywallSource.swift */; };
887A607B2C1D037000E1A461 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A5FDE2C1D037000E1A461 /* Bundle+Extensions.swift */; };
887A607C2C1D037000E1A461 /* ColorInformation+MultiScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A5FDF2C1D037000E1A461 /* ColorInformation+MultiScheme.swift */; };
887A607D2C1D037000E1A461 /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887A5FE02C1D037000E1A461 /* ImageLoader.swift */; };
Expand Down Expand Up @@ -2441,6 +2443,8 @@
887A5FD92C1D037000E1A461 /* TemplateViewConfiguration+Images.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TemplateViewConfiguration+Images.swift"; sourceTree = "<group>"; };
887A5FDA2C1D037000E1A461 /* TestData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestData.swift; sourceTree = "<group>"; };
887A5FDB2C1D037000E1A461 /* UserInterfaceIdiom.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInterfaceIdiom.swift; sourceTree = "<group>"; };
57B1D7B12F2A1F1800A77E21 /* PaywallSourceEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallSourceEnvironment.swift; sourceTree = "<group>"; };
57B1D7B32F2A213800A77E21 /* PaywallSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallSource.swift; sourceTree = "<group>"; };
887A5FDC2C1D037000E1A461 /* Variables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Variables.swift; sourceTree = "<group>"; };
887A5FDE2C1D037000E1A461 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = "<group>"; };
887A5FDF2C1D037000E1A461 /* ColorInformation+MultiScheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ColorInformation+MultiScheme.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -5203,8 +5207,10 @@
887A5FD72C1D037000E1A461 /* TemplateViewConfiguration.swift */,
887A5FD82C1D037000E1A461 /* TemplateViewConfiguration+Extensions.swift */,
887A5FD92C1D037000E1A461 /* TemplateViewConfiguration+Images.swift */,
57B1D7B32F2A213800A77E21 /* PaywallSource.swift */,
887A5FDA2C1D037000E1A461 /* TestData.swift */,
887A5FDB2C1D037000E1A461 /* UserInterfaceIdiom.swift */,
57B1D7B12F2A1F1800A77E21 /* PaywallSourceEnvironment.swift */,
887A5FDC2C1D037000E1A461 /* Variables.swift */,
);
path = Data;
Expand Down Expand Up @@ -7481,6 +7487,7 @@
887A608B2C1D037000E1A461 /* PurchaseHandler+TestData.swift in Sources */,
887A60682C1D037000E1A461 /* TemplateError.swift in Sources */,
887A60792C1D037000E1A461 /* UserInterfaceIdiom.swift in Sources */,
57B1D7B22F2A1F1800A77E21 /* PaywallSourceEnvironment.swift in Sources */,
03C06FCC2D479C7C00600693 /* CarouselComponentViewModel.swift in Sources */,
887A60742C1D037000E1A461 /* Strings.swift in Sources */,
887A60712C1D037000E1A461 /* PaywallViewConfiguration.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ struct NoSubscriptionsCardView: View {
performRestore: viewModel.performRestore)
)
)
.paywallSource(.customerCenter)
})
.onAppear {
viewModel.refreshOffering()
Expand Down
35 changes: 35 additions & 0 deletions RevenueCatUI/Data/PaywallSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// swiftlint:disable missing_docs
//
// PaywallSource.swift
//
//
// Created by RevenueCat on 2/26/25.
//

/// Identifies where a paywall was presented from so that backend analytics can classify the event.
@_spi(Internal) public struct PaywallSource: RawRepresentable, Hashable, Sendable, ExpressibleByStringLiteral {

/// Raw backend identifier describing the source.
public let rawValue: String

public typealias StringLiteralType = String

/// Creates a typed paywall source from a raw backend identifier.
public init(rawValue: String) {
self.rawValue = rawValue
}

/// Allows initializing from string literals (e.g. `PaywallSource("foo")`).
public init(stringLiteral value: StringLiteralType) {
self.init(rawValue: value)
}

}

@_spi(Internal) public extension PaywallSource {

/// Source identifier used when the paywall originated from Customer Center.
static let customerCenter: PaywallSource = "presented_from_customer_center"

}
// swiftlint:enable missing_docs
43 changes: 43 additions & 0 deletions RevenueCatUI/Data/PaywallSourceEnvironment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// PaywallSourceEnvironment.swift
//
//
// Created by RevenueCat on 2/26/25.
//

#if canImport(SwiftUI)

import SwiftUI

#if !os(tvOS)

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
private struct PaywallSourceKey: EnvironmentKey {

static let defaultValue: PaywallSource? = nil

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension EnvironmentValues {

/// The optional paywall source available in the current environment.
@_spi(Internal) public var paywallSource: PaywallSource? {
get { self[PaywallSourceKey.self] }
set { self[PaywallSourceKey.self] = newValue }
}
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension View {

/// Associates the provided paywall source with this view hierarchy so downstream paywall presentations can read it.
@_spi(Internal) public func paywallSource(_ source: PaywallSource?) -> some View {
self.environment(\.paywallSource, source)
}
Comment on lines +35 to +37
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This felt cleaner than adding a parameter to the view


}

#endif

#endif
15 changes: 13 additions & 2 deletions RevenueCatUI/PaywallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ public struct PaywallView: View {
@Environment(\.dismiss)
private var dismiss

@Environment(\.paywallSource)
private var paywallSource

/// Create a view to display the paywall in `Offerings.current`.
///
/// - Parameter fonts: An optional ``PaywallFontProvider``.
Expand Down Expand Up @@ -505,6 +508,9 @@ struct LoadedOfferingPaywallView: View {
@Environment(\.dismiss)
private var dismiss

@Environment(\.paywallSource)
private var paywallSource

init(
offering: Offering,
activelySubscribedProductIdentifiers: Set<String>,
Expand Down Expand Up @@ -570,8 +576,13 @@ struct LoadedOfferingPaywallView: View {
.environmentObject(self.introEligibility)
.environmentObject(self.purchaseHandler)
.disabled(self.purchaseHandler.actionInProgress)
.onAppear { self.purchaseHandler.trackPaywallImpression(self.createEventData()) }
.onDisappear { self.purchaseHandler.trackPaywallClose() }
.onAppear {
self.purchaseHandler.trackPaywallImpression(self.createEventData(),
source: self.paywallSource)
}
.onDisappear {
self.purchaseHandler.trackPaywallClose()
}
.onChangeOf(self.purchaseHandler.purchased) { purchased in
if purchased {
guard let onRequestedDismissal = self.onRequestedDismissal else {
Expand Down
82 changes: 63 additions & 19 deletions RevenueCatUI/Purchasing/MockPurchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ final class MockPurchases: PaywallPurchasesType {
return try await self.restoreBlock()
}

func track(paywallEvent: PaywallEvent) async {
await self.trackEventBlock(paywallEvent)
func track(paywallEvent: PaywallEvent, source: PaywallSource?) async {
let finalSource = source?.rawValue ?? paywallEvent.data.source
await self.trackEventBlock(paywallEvent.overridingSource(finalSource))
}

#if !ENABLE_CUSTOM_ENTITLEMENT_COMPUTATION
Expand All @@ -101,29 +102,72 @@ extension PaywallPurchasesType {
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)
} customerInfo: {
try await self.customerInfo()
}
return MockPurchases(
purchase: { package in
try await purchase(self.purchase(package:))(package)
},
restorePurchases: {
try await restore(self.restorePurchases)()
},
trackEvent: { event in
await self.track(
paywallEvent: event,
source: event.data.source.map { PaywallSource(rawValue: $0) }
)
},
customerInfo: {
try await self.customerInfo()
}
)
}

/// 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)
} customerInfo: {
try await self.customerInfo()
let trackBlock: MockPurchases.TrackEventBlock = { event in
await self.track(paywallEvent: event,
source: event.data.source.map { PaywallSource(rawValue: $0) })
}

return MockPurchases(
purchase: { package in
try await self.purchase(package: package)
},
restorePurchases: {
try await self.restorePurchases()
},
trackEvent: { event in
await trackEvent(trackBlock)(event)
},
customerInfo: {
try await self.customerInfo()
}
)
}

}

private extension PaywallEvent {

func overridingSource(_ source: String?) -> PaywallEvent {
guard let source else { return self }

switch self {
case let .impression(creationData, data):
var updated = data
updated.source = source
return .impression(creationData, updated)

case let .close(creationData, data):
var updated = data
updated.source = source
return .close(creationData, updated)

case let .cancel(creationData, data):
var updated = data
updated.source = source
return .cancel(creationData, updated)
}
}

Expand Down
12 changes: 10 additions & 2 deletions RevenueCatUI/Purchasing/PaywallPurchasesType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ protocol PaywallPurchasesType: Sendable {
func customerInfo() async throws -> CustomerInfo

@Sendable
func track(paywallEvent: PaywallEvent) async
func track(paywallEvent: PaywallEvent, source: PaywallSource?) async

#if !ENABLE_CUSTOM_ENTITLEMENT_COMPUTATION
func invalidateCustomerInfoCache()
Expand All @@ -57,4 +57,12 @@ protocol PaywallPurchasesType: Sendable {

}

extension Purchases: PaywallPurchasesType {}
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension Purchases: PaywallPurchasesType {

@Sendable
func track(paywallEvent: PaywallEvent, source: PaywallSource?) async {
await self.track(paywallEvent: paywallEvent, source: source?.rawValue)
}

}
Loading