UNPKG

react-native-purchases-ui

Version:

React Native in-app purchases and subscriptions made easy. Supports iOS and Android.

568 lines (547 loc) 25.9 kB
import { NativeEventEmitter, NativeModules, Platform, requireNativeComponent, ScrollView, UIManager, View } from "react-native"; import { PAYWALL_RESULT } from "@revenuecat/purchases-typescript-internal"; import React, { useEffect, useState } from "react"; import { shouldUsePreviewAPIMode } from "./utils/environment"; import { previewNativeModuleRNCustomerCenter, previewNativeModuleRNPaywalls } from "./preview/nativeModules"; import { PreviewCustomerCenter, PreviewPaywall } from "./preview/previewComponents"; import { convertCustomVariablesToNativeMap, transformOptionsForNative } from "./customVariables"; export { PAYWALL_RESULT } from "@revenuecat/purchases-typescript-internal"; export { CustomVariableValue } from "./customVariables"; // Re-export for testing purposes (marked as @internal) export { convertCustomVariablesToNativeMap, convertCustomVariablesToStringMap, transformOptionsForNative } from "./customVariables"; /** * The result of a purchase or restore operation performed by custom app-based logic. * Used when `purchasesAreCompletedBy` is set to `MY_APP`. * @readonly * @enum {string} */ export let PURCHASE_LOGIC_RESULT = /*#__PURE__*/function (PURCHASE_LOGIC_RESULT) { /** The purchase or restore was successful. */ PURCHASE_LOGIC_RESULT["SUCCESS"] = "SUCCESS"; /** The purchase was cancelled by the user. */ PURCHASE_LOGIC_RESULT["CANCELLATION"] = "CANCELLATION"; /** An error occurred during the purchase or restore. */ PURCHASE_LOGIC_RESULT["ERROR"] = "ERROR"; return PURCHASE_LOGIC_RESULT; }({}); /** * The result of a purchase or restore operation performed by custom purchase logic. * Uses a discriminated union to allow structured error information. */ /** * Interface for handling purchases and restores within paywalls when * `purchasesAreCompletedBy` is set to `MY_APP`. * * When provided, the paywall will call these functions instead of using * RevenueCat's default purchase/restore behavior. */ const NATIVE_MODULE_NOT_FOUND_ERROR = `[RevenueCatUI] Native module not found. This can happen if:\n\n` + `- You are running in an unsupported environment (e.g., A browser or a container app that doesn't actually use the native modules)\n` + `- The native module failed to initialize\n` + `- react-native-purchases is not properly installed\n\n` + `To fix this:\n` + `- If using Expo, create a development build: https://docs.expo.dev/develop/development-builds/create-a-build/\n` + `- If using bare React Native, run 'pod install' and rebuild the app\n` + `- Make sure react-native-purchases is installed and you have rebuilt the app\n`; // Get the native module or use the preview implementation const usingPreviewAPIMode = shouldUsePreviewAPIMode(); const RNPaywalls = usingPreviewAPIMode ? previewNativeModuleRNPaywalls : NativeModules.RNPaywalls; const RNCustomerCenter = usingPreviewAPIMode ? previewNativeModuleRNCustomerCenter : NativeModules.RNCustomerCenter; // Helper function to check native module availability - called at usage time, not import time function throwIfNativeModulesNotAvailable() { if (!RNPaywalls || !RNCustomerCenter) { throw new Error(NATIVE_MODULE_NOT_FOUND_ERROR); } } // Internal native props include purchase logic bridge events and native custom variable transforms const NativePaywall = !usingPreviewAPIMode && UIManager.getViewManagerConfig('Paywall') != null ? requireNativeComponent('Paywall') : null; const NativePaywallFooter = !usingPreviewAPIMode && UIManager.getViewManagerConfig('Paywall') != null ? requireNativeComponent('RCPaywallFooterView') : null; // Only create event emitters if native modules are available const eventEmitter = !usingPreviewAPIMode && RNPaywalls ? new NativeEventEmitter(RNPaywalls) : null; const customerCenterEventEmitter = !usingPreviewAPIMode && RNCustomerCenter ? new NativeEventEmitter(RNCustomerCenter) : null; function resolveLogicResult(requestId, logicResult) { const errorMessage = logicResult.result === PURCHASE_LOGIC_RESULT.ERROR && logicResult.error ? logicResult.error.message : null; RNPaywalls === null || RNPaywalls === void 0 || RNPaywalls.resolvePurchaseLogicResult(requestId, logicResult.result, errorMessage); } function createPurchaseLogicHandlers(purchaseLogic) { if (!purchaseLogic) { return { nativeOptions: {}, handlePerformPurchase: undefined, handlePerformRestore: undefined }; } const nativeOptions = { hasPurchaseLogic: true }; const handlePerformPurchase = async event => { const { requestId, packageBeingPurchased } = event.nativeEvent; try { const logicResult = await purchaseLogic.performPurchase({ packageToPurchase: packageBeingPurchased }); resolveLogicResult(requestId, logicResult); } catch (e) { RNPaywalls === null || RNPaywalls === void 0 || RNPaywalls.resolvePurchaseLogicResult(requestId, PURCHASE_LOGIC_RESULT.ERROR, e instanceof Error ? e.message : null); } }; const handlePerformRestore = async event => { const { requestId } = event.nativeEvent; try { const logicResult = await purchaseLogic.performRestore(); resolveLogicResult(requestId, logicResult); } catch (e) { RNPaywalls === null || RNPaywalls === void 0 || RNPaywalls.resolvePurchaseLogicResult(requestId, PURCHASE_LOGIC_RESULT.ERROR, e instanceof Error ? e.message : null); } }; return { nativeOptions, handlePerformPurchase, handlePerformRestore }; } const InternalPaywall = ({ style, children, options, purchaseLogic, onPurchaseStarted, onPurchaseCompleted, onPurchaseError, onPurchaseCancelled, onRestoreStarted, onRestoreCompleted, onRestoreError, onDismiss, onPurchasePackageInitiated }) => { const { nativeOptions, handlePerformPurchase, handlePerformRestore } = createPurchaseLogicHandlers(purchaseLogic); if (usingPreviewAPIMode) { return /*#__PURE__*/React.createElement(PreviewPaywall, { offering: options === null || options === void 0 ? void 0 : options.offering, displayCloseButton: options === null || options === void 0 ? void 0 : options.displayCloseButton, fontFamily: options === null || options === void 0 ? void 0 : options.fontFamily, onPurchaseStarted: onPurchaseStarted, onPurchaseCompleted: onPurchaseCompleted, onPurchaseError: onPurchaseError, onPurchaseCancelled: onPurchaseCancelled, onRestoreStarted: onRestoreStarted, onRestoreCompleted: onRestoreCompleted, onRestoreError: onRestoreError, onDismiss: onDismiss }); } else if (!!NativePaywall) { // Transform options to native format (CustomVariables -> string map) const transformedOptions = transformOptionsForNative(options); return /*#__PURE__*/React.createElement(NativePaywall, { style: style, children: children, options: { ...transformedOptions, ...nativeOptions }, onPurchaseStarted: event => onPurchaseStarted && onPurchaseStarted(event.nativeEvent), onPurchaseCompleted: event => onPurchaseCompleted && onPurchaseCompleted(event.nativeEvent), onPurchaseError: event => onPurchaseError && onPurchaseError(event.nativeEvent), onPurchaseCancelled: () => onPurchaseCancelled && onPurchaseCancelled(), onRestoreStarted: () => onRestoreStarted && onRestoreStarted(), onRestoreCompleted: event => onRestoreCompleted && onRestoreCompleted(event.nativeEvent), onRestoreError: event => onRestoreError && onRestoreError(event.nativeEvent), onDismiss: () => onDismiss && onDismiss(), onPurchasePackageInitiated: event => { const { packageBeingPurchased, requestId } = event.nativeEvent; if (onPurchasePackageInitiated) { const resume = shouldProceed => { RNPaywalls.resumePurchasePackageInitiated(requestId, shouldProceed); }; onPurchasePackageInitiated({ packageBeingPurchased, resume }); } else { RNPaywalls.resumePurchasePackageInitiated(requestId, true); } }, onPerformPurchase: handlePerformPurchase, onPerformRestore: handlePerformRestore }); } throw new Error(NATIVE_MODULE_NOT_FOUND_ERROR); }; const InternalPaywallFooterView = ({ style, children, options, onPurchaseStarted, onPurchaseCompleted, onPurchaseError, onPurchaseCancelled, onRestoreStarted, onRestoreCompleted, onRestoreError, onDismiss, onMeasure }) => { if (usingPreviewAPIMode) { return /*#__PURE__*/React.createElement(PreviewPaywall, { offering: options === null || options === void 0 ? void 0 : options.offering, displayCloseButton: true, fontFamily: options === null || options === void 0 ? void 0 : options.fontFamily, onPurchaseStarted: onPurchaseStarted, onPurchaseCompleted: onPurchaseCompleted, onPurchaseError: onPurchaseError, onPurchaseCancelled: onPurchaseCancelled, onRestoreStarted: onRestoreStarted, onRestoreCompleted: onRestoreCompleted, onRestoreError: onRestoreError, onDismiss: onDismiss }); } else if (!!NativePaywallFooter) { // Transform options to native format (CustomVariables -> string map) const transformedOptions = transformOptionsForNative(options); return /*#__PURE__*/React.createElement(NativePaywallFooter, { style: style, children: children, options: transformedOptions, onPurchaseStarted: event => onPurchaseStarted && onPurchaseStarted(event.nativeEvent), onPurchaseCompleted: event => onPurchaseCompleted && onPurchaseCompleted(event.nativeEvent), onPurchaseError: event => onPurchaseError && onPurchaseError(event.nativeEvent), onPurchaseCancelled: () => onPurchaseCancelled && onPurchaseCancelled(), onRestoreStarted: () => onRestoreStarted && onRestoreStarted(), onRestoreCompleted: event => onRestoreCompleted && onRestoreCompleted(event.nativeEvent), onRestoreError: event => onRestoreError && onRestoreError(event.nativeEvent), onDismiss: () => onDismiss && onDismiss(), onMeasure: onMeasure }); } throw new Error(NATIVE_MODULE_NOT_FOUND_ERROR); }; // Currently the same as the base type, but can be extended later if needed /** * Helper type that replaces CustomVariables with NativeCustomVariables in an options type. * @internal */ const InternalCustomerCenterView = !usingPreviewAPIMode && UIManager.getViewManagerConfig('CustomerCenterView') != null ? requireNativeComponent('CustomerCenterView') : null; // This is to prevent breaking changes when the native SDK adds new options export default class RevenueCatUI { static Defaults = { PRESENT_PAYWALL_DISPLAY_CLOSE_BUTTON: true }; /** * The result of presenting a paywall. This will be the last situation the user experienced before the paywall closed. * @readonly * @enum {string} */ static PAYWALL_RESULT = PAYWALL_RESULT; /** * Presents a paywall to the user with optional customization. * * This method allows for presenting a specific offering's paywall to the user. The caller * can decide whether to display a close button on the paywall through the `displayCloseButton` * parameter. By default, the close button is displayed. * * @param {PresentPaywallParams} params - The options for presenting the paywall. * @returns {Promise<PAYWALL_RESULT>} A promise that resolves with the result of the paywall presentation. */ static presentPaywall({ offering, displayCloseButton = RevenueCatUI.Defaults.PRESENT_PAYWALL_DISPLAY_CLOSE_BUTTON, fontFamily, customVariables } = {}) { var _offering$availablePa; throwIfNativeModulesNotAvailable(); RevenueCatUI.logWarningIfPreviewAPIMode("presentPaywall"); return RNPaywalls.presentPaywall((offering === null || offering === void 0 ? void 0 : offering.identifier) ?? null, offering === null || offering === void 0 || (_offering$availablePa = offering.availablePackages) === null || _offering$availablePa === void 0 || (_offering$availablePa = _offering$availablePa[0]) === null || _offering$availablePa === void 0 ? void 0 : _offering$availablePa.presentedOfferingContext, displayCloseButton, fontFamily, convertCustomVariablesToNativeMap(customVariables)); } /** * Presents a paywall to the user if a specific entitlement is not already owned. * * This method evaluates whether the user already owns the specified entitlement. * If the entitlement is not owned, it presents a paywall for the specified offering (if provided), or the * default offering (if no offering is provided), to the user. The paywall will be presented * allowing the user the opportunity to purchase the offering. The caller * can decide whether to display a close button on the paywall through the `displayCloseButton` * parameter. By default, the close button is displayed. * * @param {PresentPaywallIfNeededParams} params - The parameters for presenting the paywall. * @returns {Promise<PAYWALL_RESULT>} A promise that resolves with the result of the paywall presentation. */ static presentPaywallIfNeeded({ requiredEntitlementIdentifier, offering, displayCloseButton = RevenueCatUI.Defaults.PRESENT_PAYWALL_DISPLAY_CLOSE_BUTTON, fontFamily, customVariables }) { var _offering$availablePa2; throwIfNativeModulesNotAvailable(); RevenueCatUI.logWarningIfPreviewAPIMode("presentPaywallIfNeeded"); return RNPaywalls.presentPaywallIfNeeded(requiredEntitlementIdentifier, (offering === null || offering === void 0 ? void 0 : offering.identifier) ?? null, offering === null || offering === void 0 || (_offering$availablePa2 = offering.availablePackages) === null || _offering$availablePa2 === void 0 || (_offering$availablePa2 = _offering$availablePa2[0]) === null || _offering$availablePa2 === void 0 ? void 0 : _offering$availablePa2.presentedOfferingContext, displayCloseButton, fontFamily, convertCustomVariablesToNativeMap(customVariables)); } static Paywall = ({ style, children, options, purchaseLogic, onPurchaseStarted, onPurchaseCompleted, onPurchaseError, onPurchaseCancelled, onRestoreStarted, onRestoreCompleted, onRestoreError, onDismiss, onPurchasePackageInitiated }) => { return /*#__PURE__*/React.createElement(InternalPaywall, { options: options, children: children, purchaseLogic: purchaseLogic, onPurchaseStarted: onPurchaseStarted, onPurchaseCompleted: onPurchaseCompleted, onPurchaseError: onPurchaseError, onPurchaseCancelled: onPurchaseCancelled, onRestoreStarted: onRestoreStarted, onRestoreCompleted: onRestoreCompleted, onRestoreError: onRestoreError, onDismiss: onDismiss, onPurchasePackageInitiated: onPurchasePackageInitiated, style: [{ flex: 1 }, style] }); }; static OriginalTemplatePaywallFooterContainerView = ({ style, children, options, onPurchaseStarted, onPurchaseCompleted, onPurchaseError, onPurchaseCancelled, onRestoreStarted, onRestoreCompleted, onRestoreError, onDismiss }) => { // We use 20 as the default paddingBottom because that's the corner radius in the Android native SDK. // We also listen to safeAreaInsetsDidChange which is only sent from iOS and which is triggered when the // safe area insets change. Not adding this extra padding on iOS will cause the content of the scrollview // to be hidden behind the rounded corners of the paywall. const [paddingBottom, setPaddingBottom] = useState(20); const [height, setHeight] = useState(0); useEffect(() => { const handleSafeAreaInsetsChange = ({ bottom }) => { setPaddingBottom(20 + bottom); }; const subscription = eventEmitter === null || eventEmitter === void 0 ? void 0 : eventEmitter.addListener('safeAreaInsetsDidChange', handleSafeAreaInsetsChange); return () => { subscription === null || subscription === void 0 || subscription.remove(); }; }, []); return /*#__PURE__*/React.createElement(View, { style: [{ flex: 1 }, style] }, /*#__PURE__*/React.createElement(ScrollView, { contentContainerStyle: { flexGrow: 1, paddingBottom } }, children), /*#__PURE__*/React.createElement(InternalPaywallFooterView, { style: Platform.select({ ios: { marginTop: -20 }, android: { marginTop: -20, height } }), options: options, onPurchaseStarted: onPurchaseStarted, onPurchaseCompleted: onPurchaseCompleted, onPurchaseError: onPurchaseError, onPurchaseCancelled: onPurchaseCancelled, onRestoreStarted: onRestoreStarted, onRestoreCompleted: onRestoreCompleted, onRestoreError: onRestoreError, onDismiss: onDismiss, onMeasure: event => setHeight(event.nativeEvent.measurements.height) })); }; /** * A React component for embedding the Customer Center directly in your UI. * * This component renders the RevenueCat Customer Center as a native view that can be * embedded within your existing React Native screens. Unlike `presentCustomerCenter()`, * which presents the Customer Center modally, this component gives you full control * over layout and presentation. * * The Customer Center allows users to manage their subscriptions, view purchase history, * request refunds (iOS only), and access support options - all configured through the * RevenueCat dashboard. * * @param style - Optional style prop to customize the appearance and layout * @param onDismiss - Callback fired when the user dismisses the Customer Center (e.g., taps a close button) * * @example * ```tsx * import RevenueCatUI from 'react-native-purchases-ui'; * * function MyScreen() { * return ( * <View style={{ flex: 1 }}> * <RevenueCatUI.CustomerCenterView * style={{ flex: 1 }} * onDismiss={() => navigation.goBack()} * /> * </View> * ); * } * ``` */ static CustomerCenterView = ({ style, onDismiss, onCustomActionSelected, onFeedbackSurveyCompleted, onShowingManageSubscriptions, onRestoreCompleted, onRestoreFailed, onRestoreStarted, onRefundRequestStarted, onRefundRequestCompleted, onManagementOptionSelected, onPromotionalOfferSucceeded, shouldShowCloseButton = true }) => { if (usingPreviewAPIMode) { return /*#__PURE__*/React.createElement(PreviewCustomerCenter, { onDismiss: () => onDismiss && onDismiss(), style: [{ flex: 1 }, style] }); } if (!InternalCustomerCenterView) { throw new Error(NATIVE_MODULE_NOT_FOUND_ERROR); } return /*#__PURE__*/React.createElement(InternalCustomerCenterView, { onDismiss: onDismiss ? () => onDismiss() : undefined, onCustomActionSelected: onCustomActionSelected ? event => onCustomActionSelected(event.nativeEvent) : undefined, onFeedbackSurveyCompleted: onFeedbackSurveyCompleted ? event => onFeedbackSurveyCompleted(event.nativeEvent) : undefined, onShowingManageSubscriptions: onShowingManageSubscriptions ? () => onShowingManageSubscriptions() : undefined, onRestoreCompleted: onRestoreCompleted ? event => onRestoreCompleted(event.nativeEvent) : undefined, onRestoreFailed: onRestoreFailed ? event => onRestoreFailed(event.nativeEvent) : undefined, onRestoreStarted: onRestoreStarted ? () => onRestoreStarted() : undefined, onRefundRequestStarted: onRefundRequestStarted ? event => onRefundRequestStarted(event.nativeEvent) : undefined, onRefundRequestCompleted: onRefundRequestCompleted ? event => onRefundRequestCompleted(event.nativeEvent) : undefined, onManagementOptionSelected: onManagementOptionSelected ? event => onManagementOptionSelected(event.nativeEvent) : undefined, onPromotionalOfferSucceeded: onPromotionalOfferSucceeded ? event => onPromotionalOfferSucceeded(event.nativeEvent) : undefined, shouldShowCloseButton: shouldShowCloseButton, style: [{ flex: 1 }, style] }); }; /** * Presents the customer center to the user. * * @param {PresentCustomerCenterParams} params - Optional parameters for presenting the customer center. * @returns {Promise<void>} A promise that resolves when the customer center is presented. */ static presentCustomerCenter(params) { throwIfNativeModulesNotAvailable(); if (params !== null && params !== void 0 && params.callbacks) { const subscriptions = []; const callbacks = params.callbacks; if (callbacks.onFeedbackSurveyCompleted) { const subscription = customerCenterEventEmitter === null || customerCenterEventEmitter === void 0 ? void 0 : customerCenterEventEmitter.addListener('onFeedbackSurveyCompleted', event => callbacks.onFeedbackSurveyCompleted && callbacks.onFeedbackSurveyCompleted(event)); if (subscription) { subscriptions.push(subscription); } } if (callbacks.onShowingManageSubscriptions) { const subscription = customerCenterEventEmitter === null || customerCenterEventEmitter === void 0 ? void 0 : customerCenterEventEmitter.addListener('onShowingManageSubscriptions', () => callbacks.onShowingManageSubscriptions && callbacks.onShowingManageSubscriptions()); if (subscription) { subscriptions.push(subscription); } } if (callbacks.onRestoreCompleted) { const subscription = customerCenterEventEmitter === null || customerCenterEventEmitter === void 0 ? void 0 : customerCenterEventEmitter.addListener('onRestoreCompleted', event => callbacks.onRestoreCompleted && callbacks.onRestoreCompleted(event)); if (subscription) { subscriptions.push(subscription); } } if (callbacks.onRestoreFailed) { const subscription = customerCenterEventEmitter === null || customerCenterEventEmitter === void 0 ? void 0 : customerCenterEventEmitter.addListener('onRestoreFailed', event => callbacks.onRestoreFailed && callbacks.onRestoreFailed(event)); if (subscription) { subscriptions.push(subscription); } } if (callbacks.onRestoreStarted) { const subscription = customerCenterEventEmitter === null || customerCenterEventEmitter === void 0 ? void 0 : customerCenterEventEmitter.addListener('onRestoreStarted', () => callbacks.onRestoreStarted && callbacks.onRestoreStarted()); if (subscription) { subscriptions.push(subscription); } } if (callbacks.onRefundRequestStarted) { const subscription = customerCenterEventEmitter === null || customerCenterEventEmitter === void 0 ? void 0 : customerCenterEventEmitter.addListener('onRefundRequestStarted', event => callbacks.onRefundRequestStarted && callbacks.onRefundRequestStarted(event)); if (subscription) { subscriptions.push(subscription); } } if (callbacks.onRefundRequestCompleted) { const subscription = customerCenterEventEmitter === null || customerCenterEventEmitter === void 0 ? void 0 : customerCenterEventEmitter.addListener('onRefundRequestCompleted', event => callbacks.onRefundRequestCompleted && callbacks.onRefundRequestCompleted(event)); if (subscription) { subscriptions.push(subscription); } } if (callbacks.onManagementOptionSelected) { const subscription = customerCenterEventEmitter === null || customerCenterEventEmitter === void 0 ? void 0 : customerCenterEventEmitter.addListener('onManagementOptionSelected', event => callbacks.onManagementOptionSelected && callbacks.onManagementOptionSelected(event)); if (subscription) { subscriptions.push(subscription); } } if (callbacks.onCustomActionSelected) { const subscription = customerCenterEventEmitter === null || customerCenterEventEmitter === void 0 ? void 0 : customerCenterEventEmitter.addListener('onCustomActionSelected', event => callbacks.onCustomActionSelected && callbacks.onCustomActionSelected(event)); if (subscription) { subscriptions.push(subscription); } } if (callbacks.onPromotionalOfferSucceeded) { const subscription = customerCenterEventEmitter === null || customerCenterEventEmitter === void 0 ? void 0 : customerCenterEventEmitter.addListener('onPromotionalOfferSucceeded', event => callbacks.onPromotionalOfferSucceeded && callbacks.onPromotionalOfferSucceeded(event)); if (subscription) { subscriptions.push(subscription); } } // Return a promise that resolves when the customer center is dismissed return RNCustomerCenter.presentCustomerCenter().finally(() => { // Clean up all event listeners when the customer center is dismissed subscriptions.forEach(subscription => subscription.remove()); }); } RevenueCatUI.logWarningIfPreviewAPIMode("presentCustomerCenter"); return RNCustomerCenter.presentCustomerCenter(); } /** * @deprecated, Use {@link OriginalTemplatePaywallFooterContainerView} instead */ static PaywallFooterContainerView = RevenueCatUI.OriginalTemplatePaywallFooterContainerView; static logWarningIfPreviewAPIMode(methodName) { if (usingPreviewAPIMode) { // tslint:disable-next-line:no-console console.warn(`[RevenueCatUI] [${methodName}] This method is available but has no effect in Preview API mode.`); } } } //# sourceMappingURL=index.js.map