Native iOS and Android module that wraps the RevenueCat SDK and RevenueCatUI paywalls for Titanium apps.
Ti.RevenueCat brings RevenueCat's subscription and in-app purchase infrastructure to Titanium: offerings, purchases, restores, customer info, subscription management, and both modal and embeddable RevenueCatUI paywalls — all from JavaScript, backed by native Swift (iOS) and Kotlin (Android) modules that share the same JS API.
- Core Purchases API (offerings, purchase, restore, customer info, login/logout)
- Real-time customer info updates
- RevenueCatUI Paywall — modal presentation
- RevenueCatUI Paywall — embeddable view
- Subscription management (
showManageSubscriptions) - Android support
- Core Purchases API - Configure the SDK, fetch offerings, purchase packages, restore purchases, read customer info, and manage user identity (login/logout)
- Real-Time Customer Info Updates - A single event keeps your app in sync with entitlement changes, including renewals and expirations that happen in the background
- Paywall — Modal Presentation - Present RevenueCat's dashboard-configured paywall (or the automatic default template) as a full-screen modal with one call
- Paywall — Embeddable View - Drop the same paywall into any Titanium view hierarchy as a regular view, for custom layouts and flows
- Subscription Management - Send users straight to the native "Manage Subscription" sheet
Download the latest version from the releases page. The zip contains both the iOS and Android builds.
# Copy the compiled modules to:
{YOUR_PROJECT}/modules/iphone/
{YOUR_PROJECT}/modules/android/<modules>
<module platform="iphone">ti.revenuecat</module>
<module platform="android">ti.revenuecat</module>
</modules>
<ios>
<min-ios-ver>15.0</min-ios-ver>
</ios>Why iOS 15.0? RevenueCatUI (the paywall UI) is built on SwiftUI and requires iOS 15.0+. The core Purchases API alone only needs iOS 13.0+, but since this module bundles both, 15.0 is the effective minimum.
RevenueCat's Android paywall UI is built on Jetpack Compose. As of this writing, Titanium's module build system doesn't have first-class support for the Compose Compiler Gradle plugin, so two manual steps are required until that's resolved upstream. Skip this section if your app is iOS-only.
4a. Add a Gradle init script (once per development machine)
Create ~/.gradle/init.d/compose-plugin.gradle:
initscript {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
dependencies {
classpath 'org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:2.3.0'
}
}
allprojects {
buildscript {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
dependencies {
classpath 'org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:2.3.0'
}
}
}Without this, building any app that includes this module fails with Plugin with id 'org.jetbrains.kotlin.plugin.compose' not found. This has to live outside the module's own files — it's a Gradle-level fix that applies to every build on your machine, not something a module zip can carry. If your project uses a different Kotlin version, match it in the classpath line above.
4b. Also worth checking
Google Play may ask the user to confirm payment outside your app (e.g. a banking app). If your app's main Activity
launchModeisn'tstandardorsingleTop, the purchase can get cancelled when your app returns to the foreground. Check this intiapp.xmlbefore testing real purchases.
This module doesn't replace RevenueCat's own dashboard setup. Before calling configure(), you'll need, in the RevenueCat dashboard:
- An In-App Purchase Key uploaded for iOS (Apps → your iOS app → In-app Purchase Key Configuration) — required for the SDK to report transactions
- At least one Entitlement, with your store products (iOS and/or Android) attached to it
- At least one Offering with Packages pointing at those products, marked as Default
See RevenueCat's Configuring Products guide if you're setting this up for the first time.
import RevenueCatModule from 'ti.revenuecat';
RevenueCatModule.configure({ apiKey: 'appl_YOUR_PUBLIC_API_KEY' });
RevenueCatModule.getOfferings(result => {
if (!result.success || !result.current) return;
const offering = result.offerings[result.current];
const pkg = offering.packages[0];
RevenueCatModule.purchase({ packageIdentifier: pkg.identifier }, purchaseResult => {
if (purchaseResult.success) {
console.log('Purchased! Active entitlements:', JSON.stringify(purchaseResult.customerInfo.activeEntitlements));
}
});
});Configure the SDK, fetch products, and process purchases — everything needed for a bespoke purchase flow.
RevenueCatModule.configure({
apiKey: 'appl_YOUR_PUBLIC_API_KEY',
appUserID: 'optional-your-own-user-id' // omit to let RevenueCat generate an anonymous ID
});RevenueCatModule.getOfferings(result => {
if (!result.success) {
console.error(result.error);
return;
}
console.log('Current offering:', result.current);
console.log('All offerings:', JSON.stringify(result.offerings));
});RevenueCatModule.purchase({ packageIdentifier: '$rc_monthly' }, result => {
if (result.userCancelled) {
console.log('User cancelled the purchase');
return;
}
if (!result.success) {
console.error('Purchase failed:', result.error);
return;
}
console.log('Purchased! Customer info:', JSON.stringify(result.customerInfo));
});
packageIdentifiermust come from a package returned bygetOfferings()— the module caches packages from the lastgetOfferings()call so it can look them up by identifier.
RevenueCatModule.restorePurchases(result => {
if (result.success) {
console.log('Restored:', JSON.stringify(result.customerInfo));
}
});RevenueCatModule.getCustomerInfo(result => {
if (result.success) {
const isPremium = !!result.customerInfo.activeEntitlements['premium'];
console.log('Is premium:', isPremium);
}
});// Log in with your own user ID (e.g. after your app's own login flow)
RevenueCatModule.logIn('your-internal-user-id', result => {
console.log('Logged in, newly created:', result.created);
});
// Log out — reverts to a new anonymous user
RevenueCatModule.logOut(result => {
console.log('Logged out:', JSON.stringify(result.customerInfo));
});One listener keeps entitlement state in sync automatically — including renewals, expirations, refunds, and billing issues that happen while the app is open, without polling.
RevenueCatModule.addEventListener('customerInfoUpdated', e => {
const isPremium = !!e.customerInfo.activeEntitlements['premium'];
updateAppUI(isPremium);
});- Live subscription state - Reflect a lapsed renewal or refund immediately, without requiring the user to reopen the app
- Cross-device consistency - Pick up entitlement changes made on another device tied to the same user
- Post-migration sync - Confirm existing subscribers keep access automatically after adopting this module in an app that previously used raw StoreKit
Present RevenueCat's dashboard-configured paywall as a full-screen modal. If no custom paywall was designed in the dashboard for the offering, RevenueCatUI automatically falls back to a sensible default template — no extra configuration required to get started.
RevenueCatModule.presentPaywall({
// offeringIdentifier: 'default' // optional — omit to use the current offering
}, result => {
switch (result.event) {
case 'purchaseStarted':
console.log('Purchase started for', result.packageIdentifier);
break;
case 'purchased':
console.log('Purchased!', JSON.stringify(result.customerInfo));
break;
case 'restored':
console.log('Restored!', JSON.stringify(result.customerInfo));
break;
case 'dismissed':
console.log('Paywall dismissed');
break;
}
});The same paywall, embedded as a regular Titanium view — for cases where a full-screen modal doesn't fit your navigation flow (onboarding steps, a tab, an inline upsell card, etc).
const win = Ti.UI.createWindow({ backgroundColor: '#fff', extendSafeArea: false });
const paywallView = RevenueCatModule.createPaywallView({
// offeringIdentifier: 'default', // optional — omit to use the current offering
width: Ti.UI.FILL,
height: Ti.UI.FILL
});
[
'purchaseStarted', 'purchaseCompleted', 'purchaseCancelled', 'purchaseFailure',
'restoreStarted', 'restoreCompleted', 'restoreFailure', 'requestedDismissal'
].forEach(eventName => {
paywallView.addEventListener(eventName, e => {
console.log(eventName, JSON.stringify(e));
if (eventName === 'requestedDismissal') win.close();
});
});
win.add(paywallView);
win.open();Note: unlike the modal presentation, exit offers configured in the paywall builder don't apply to the embedded view — that's a RevenueCatUI limitation, not specific to this module.
Send the user straight to the native subscription management sheet (cancel, change plan, etc.), without leaving your app.
RevenueCatModule.showManageSubscriptions(result => {
if (!result.success) {
console.warn('Could not open subscription management:', result.error);
}
});If this fails or isn't available, fall back to opening
customerInfo.managementURL(included in everycustomerInfopayload) withTi.Platform.openURL().
Initializes the SDK. Must be called before any other method.
Parameters:
apiKey(String, required) - Your RevenueCat public API keyappUserID(String, optional) - Your own user identifier. Omit to use an anonymous RevenueCat-generated ID
Fetches all configured offerings and their packages.
Callback payload:
success(Boolean)error(String, on failure)offerings(Object) - keyed by offering identifiercurrent(String) - identifier of the offering marked "Default" in the dashboard
Purchases a package returned by a previous getOfferings() call.
Parameters:
packageIdentifier(String, required)
Callback payload:
success(Boolean)userCancelled(Boolean)error(String, on failure)customerInfo(Object, on success)
Restores previous purchases for the current App Store account.
Callback payload: success, error, customerInfo
Fetches the current customer's entitlement and purchase state.
Callback payload: success, error, customerInfo
Identifies the current user with your own ID, merging anonymous purchase history where possible.
Callback payload: success, error, created (Boolean), customerInfo
Logs out the current user, reverting to a new anonymous ID.
Callback payload: success, error, customerInfo
Opens the native subscription management sheet.
Callback payload: success, error
Presents the paywall for an offering as a full-screen modal.
Parameters:
offeringIdentifier(String, optional) - defaults to the current offering
Callback payload (callback is called once per event):
event(String) - one ofpurchaseStarted,purchased,restored,dismissedpackageIdentifier(String, onpurchaseStarted)customerInfo(Object, onpurchased/restored)
Creates an embeddable paywall view.
Parameters:
offeringIdentifier(String, optional) - defaults to the current offering- Any standard Titanium view layout property (
width,height,top, etc.)
Returns: a Titanium view proxy
Events: purchaseStarted, purchaseCompleted, purchaseCancelled, purchaseFailure, restoreStarted, restoreCompleted, restoreFailure, requestedDismissal
Returned by every method above that includes a customerInfo field:
{
originalAppUserId: "$RCAnonymousID:...",
activeEntitlements: {
premium: {
identifier: "premium",
isActive: true,
willRenew: true,
productIdentifier: "com.yourapp.subscription.monthly",
expirationDate: 1786994772 // seconds since epoch
}
},
allPurchasedProductIdentifiers: ["com.yourapp.subscription.monthly"],
managementURL: "https://apps.apple.com/account/subscriptions"
}
allPurchasedProductIdentifierslists every product ever purchased, including expired or cancelled ones — useactiveEntitlementsto gate access, not this list.
customerInfoUpdated- fires whenever entitlement state changes, including renewals and expirations that happen in the background
Both platforms:
- Titanium SDK 13.2.0.GA or later
- A RevenueCat account with at least one Entitlement, Offering, and Package configured
iOS:
- Titanium SDK 13.1.1.GA minimum (for SPM support)
- iOS 15.0+ deployment target
- An In-App Purchase Key uploaded to your RevenueCat app config
Android:
- minSdkVersion 24+
- Kotlin 2.3.0+ (must match the version used in the Gradle init script — see Installation)
- RevenueCat Android SDK 10.16.2+ (
com.revenuecat.purchases:purchases/purchases-ui) - Both manual setup steps from Installation (Gradle init script +
tiapp.xmlActivity declaration)
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT