diff --git a/__tests__/index.test.js b/__tests__/index.test.js
index 2d2905ee..f8c57cb7 100644
--- a/__tests__/index.test.js
+++ b/__tests__/index.test.js
@@ -1502,4 +1502,41 @@ describe("Purchases", () => {
});
});
});
+
+ describe("trackCustomPaywallImpression", () => {
+ describe("when Purchases is not configured", () => {
+ it("it rejects", async () => {
+ NativeModules.RNPurchases.isConfigured.mockResolvedValueOnce(false);
+
+ try {
+ await Purchases.trackCustomPaywallImpression();
+ fail("expected error");
+ } catch (error) { }
+
+ expect(NativeModules.RNPurchases.trackCustomPaywallImpression).toBeCalledTimes(0);
+ });
+ });
+
+ it("makes right call with no params", async () => {
+ await Purchases.trackCustomPaywallImpression();
+
+ expect(NativeModules.RNPurchases.trackCustomPaywallImpression).toBeCalledTimes(1);
+ expect(NativeModules.RNPurchases.trackCustomPaywallImpression).toBeCalledWith({});
+ });
+
+ it("makes right call with paywallId", async () => {
+ await Purchases.trackCustomPaywallImpression({ paywallId: "my_paywall" });
+
+ expect(NativeModules.RNPurchases.trackCustomPaywallImpression).toBeCalledTimes(1);
+ expect(NativeModules.RNPurchases.trackCustomPaywallImpression).toBeCalledWith({ paywallId: "my_paywall" });
+ });
+
+ it("makes right call with null paywallId", async () => {
+ await Purchases.trackCustomPaywallImpression({ paywallId: null });
+
+ expect(NativeModules.RNPurchases.trackCustomPaywallImpression).toBeCalledTimes(1);
+ expect(NativeModules.RNPurchases.trackCustomPaywallImpression).toBeCalledWith({ paywallId: null });
+ });
+
+ });
});
diff --git a/android/src/main/java/com/revenuecat/purchases/react/RNPurchasesModule.java b/android/src/main/java/com/revenuecat/purchases/react/RNPurchasesModule.java
index abc888fc..fd837315 100644
--- a/android/src/main/java/com/revenuecat/purchases/react/RNPurchasesModule.java
+++ b/android/src/main/java/com/revenuecat/purchases/react/RNPurchasesModule.java
@@ -609,6 +609,11 @@ public void redeemWebPurchase(String urlString, final Promise promise) {
CommonKt.redeemWebPurchase(urlString, getOnResult(promise));
}
+ @ReactMethod
+ public void trackCustomPaywallImpression(ReadableMap data) {
+ CommonKt.trackCustomPaywallImpression(data.toHashMap());
+ }
+
// endregion
//================================================================================
diff --git a/apitesters/purchases.ts b/apitesters/purchases.ts
index d2298741..df25725c 100644
--- a/apitesters/purchases.ts
+++ b/apitesters/purchases.ts
@@ -35,7 +35,7 @@ import {
} from "../dist";
import Purchases from "../dist/purchases";
-import { GoogleProductChangeInfo, SubscriptionOption } from "../src";
+import { GoogleProductChangeInfo, SubscriptionOption, TrackCustomPaywallImpressionOptions } from "../src";
async function checkPurchases(purchases: Purchases) {
const productIds: string[] = [];
@@ -455,6 +455,15 @@ async function checkOverridePreferredLocale() {
await Purchases.overridePreferredLocale(preferredUILocaleOverride);
}
+async function checkTrackCustomPaywallImpression() {
+ await Purchases.trackCustomPaywallImpression();
+ await Purchases.trackCustomPaywallImpression({});
+ await Purchases.trackCustomPaywallImpression({ paywallId: "my_paywall" });
+ await Purchases.trackCustomPaywallImpression({ paywallId: null });
+ const options: TrackCustomPaywallImpressionOptions = { paywallId: "test" };
+ await Purchases.trackCustomPaywallImpression(options);
+}
+
async function checkSyncPurchasesResult() {
const syncPurchasesResult: SyncPurchasesResult = await Purchases.syncPurchasesForResult();
const customerInfo: CustomerInfo = syncPurchasesResult.customerInfo;
diff --git a/examples/purchaseTesterTypescript/App.tsx b/examples/purchaseTesterTypescript/App.tsx
index bc403099..3fe9aabb 100644
--- a/examples/purchaseTesterTypescript/App.tsx
+++ b/examples/purchaseTesterTypescript/App.tsx
@@ -27,6 +27,7 @@ import VirtualCurrencyScreen from "./app/screens/VirtualCurrencyScreen";
import CustomVariablesScreen from "./app/screens/CustomVariablesScreen";
import { CustomVariablesProvider } from "./app/context/CustomVariablesContext";
import PurchaseLogicPaywallScreen from "./app/screens/PurchaseLogicPaywallScreen";
+import CustomPaywallScreen from "./app/screens/CustomPaywallScreen";
import APIKeys from './app/APIKeys';
import { SafeAreaView } from 'react-native-safe-area-context';
@@ -178,6 +179,11 @@ const App = () => {
component={PurchaseLogicPaywallScreen}
options={{ title: 'PurchaseLogic Paywall' }}
/>
+
diff --git a/examples/purchaseTesterTypescript/app/RootStackParamList.tsx b/examples/purchaseTesterTypescript/app/RootStackParamList.tsx
index 3ef3ac95..144da3fe 100644
--- a/examples/purchaseTesterTypescript/app/RootStackParamList.tsx
+++ b/examples/purchaseTesterTypescript/app/RootStackParamList.tsx
@@ -24,6 +24,7 @@ type RootStackParamList = {
CustomerCenterModalWithHeader: { shouldShowCloseButton?: boolean };
CustomVariables: undefined;
PurchaseLogicPaywall: { offering: PurchasesOffering | null };
+ CustomPaywall: undefined;
};
export default RootStackParamList;
diff --git a/examples/purchaseTesterTypescript/app/screens/CustomPaywallScreen.tsx b/examples/purchaseTesterTypescript/app/screens/CustomPaywallScreen.tsx
new file mode 100644
index 00000000..ec82cf81
--- /dev/null
+++ b/examples/purchaseTesterTypescript/app/screens/CustomPaywallScreen.tsx
@@ -0,0 +1,165 @@
+import React, {useState} from 'react';
+
+import {
+ SafeAreaView,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from 'react-native';
+
+import Purchases from 'react-native-purchases';
+import {NativeStackScreenProps} from '@react-navigation/native-stack';
+import RootStackParamList from '../RootStackParamList';
+
+type Props = NativeStackScreenProps;
+
+const CustomPaywallScreen: React.FC = ({navigation}) => {
+ const [status, setStatus] = useState(null);
+
+ const trackImpressionNoId = async () => {
+ try {
+ await Purchases.trackCustomPaywallImpression();
+ setStatus('trackCustomPaywallImpression (no id) succeeded');
+ console.log('[CustomPaywall] Tracked custom paywall impression (no id)');
+ } catch (e) {
+ setStatus(`Error: ${e}`);
+ }
+ };
+
+ const trackImpressionWithId = async () => {
+ try {
+ await Purchases.trackCustomPaywallImpression({
+ paywallId: 'my-test-paywall',
+ });
+ setStatus('trackCustomPaywallImpression (paywallId) succeeded');
+ console.log('[CustomPaywall] Tracked custom paywall impression (paywallId)');
+ } catch (e) {
+ setStatus(`Error: ${e}`);
+ }
+ };
+
+ return (
+
+
+ Custom Paywall Impression
+
+ Use this screen to test tracking custom paywall impressions.
+
+
+
+
+ Track impression (no params)
+
+
+
+
+
+ Track impression (paywallId only)
+
+
+
+ {status && (
+
+
+ {status}
+
+
+ )}
+
+ navigation.goBack()}>
+ Close
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#f8f9fa',
+ },
+ content: {
+ flex: 1,
+ padding: 24,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: '700',
+ marginBottom: 8,
+ color: '#212529',
+ },
+ description: {
+ fontSize: 14,
+ color: '#868e96',
+ marginBottom: 24,
+ textAlign: 'center',
+ },
+ button: {
+ width: '100%',
+ padding: 16,
+ backgroundColor: '#1971c2',
+ borderRadius: 12,
+ alignItems: 'center',
+ marginBottom: 12,
+ },
+ buttonText: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#ffffff',
+ },
+ statusContainer: {
+ width: '100%',
+ padding: 16,
+ borderRadius: 8,
+ marginTop: 12,
+ marginBottom: 12,
+ },
+ successContainer: {
+ backgroundColor: '#d3f9d8',
+ borderColor: '#69db7c',
+ borderWidth: 1,
+ },
+ errorContainer: {
+ backgroundColor: '#ffe3e3',
+ borderColor: '#ff8787',
+ borderWidth: 1,
+ },
+ successText: {
+ color: '#2b8a3e',
+ fontSize: 14,
+ },
+ errorText: {
+ color: '#c92a2a',
+ fontSize: 14,
+ },
+ closeButton: {
+ padding: 12,
+ },
+ closeButtonText: {
+ fontSize: 16,
+ color: '#868e96',
+ },
+});
+
+export default CustomPaywallScreen;
diff --git a/examples/purchaseTesterTypescript/app/screens/HomeScreen.tsx b/examples/purchaseTesterTypescript/app/screens/HomeScreen.tsx
index d95d1fe0..1f5b3b7f 100644
--- a/examples/purchaseTesterTypescript/app/screens/HomeScreen.tsx
+++ b/examples/purchaseTesterTypescript/app/screens/HomeScreen.tsx
@@ -570,6 +570,10 @@ const HomeScreen: React.FC = ({navigation}) => {
+ navigation.navigate('CustomPaywall')}>
+ Custom Paywall Screen (track impression)
+
navigation.navigate('WinBackTesting', {})}>
Win-Back Offer Testing
diff --git a/ios/RNPurchases.m b/ios/RNPurchases.m
index 12b05e47..494c18f4 100644
--- a/ios/RNPurchases.m
+++ b/ios/RNPurchases.m
@@ -627,6 +627,14 @@ static void logUnavailablePresentCodeRedemptionSheet() {
}
}
+RCT_EXPORT_METHOD(trackCustomPaywallImpression:(NSDictionary *)data) {
+ if (@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)) {
+ [RCCommonFunctionality trackCustomPaywallImpression:data];
+ } else {
+ NSLog(@"[Purchases] Warning: tried to call trackCustomPaywallImpression, but it's only available on iOS 15.0 or greater.");
+ }
+}
+
#pragma mark -
#pragma mark Delegate Methods
- (void)purchases:(RCPurchases *)purchases receivedUpdatedCustomerInfo:(RCCustomerInfo *)customerInfo {
diff --git a/scripts/setupJest.js b/scripts/setupJest.js
index 3c2e7122..3978d3f4 100644
--- a/scripts/setupJest.js
+++ b/scripts/setupJest.js
@@ -809,7 +809,8 @@ NativeModules.RNPurchases = {
redeemWebPurchase: jest.fn(),
getVirtualCurrencies: jest.fn(),
invalidateVirtualCurrenciesCache: jest.fn(),
- getCachedVirtualCurrencies: jest.fn()
+ getCachedVirtualCurrencies: jest.fn(),
+ trackCustomPaywallImpression: jest.fn()
};
jest.mock(
diff --git a/src/purchases.ts b/src/purchases.ts
index ede1d38a..93d6eae1 100644
--- a/src/purchases.ts
+++ b/src/purchases.ts
@@ -141,6 +141,13 @@ export interface DebugEvent {
*/
export type DebugEventListener = (event: DebugEvent) => void;
+/**
+ * Options for tracking a custom paywall impression.
+ */
+export interface TrackCustomPaywallImpressionOptions {
+ paywallId?: string | null;
+}
+
let debugEventListeners: DebugEventListener[] = [];
eventEmitter?.addListener(
@@ -1831,6 +1838,21 @@ export default class Purchases {
return RNPurchases.isConfigured();
}
+ /**
+ * Tracks an impression of a custom (non-RevenueCat) paywall.
+ * Call this method when your custom paywall is displayed to a user.
+ * This enables RevenueCat to track paywall impressions for analytics.
+ *
+ * @param params - Optional parameters for the impression event.
+ * @param params.paywallId - Optional identifier for the custom paywall being shown.
+ */
+ public static async trackCustomPaywallImpression(
+ params?: TrackCustomPaywallImpressionOptions
+ ): Promise {
+ await Purchases.throwIfNotConfigured();
+ RNPurchases.trackCustomPaywallImpression(params ?? {});
+ }
+
private static async throwIfNotConfigured() {
const isConfigured = await Purchases.isConfigured();
if (!isConfigured) {