* WIP * Merge branch 'main' into default-tuist-to-local-swift-package * Cleanup * Merge branch 'main' into default-tuist-to-local-swift-package * Fix error with RevenueCatUI * Removes project v3LoadShedderIntegration from tuist integration * Add both CI.xcconfig and Local.xcconfig if they are both available * Remove unnecessary package * Remove purchases-ios as s tuist dependency It's already resolved in Tuist/ProjectDescriptionHelpers/ProjectDescriptionPackages.swift * New tuist project "RevenueCatTests" containing all test targets * Merge branch 'main' into default-tuist-to-local-swift-package * Fix Tuist dependency mode issues and clean up project structure - Use exhaustive switch in ProjectDescriptionPackages instead of if/else if - Extract shared allDestinations/allDeploymentTargets into ProjectDescriptionHelpers (DRY) - Fix duplicate ReceiptParser scheme across RevenueCat and RevenueCatTests projects - Remove stale cross-project test target references from schemes - Fix bundleId typo in BackendCustomEntitlementsIntegrationTests - Restore purchases-ios in Tuist/Package.swift for remoteXcodeProject mode - Make RevenueCat/RevenueCatUI workspace inclusion conditional on dependency mode - Fix BinarySizeTest to use shared dependency helpers instead of removed APIs - Fix misleading doc comments on internal dependency helpers Co-authored-by: Cursor <cursoragent@cursor.com> * Remove and gitignore file * Fix Bundle.module ambiguity in RevenueCatUITests and add explicit scheme Use Bundle.revenueCatUI instead of Bundle.module to resolve ambiguity when both RevenueCat and RevenueCatUI are imported via @testable in SPM modes (each module synthesises its own internal Bundle.module). Add explicit RevenueCatUITests scheme to RevenueCatTests project for proper scheme generation during full tuist generate. Made-with: Cursor * Fix BinarySizeTest localSource deps and remove dead code Revert BinarySizeTest localSource to explicit .project(target:) references — it manages its own integration modes independently of DependencyMode, and the switch to .revenueCat/.revenueCatUI broke it in the default localSwiftPackage mode (no SPM package declared). Remove unused allDeploymentTargets constant from RevenueCatUI project. Made-with: Cursor * Add priority flush with rate limiting and queuing for feature events Priority events (paywall impressions, custom paywall events) now trigger an immediate flush to the backend. A rate limiter (5 calls/60s) prevents excessive network requests from rapid tracking calls. When a priority event arrives during an in-progress flush, it is queued and drained after the current flush completes. * Extract EventsManagerStrings to separate file to fix file length lint * Fix integration tests: use non-priority events for flush tests The `.impression` event now triggers a priority flush, which can consume the event before the test's explicit `flushPaywallEvents` call. Replace with `.cancel` (non-priority) since these tests verify flush mechanics, not event-type behavior. * Fix duplicate RevenueCat targets in localSwiftPackage mode Exclude RevenueCat/RevenueCatUI Tuist projects from the workspace when using localSwiftPackage dependency mode, since the local SPM package already provides these targets. Including both caused "Multiple commands produce" build errors (e.g. in the BinarySizeTest CI job). Also update BinarySizeTest's localSource integration to use SPM package dependencies instead of Tuist project target references, consistent with the workspace no longer including those projects in SPM mode. Made-with: Cursor * Move rate limiter check to drain phase to prevent queued events from exhausting capacity When multiple priority events arrived during an in-progress flush, each one called shouldProceed() on the rate limiter but they all coalesced into a single pending flush. This wasted rate limiter capacity without performing actual flushes. Now the rate limiter is only checked when a flush actually starts in startPendingPriorityFlushIfNeeded(). * Add missing packages declaration to RCTTester project RCTTester uses .revenueCat and .revenueCatUI dependencies which resolve to SPM package products in localSwiftPackage mode, but was missing the packages: .projectPackages declaration needed to provide the package source. Made-with: Cursor * Fix Bundle.module resolution for RevenueCatUI snapshot tests Use #if SWIFT_PACKAGE to select the correct resource bundle in test code: - SPM builds (CI): Bundle.module resolves to the test target's bundle, which contains the test images (header.heic, background.heic) - Tuist builds: Bundle.revenueCatUI resolves to RevenueCatUI's bundle, avoiding ambiguity between multiple Bundle.module definitions Made-with: Cursor * Remove internal jwsRepresentation from ObjC API tester jwsRepresentation is @objc internal on StoreTransaction — it is not part of the public API (absent from .swiftinterface files). Previously this compiled because Tuist framework targets expose internal @objc members in the generated -Swift.h header, but SPM packages only expose public members. Made-with: Cursor * Revert "Merge remote-tracking branch 'origin/rickvdl/priority-flush-events' into default-tuist-to-local-swift-package" This reverts commitaa585aa9c2, reversing changes made to7d10a496ad. * Restore BinarySizeTest localSource to use Xcode project targets localSource now uses .project(target:path:) like on main, with TUIST_RC_XCODE_PROJECT=true set in CI so the workspace includes the RevenueCat/RevenueCatUI projects. spm uses local SPM package refs. Added documentation explaining why BinarySizeTest manages its own dependency integration independently of Environment.dependencyMode. Made-with: Cursor --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Facundo Menzella <facumenzella@users.noreply.github.com> Co-authored-by: Rick van der Linden <rick.vanderlinden@revenuecat.com>
Custom Entitlements Computation Example App
This app is useful for testing RevenueCat under Custom Entitlements Computation mode and understanding how it works.
What is Custom Entitlements Computation mode?
This is a special behavior mode for RevenueCat SDK, is intended for apps that will do their own entitlement computation separate from RevenueCat.
Apps using this mode rely on webhooks to signal their backends to refresh entitlements with RevenueCat.
In this mode, RevenueCat will not generate anonymous user IDs, it will not refresh customerInfo cache automatically only when a purchase goes through and it will disallow all methods other than those for configuration, switching users, getting offerings and making purchases.
When in this mode, the app should use switchUser(to:) to switch to a different App User ID if needed. The SDK should only be configured once the initial appUserID is known.
Using the app
To use the app, you should do the following:
- Configure your app in the RevenueCat dashboard. No special configuration is needed, but you should contact RevenueCat support before enabling this mode to ensure that it's the right one for your app. It's highly recommended to set Transfer Behavior to "Keep with original App User ID" in the RevenueCat Dashboard.
- Update the API key in Constants.swift and remove the #error line. You can update the default
appUserIDthere too, although apps in this mode should always be calling configure only when the appUserID is already known. - Update the bundle ID to match your RevenueCat app configuration.
- Have at least one Offering with at least one Package configured for iOS, since this is the one that the purchase button will use.
Once configured correctly, the app will allow you to log in with different users, and will show a list of all the times CustomerInfoAsyncStream fired, as well as the values for each one.
To use this mode, ensure that you install the RevenueCat_CustomEntitlementComputation SDK in Swift Package Manager.
Happy testing!
Using Custom Entitlements mode
Installation:
Install the SDK through Swift Package Manager.
Select File » Add Packages... and enter the repository URL of the https://github.com/RevenueCat/purchases-ios.git into the search bar (top right). Set the Dependency Rule to Up to next major, and the version number to 4.18.0 < 5.0.0.
Check RevenueCat_CustomEntitlementComputation when a prompt for "Choose Package Products for purchases-ios" appears. Finally, choose the target where you want to use it.
The library should have been added to the Swift Package Dependencies section and you should be able to import it now.
Configuration:
The SDK should be configured once the user has already logged in. To configure, call:
Purchases.configureInCustomEntitlementsComputationMode(apiKey: "your_api_key", appUserID: appUserID)
Getting Offerings:
Call getOfferings through either the Async / Await or completion blocks alternatives:
let offerings = try await Purchases.shared.offerings()
Purchases.shared.getOfferings { (offerings, error) in
// code to handle here
}
Switching users:
To switch to a different user, call:
Purchases.shared.switchUser(to: appUserID)
This will ensure that all purchases made from this point on are posted for the new appUserID. After calling this method, you might need to call your backend to refresh entitlements for the new appUserID if they haven't been refreshed already.
Making purchases:
Call purchase(package:) through either the Async / Await or completion blocks alternatives:
do {
let (transaction, customerInfo, _) = try await Purchases.shared.purchase(package: package)
print(
"""
Purchase finished:
Transaction: \(transaction.debugDescription)
CustomerInfo: \(customerInfo.debugDescription)
"""
)
} catch ErrorCode.receiptAlreadyInUseError {
print("The receipt is already in use by another subscriber. " +
"Log in with the previous account or contact support to get your purchases transferred to " +
"regain access")
} catch ErrorCode.paymentPendingError {
print("The purchase is pending and may be completed at a later time." +
"This can happen when awaiting parental approval or going through extra authentication flows " +
"for credit cards in some countries.")
} catch ErrorCode.purchaseCancelledError {
print("Purchase was cancelled by the user.")
} catch {
print("FAILED TO PURCHASE: \(error.localizedDescription)")
}
Purchases.shared.purchase(package: package) { (transaction, customerInfo, error, userCancelled) in
if let error = error as? ErrorCode {
switch error {
case .receiptAlreadyInUseError:
print("The receipt is already in use by another subscriber. " +
"Log in with the previous account or contact support to get your purchases transferred to " +
"regain access")
case .paymentPendingError:
print("The purchase is pending and may be completed at a later time." +
"This can happen when awaiting parental approval or going through extra authentication flows " +
"for credit cards in some countries.")
case .purchaseCancelledError:
print("Purchase was cancelled by the user.")
default:
print("FAILED TO PURCHASE: \(error.localizedDescription)")
}
return
} else if let error = error {
print("FAILED TO PURCHASE: \(error.localizedDescription)")
return
}
print(
"""
Purchase finished:
Transaction: \(transaction.debugDescription)
CustomerInfo: \(customerInfo.debugDescription)
"""
)
}
Observing changes to purchases:
To ensure that your app reacts to changes to subscriptions in real time, you can use customerInfoStream. This stream will only fire when new customerInfo is registered
in RevenueCat, like when a subscription is renewed. If there are no changes from the last value, it will not fire. This means it's not guaranteed to fire on every app open.
for await customerInfo in Purchases.shared.customerInfoStream {
// code to handle here
}
