Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/purchaseTesterExpo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",
"react-native-web": "~0.20.0"
"react-native-web": "~0.20.0",
"react-native-webview": "~13.16.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand Down
6 changes: 5 additions & 1 deletion react-native-purchases-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,15 @@
"react": "*",
"react-native": ">= 0.73.0",
"react-native-purchases": "9.6.9",
"react-native-web": "*"
"react-native-web": "*",
"react-native-webview": "*"
},
"peerDependenciesMeta": {
"react-native-web": {
"optional": true
},
"react-native-webview": {
"optional": true
}
},
"packageManager": "yarn@3.6.1",
Expand Down
25 changes: 25 additions & 0 deletions react-native-purchases-ui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import { previewNativeModuleRNCustomerCenter, previewNativeModuleRNPaywalls } fr
import { PreviewCustomerCenter, PreviewPaywall } from "./preview/previewComponents";

export { PAYWALL_RESULT } from "@revenuecat/purchases-typescript-internal";
// WebViewPaywallProvider is no longer exported - the SDK now automatically handles modal presentation in Expo Go
// WebViewPaywallModal is kept for backward compatibility but is also deprecated
export { WebViewPaywallModal } from "./preview/WebViewPaywallPresenter";

const LINKING_ERROR =
`The package 'react-native-purchases-ui' doesn't seem to be linked. Make sure: \n\n` +
Expand Down Expand Up @@ -714,6 +717,28 @@ export default class RevenueCatUI {
public static PaywallFooterContainerView: React.FC<FooterPaywallViewProps> =
RevenueCatUI.OriginalTemplatePaywallFooterContainerView;

/**
* Modal component for WebView-based paywall presentation in Expo Go.
*
* Add this component anywhere in your app to enable paywall presentation
* in environments without native module support (like Expo Go).
*
* Usage:
* ```tsx
* import RevenueCatUI from 'react-native-purchases-ui';
*
* export default function App() {
* return (
* <>
* <YourAppContent />
* <RevenueCatUI.WebViewPaywallModal />
* </>
* );
* }
* ```
*/
public static WebViewPaywallModal: React.FC = require("./preview/WebViewPaywallPresenter").WebViewPaywallModal;

private static logWarningIfPreviewAPIMode(methodName: string) {
if (usingPreviewAPIMode) {
// tslint:disable-next-line:no-console
Expand Down
90 changes: 90 additions & 0 deletions react-native-purchases-ui/src/preview/PaywallModalRoot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { subscribe, getPendingPaywall, handlePaywallComplete, dismissPaywall } from './WebViewPaywallPresenter';
import { PAYWALL_RESULT } from '@revenuecat/purchases-typescript-internal';

/**
* Internal component that automatically renders WebView paywall modals.
* This component is auto-mounted in Expo Go environments and should not be used directly by consumers.
* @internal
*/
export const PaywallModalRoot: React.FC = () => {
const [, forceUpdate] = useState({});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [WebViewComponent, setWebViewComponent] = useState<React.ComponentType<any> | null>(null);
const mountedRef = useRef(true);

// Load the WebViewPaywall component dynamically
useEffect(() => {
mountedRef.current = true;

import('./WebViewPaywall')
.then((module) => {
if (mountedRef.current) {
console.log("[PaywallModalRoot] WebViewPaywall component loaded");
setWebViewComponent(() => module.WebViewPaywall);
}
})
.catch((error) => {
console.warn('[PaywallModalRoot] Failed to load WebViewPaywall:', error);
});

return () => {
mountedRef.current = false;
};
}, []);

// Subscribe to paywall state changes
useEffect(() => {
console.log("[PaywallModalRoot] Subscribing to paywall state");
const unsubscribe = subscribe(() => {
console.log("[PaywallModalRoot] State changed, pending:", !!getPendingPaywall());
forceUpdate({});
});
return unsubscribe;
}, []);

const pendingPaywall = getPendingPaywall();

const handlePurchased = useCallback((_customerInfo: Record<string, unknown>) => {
console.log("[PaywallModalRoot] Purchase completed");
handlePaywallComplete(PAYWALL_RESULT.PURCHASED);
}, []);

const handleCancelled = useCallback(() => {
console.log("[PaywallModalRoot] Paywall cancelled");
handlePaywallComplete(PAYWALL_RESULT.CANCELLED);
}, []);

const handleError = useCallback((error: { message: string; code?: string }) => {
console.error('[PaywallModalRoot] Paywall error:', error);
handlePaywallComplete(PAYWALL_RESULT.ERROR);
}, []);

const handleDismiss = useCallback(() => {
console.log("[PaywallModalRoot] Paywall dismissed");
dismissPaywall();
}, []);

// Don't render anything if no paywall is pending or component not loaded
if (!WebViewComponent || !pendingPaywall) {
return null;
}

console.log("[PaywallModalRoot] Rendering WebView paywall");

return (
<WebViewComponent
visible={true}
apiKey={pendingPaywall.params.apiKey}
appUserId={pendingPaywall.params.appUserId}
offeringIdentifier={pendingPaywall.params.offeringIdentifier}
presentedOfferingContext={pendingPaywall.params.presentedOfferingContext}
customerEmail={pendingPaywall.params.customerEmail}
onPurchased={handlePurchased}
onCancelled={handleCancelled}
onError={handleError}
onDismiss={handleDismiss}
/>
);
};

Loading