diff --git a/__tests__/index.test.js b/__tests__/index.test.js index 425c950a4..9c01e1f46 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -1272,6 +1272,26 @@ describe("Purchases", () => { }); }); + describe("healthRequest", () => { + it("healthRequest calls native module with correct parameter", async () => { + NativeModules.RNPurchases.healthRequest.mockResolvedValueOnce(undefined); + + await Purchases.healthRequest(true); + + expect(NativeModules.RNPurchases.healthRequest).toBeCalledWith(true); + expect(NativeModules.RNPurchases.healthRequest).toBeCalledTimes(1); + }); + + it("healthRequest passes false for signatureVerification", async () => { + NativeModules.RNPurchases.healthRequest.mockResolvedValueOnce(undefined); + + await Purchases.healthRequest(false); + + expect(NativeModules.RNPurchases.healthRequest).toBeCalledWith(false); + expect(NativeModules.RNPurchases.healthRequest).toBeCalledTimes(1); + }); + }); + describe("beginRefundRequest", () => { beforeEach(() => { Platform.OS = "ios"; 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 b345519f3..538f6b362 100644 --- a/android/src/main/java/com/revenuecat/purchases/react/RNPurchasesModule.java +++ b/android/src/main/java/com/revenuecat/purchases/react/RNPurchasesModule.java @@ -588,6 +588,53 @@ public void redeemWebPurchase(String urlString, final Promise promise) { CommonKt.redeemWebPurchase(urlString, getOnResult(promise)); } + // region Health + + @ReactMethod + public void healthRequest(boolean signatureVerification, final Promise promise) { + CommonKt.healthRequest(signatureVerification, new OnResult() { + @Override + public void onReceived(Map map) { + promise.resolve(null); + } + + @Override + public void onError(ErrorContainer errorContainer) { + promise.reject(errorContainer.getCode() + "", errorContainer.getMessage(), + convertMapToWriteableMap(errorContainer.getInfo())); + } + }); + } + + // endregion + + // region Ad Tracking + + @ReactMethod + public void trackAdFailedToLoad(ReadableMap adData) { + CommonKt.trackAdFailedToLoad(adData.toHashMap()); + } + + @ReactMethod + public void trackAdLoaded(ReadableMap adData) { + CommonKt.trackAdLoaded(adData.toHashMap()); + } + + @ReactMethod + public void trackAdDisplayed(ReadableMap adData) { + CommonKt.trackAdDisplayed(adData.toHashMap()); + } + + @ReactMethod + public void trackAdOpened(ReadableMap adData) { + CommonKt.trackAdOpened(adData.toHashMap()); + } + + @ReactMethod + public void trackAdRevenue(ReadableMap adData) { + CommonKt.trackAdRevenue(adData.toHashMap()); + } + // endregion //================================================================================ diff --git a/ios/RNPurchases.m b/ios/RNPurchases.m index 2333929bf..1df0d0164 100644 --- a/ios/RNPurchases.m +++ b/ios/RNPurchases.m @@ -337,6 +337,46 @@ static void logUnavailablePresentCodeRedemptionSheet() { } +#pragma mark - Health + +RCT_EXPORT_METHOD(healthRequest:(BOOL)signatureVerification + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [RCCommonFunctionality healthRequestWithSignatureVerification:signatureVerification completion:^(RCErrorContainer * _Nullable errorContainer) { + if (errorContainer) { + reject( + [NSString stringWithFormat:@"%ld", (long)errorContainer.code], + errorContainer.message, + errorContainer.error + ); + } else { + resolve(nil); + } + }]; +} + +#pragma mark - Ad Tracking + +RCT_EXPORT_METHOD(trackAdFailedToLoad:(NSDictionary *)adData) { + [RCCommonFunctionality trackAdFailedToLoad:adData.mappingNSNullToNil]; +} + +RCT_EXPORT_METHOD(trackAdLoaded:(NSDictionary *)adData) { + [RCCommonFunctionality trackAdLoaded:adData.mappingNSNullToNil]; +} + +RCT_EXPORT_METHOD(trackAdDisplayed:(NSDictionary *)adData) { + [RCCommonFunctionality trackAdDisplayed:adData.mappingNSNullToNil]; +} + +RCT_EXPORT_METHOD(trackAdOpened:(NSDictionary *)adData) { + [RCCommonFunctionality trackAdOpened:adData.mappingNSNullToNil]; +} + +RCT_EXPORT_METHOD(trackAdRevenue:(NSDictionary *)adData) { + [RCCommonFunctionality trackAdRevenue:adData.mappingNSNullToNil]; +} + #pragma mark - Subscriber Attributes RCT_EXPORT_METHOD(setProxyURLString:(nullable NSString *)proxyURLString diff --git a/src/ads.ts b/src/ads.ts new file mode 100644 index 000000000..bd02cfc26 --- /dev/null +++ b/src/ads.ts @@ -0,0 +1,132 @@ +/** + * Ad tracking types for reporting ad events to RevenueCat. + * + * These types map to the native SDK's ad tracking API, enabling comprehensive + * LTV tracking across subscriptions and ad monetization. + */ + +/** + * Predefined ad mediator names. You can also use custom string values. + */ +export const AD_MEDIATOR_NAME = { + AD_MOB: "AdMob", + APP_LOVIN: "AppLovin", +} as const; + +/** + * Predefined ad format types. + */ +export const AD_FORMAT = { + OTHER: "other", + BANNER: "banner", + INTERSTITIAL: "interstitial", + REWARDED: "rewarded", + REWARDED_INTERSTITIAL: "rewarded_interstitial", + NATIVE: "native", + APP_OPEN: "app_open", + MREC: "mrec", +} as const; + +/** + * Revenue precision levels for ad revenue reporting. + */ +export const AD_REVENUE_PRECISION = { + EXACT: "exact", + PUBLISHER_DEFINED: "publisher_defined", + ESTIMATED: "estimated", + UNKNOWN: "unknown", +} as const; + +/** + * Data for tracking a failed ad load event. + */ +export interface AdFailedToLoadData { + /** The mediation SDK name (e.g. "AdMob", "AppLovin") */ + mediatorName: string; + /** The ad format (e.g. "banner", "interstitial") */ + adFormat: string; + /** The ad unit identifier */ + adUnitId: string; + /** Optional placement identifier */ + placement?: string | null; + /** Optional error code from the mediation SDK */ + mediatorErrorCode?: number | null; +} + +/** + * Data for tracking a successful ad load event. + */ +export interface AdLoadedData { + /** The ad network name */ + networkName?: string | null; + /** The mediation SDK name */ + mediatorName: string; + /** The ad format */ + adFormat: string; + /** The ad unit identifier */ + adUnitId: string; + /** Unique impression identifier */ + impressionId: string; + /** Optional placement identifier */ + placement?: string | null; +} + +/** + * Data for tracking an ad display/impression event. + */ +export interface AdDisplayedData { + /** The ad network name */ + networkName?: string | null; + /** The mediation SDK name */ + mediatorName: string; + /** The ad format */ + adFormat: string; + /** The ad unit identifier */ + adUnitId: string; + /** Unique impression identifier */ + impressionId: string; + /** Optional placement identifier */ + placement?: string | null; +} + +/** + * Data for tracking an ad opened/clicked event. + */ +export interface AdOpenedData { + /** The ad network name */ + networkName?: string | null; + /** The mediation SDK name */ + mediatorName: string; + /** The ad format */ + adFormat: string; + /** The ad unit identifier */ + adUnitId: string; + /** Unique impression identifier */ + impressionId: string; + /** Optional placement identifier */ + placement?: string | null; +} + +/** + * Data for tracking ad revenue. + */ +export interface AdRevenueData { + /** The ad network name */ + networkName?: string | null; + /** The mediation SDK name */ + mediatorName: string; + /** The ad format */ + adFormat: string; + /** The ad unit identifier */ + adUnitId: string; + /** Unique impression identifier */ + impressionId: string; + /** Optional placement identifier */ + placement?: string | null; + /** Revenue in micro-units (e.g. 1500000 = $1.50) */ + revenueMicros: number; + /** ISO 4217 currency code (e.g. "USD") */ + currency: string; + /** Revenue accuracy level */ + precision: string; +} diff --git a/src/index.ts b/src/index.ts index 947501548..738098c54 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,4 @@ export * from './errors'; export * from './customerInfo'; export * from './purchases'; export * from './offerings'; +export * from './ads'; diff --git a/src/purchases.ts b/src/purchases.ts index d973d23a6..c7ed4321f 100644 --- a/src/purchases.ts +++ b/src/purchases.ts @@ -1,4 +1,11 @@ import { NativeEventEmitter, NativeModules } from "react-native"; +import type { + AdFailedToLoadData, + AdLoadedData, + AdDisplayedData, + AdOpenedData, + AdRevenueData, +} from "./ads"; import { PurchasesError, PURCHASES_ERROR_CODE, @@ -1551,6 +1558,70 @@ export default class Purchases { RNPurchases.overridePreferredLocale(locale); } + /** + * Performs an unauthenticated request to the RevenueCat API to verify connectivity. + * @param {boolean} signatureVerification Whether to verify the response signature. + * @returns {Promise} The promise will be rejected if the request fails or if configure + * has not been called yet. + */ + public static async healthRequest( + signatureVerification: boolean + ): Promise { + await Purchases.throwIfNotConfigured(); + return RNPurchases.healthRequest(signatureVerification); + } + + // region Ad Tracking + + /** + * Tracks when an ad fails to load. Call this from your ad SDK's failure callback. + * @param {AdFailedToLoadData} data The failed-to-load event data + */ + public static async trackAdFailedToLoad( + data: AdFailedToLoadData + ): Promise { + await Purchases.throwIfNotConfigured(); + RNPurchases.trackAdFailedToLoad(data); + } + + /** + * Tracks when an ad successfully loads. + * @param {AdLoadedData} data The ad loaded event data + */ + public static async trackAdLoaded(data: AdLoadedData): Promise { + await Purchases.throwIfNotConfigured(); + RNPurchases.trackAdLoaded(data); + } + + /** + * Tracks when an ad is displayed (impression). + * @param {AdDisplayedData} data The ad displayed event data + */ + public static async trackAdDisplayed(data: AdDisplayedData): Promise { + await Purchases.throwIfNotConfigured(); + RNPurchases.trackAdDisplayed(data); + } + + /** + * Tracks when an ad is opened/clicked. + * @param {AdOpenedData} data The ad opened event data + */ + public static async trackAdOpened(data: AdOpenedData): Promise { + await Purchases.throwIfNotConfigured(); + RNPurchases.trackAdOpened(data); + } + + /** + * Tracks ad revenue. Use this to report ad revenue alongside subscription data. + * @param {AdRevenueData} data The ad revenue event data + */ + public static async trackAdRevenue(data: AdRevenueData): Promise { + await Purchases.throwIfNotConfigured(); + RNPurchases.trackAdRevenue(data); + } + + // endregion + /** * Check if billing is supported for the current user (meaning IN-APP purchases are supported) * and optionally, whether a list of specified feature types are supported.