(Optional) Configuration for this paywall presentation
struct PaywallPresentationConfig { // View controller to present from. Defaults to current top view controller var presentFromViewController: UIViewController? = nil // Custom traits to send to the paywall var customPaywallTraits: HeliumUserTraits? = nil // Don't show paywall if user is entitled to a product in paywall var dontShowIfAlreadyEntitled: Bool = false // How long to allow loading state before switching to fallback logic. // Use zero or negative value to disable loading state. var loadingBudget: TimeInterval = DEFAULT_LOADING_BUDGET}
Handle any scenario where the paywall does not show. If user is already entitled and config.dontShowIfAlreadyEntitled is true, onPaywallNotShown(.alreadyEntitled) will be called only if onEntitled is not provided.
You should now be able to see Helium paywalls in your app! Well done! 🎉
Looking for alternative presentation methods? Check out the guide on Ways to Show a Paywall.
Identifying users is optional but can help with targeting and when forwarding events to external analytics platforms. If you are not sure, you probably do not need to identify your users.
Identify users as early as you can to maximize consistency in metrics and targeting. Ideally right before you call Helium.shared.initialize!
Set a custom user ID
Helium.identify.userId = "custom-user-id"
Set custom user traits for targeting and analytics visibility
Helium.identify.setUserTraits(HeliumUserTraits(["hasOnboarded": true]))// or Helium.identify.addUserTraits() if you don't want to clear existing traits
appAccountToken
If you use an appAccountToken for your existing purchases, then we recommend you also share this value with Helium so Helium can apply the appAccountToken to paywall purchases.
if let appAccountTokenUUID = UUID(uuidString: "app-account-token-uuid") { Helium.identify.appAccountToken = appAccountTokenUUID}
Helium dispatches various events during paywall presentation and purchase flow. You can optionally handle these events in your mobile app. You can also configure Helium to forward them to your existing analytics provider.
When displaying a paywall you can pass in event handlers to listen for select events:
Helium.shared.presentPaywall( trigger: "post_onboarding", eventHandlers: PaywallEventHandlers() .onOpen { event in print("open via trigger \(event.triggerName)") } .onClose { event in print("close for trigger \(event.triggerName)") } .onDismissed { event in print("dismiss for trigger \(event.triggerName)") } .onPurchaseSucceeded { event in print("purchase succeeded for trigger \(event.triggerName)") } .onCustomPaywallAction { event in print("Custom action: \(event.actionName) with params: \(event.params)") } .onAnyEvent { event in // A handler for all paywall-related events. // Note that if you have other handlers (i.e. onOpen) set up, // both that handler AND this one will fire during paywall open. }) { paywallNotShownReason in // handle paywall not shown}
You can also add one or more global event listeners. For example:
/// Implement this where you want to handle eventspublic protocol HeliumEventListener : AnyObject { func onHeliumEvent(event: HeliumEvent)}/// Add a listener for all Helium events.public func addHeliumEventListener(_ listener: HeliumEventListener)/// Remove a specific Helium event listener.public func removeHeliumEventListener(_ listener: HeliumEventListener)
Listeners are held weakly to prevent memory leaks. If you don’t maintain a strong reference to your listener, it will be deallocated immediately and no events will fire.
// ❌ Wrong - listener is deallocated immediately, no events will fireHelium.shared.addHeliumEventListener(MyListener())// ✅ Works - singleton keeps a strong referenceclass MyHeliumEventListener: HeliumEventListener { static let shared = MyHeliumEventListener() func onHeliumEvent(event: any HeliumEvent) { print("Helium event: \(event.toDictionary())") }}// And make sure to register it:Helium.shared.addHeliumEventListener(MyHeliumEventListener.shared)
Check entitlements before showing paywalls to avoid showing a paywall to a user who should not see it.
Helium.shared.presentPaywall( trigger: "my_paywall_trigger", config: PaywallPresentationConfig( dontShowIfAlreadyEntitled: true ), onEntitled: { // handle user with existing entitlement // or new entitlement after they completed/restored a purchase }) { paywallNotShownReason in // handle paywall not shown}
List of All Entitlement Helper Methods
hasAny() Checks if the user has purchased any subscription or non-consumable product.hasAnyActiveSubscription() Checks if the user has any active subscription.hasEntitlementForPaywall(trigger: String, considerAssociatedSubscriptions: Bool = false) Checks if the user has entitlements for any product in a specific paywall. Returns nil if paywall configuration hasn’t been downloaded yet.hasActiveEntitlementFor(productId: String) Checks if the user has entitlement to a specific product.hasActiveSubscriptionFor(productId: String) Checks if the user has an active subscription for a specific product.hasActiveSubscriptionFor(subscriptionGroupID: String) Checks if the user has an active subscription in a specific subscription group.purchasedProductIds() Retrieves a list of all product IDs the user currently has access to.activeSubscriptions() Returns detailed information about all active auto-renewing subscriptions.subscriptionStatusFor(productId: String) Gets detailed subscription status for a specific product, including state information like subscribed, expired, or in grace period.subscriptionStatusFor(subscriptionGroupID: String) Gets detailed subscription status for a specific subscription group.
Add the HeliumRevenueCat product to your app’s main target.
The HeliumRevenueCat package includes purchases-ios-spm as a dependency, notpurchases-ios and you may encounter build issues if you are using purchases-ios with SPM. (We recommend just switching to purchases-ios-spm).
Simply use Helium’s pre-built RevenueCatDelegate to let RevenueCat handle paywall purchases.
import HeliumRevenueCat // unless using Cocoapod then can just import HeliumHelium.config.purchaseDelegate = RevenueCatDelegate( // Optional - pass in to have Helium handle RevenueCat initialization. revenueCatApiKey: "<revenue-cat-api-id>")
If you do not supply revenueCatApiKey, make sure to initialize RevenueCat before creating the RevenueCatDelegate!
It is best to do this configuration before you call Helium.shared.initialize
By default, Helium will handle purchases for you! This section is for those who want to implement custom purchase logic.
Want to add some custom behavior but still use the built-in purchase logic? Just subclass StoreKitDelegate or RevenueCatDelegate! (Be sure to make a super call for any overridden methods.)
You can also create a custom delegate and implement your own purchase logic. You can look at our StoreKitDelegate and RevenueCatDelegate in the SDK for examples (also linked below).The HeliumPurchaseDelegate is defined as follows:
public protocol HeliumPurchaseDelegate: AnyObject { // Execute the purchase of a product given the product ID. func makePurchase(productId: String) async -> HeliumPaywallTransactionStatus // (Optional) - Restore any existing subscriptions. // Return a boolean indicating whether the restore was successful. func restorePurchases() async -> Bool // (Optional) - Called for all Helium events (e.g. PaywallOpenEvent) func onPaywallEvent(_ event: HeliumEvent)}
For the full public API and detailed parameter documentation, see the inline docstrings in the SDK source. Import Helium in your project and use your IDE’s autocomplete or jump-to-definition to explore all available methods and types.
Checking Download Status
In most cases there is no need to check download status. Helium will display a loading indication if a paywall is presented before download has completed.
You can check the status of the paywall configuration download using the Helium.shared.getDownloadStatus() method. This method returns a value of type HeliumFetchedConfigStatus, which is defined as follows:
public enum HeliumFetchedConfigStatus: String, Codable, Equatable { case notDownloadedYet case inProgress case downloadSuccess case downloadFailure}
You can also simply check if paywalls have been successfully downloaded with Helium.shared.paywallsLoaded().
Get Paywall Info By Trigger
Retrieve basic information about the paywall for a specific trigger with Helium.shared.getPaywallInfo(trigger: String) which returns:
public struct PaywallInfo { public let paywallTemplateName: String // shouldShow only false if the paywall should not be shown due to targeting or workflow configuration (Helium handles this for you in presentPaywall) public let shouldShow: Bool}
This method can be used if you want to be certain that a paywall is ready for display before attempting to display.
Hiding Paywalls Programmatically
You can programmatically hide paywalls using:
// Hide the current paywallHelium.shared.hidePaywall()// Hide all currently displayed paywallsHelium.shared.hideAllPaywalls()
Reset Helium
Reset Helium entirely so you can call initialize again, for example after changing user traits that can affect the paywalls a user might see via targeting.