diff --git a/example/package.json b/example/package.json
index 0492ea04..a51cc2b6 100644
--- a/example/package.json
+++ b/example/package.json
@@ -13,8 +13,10 @@
"cleanIos": "yarn clean && yarn pods"
},
"dependencies": {
+ "@react-native-clipboard/clipboard": "^1.16.3",
"react": "19.1.0",
- "react-native": "0.80.1"
+ "react-native": "0.80.1",
+ "react-native-snackbar": "^2.9.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 6ef33c30..3a179f14 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -1,414 +1,200 @@
-import { Text, View, StyleSheet, Image, ActivityIndicator, TouchableOpacity, Alert, SafeAreaView } from 'react-native';
-import Qonversion, { NoCodesAction, Product } from '@qonversion/react-native-sdk';
-import { NoCodesConfigBuilder, NoCodes, QonversionConfigBuilder, LaunchMode, Environment, EntitlementsCacheLifetime } from '@qonversion/react-native-sdk';
-import React, { Component } from 'react';
-import type NoCodesError from '../../src/dto/NoCodesError';
+import React, { useReducer, useEffect } from 'react';
+import {
+ View,
+ StyleSheet,
+ TouchableOpacity,
+ Text,
+ SafeAreaView,
+ Alert,
+} from 'react-native';
+import {
+ AppContext,
+ initialState,
+ appReducer,
+ getCurrentScreen,
+} from './store/AppStore';
+import Qonversion, {
+ QonversionConfigBuilder,
+ LaunchMode,
+ Environment,
+ EntitlementsCacheLifetime,
+} from '@qonversion/react-native-sdk';
+import MainScreen from './screens/MainScreen';
+import ProductsScreen from './screens/ProductsScreen';
+import ProductDetailScreen from './screens/ProductDetailScreen';
+import EntitlementsScreen from './screens/EntitlementsScreen';
+import EntitlementDetailScreen from './screens/EntitlementDetailScreen';
+import OfferingsScreen from './screens/OfferingsScreen';
+import RemoteConfigsScreen from './screens/RemoteConfigsScreen';
+import UserScreen from './screens/UserScreen';
+import NoCodesScreen from './screens/NoCodesScreen';
+import OtherScreen from './screens/OtherScreen';
const ProjectKey = 'PV77YHL7qnGvsdmpTs7gimsxUvY-Znl2';
-const InAppProductId = 'in_app';
-const SubscriptionProductId = 'weekly';
-const NoCodeScreenContextKey = 'kamo_test';
-interface QonversionSampleState {
- inAppButtonTitle: string;
- subscriptionButtonTitle: string;
- loading: boolean;
- checkEntitlementsHidden: boolean;
- subscriptionProduct: Product | null;
- inAppProduct: Product | null;
-}
-
-export class QonversionSample extends React.PureComponent<{}, QonversionSampleState> {
- constructor(props: any) {
- super(props);
-
- this.state = {
- inAppButtonTitle: 'Loading...',
- subscriptionButtonTitle: 'Loading...',
- loading: true,
- checkEntitlementsHidden: true,
- subscriptionProduct: null,
- inAppProduct: null,
- };
-
- // eslint-disable-next-line consistent-this
- const outerClassRef = this; // necessary for anonymous classes to access this.
- const config = new QonversionConfigBuilder(ProjectKey, LaunchMode.SUBSCRIPTION_MANAGEMENT)
- .setEnvironment(Environment.SANDBOX)
- .setEntitlementsCacheLifetime(EntitlementsCacheLifetime.MONTH)
- .setEntitlementsUpdateListener({
- onEntitlementsUpdated(entitlements: any) {
- console.log('Entitlements updated!', entitlements);
- outerClassRef.handleEntitlements(entitlements);
- },
- })
- .setProxyURL("api-eu.qonversion.io")
- .build();
- Qonversion.initialize(config);
-
- // Initialize NoCodes
- const noCodesConfig = new NoCodesConfigBuilder(ProjectKey)
- .setNoCodesListener({
- onScreenShown: (id: string) => {
- console.log('No-Codes screen shown:', id);
- },
- onActionStartedExecuting: (action: NoCodesAction) => {
- console.log('No-Codes starts executing action:', action);
- },
- onActionFailedToExecute: (action: NoCodesAction) => {
- console.log('No-Codes failed to execute action:', action);
- },
- onActionFinishedExecuting: (action: NoCodesAction) => {
- console.log('No-Codes finished executing action:', action);
- },
- onFinished: () => {
- console.log('No-Codes flow finished');
- },
- onScreenFailedToLoad: (error: NoCodesError) => {
- console.log('No-Codes failed to load screen:', error);
- NoCodes.getSharedInstance().close();
- }
- })
- .build();
- NoCodes.initialize(noCodesConfig);
+// Main App Component
+const App: React.FC = () => {
+ const [state, dispatch] = useReducer(appReducer, initialState);
+
+ const loadUserInfo = async () => {
+ try {
+ console.log('🔄 [Qonversion] Starting userInfo() call...');
+ const userInfo = await Qonversion.getSharedInstance().userInfo();
+ console.log('✅ [Qonversion] userInfo() call successful:', userInfo);
+ dispatch({ type: 'SET_USER_INFO', payload: userInfo });
+ } catch (error: any) {
+ console.error('❌ [Qonversion] userInfo() call failed:', error);
+ // Don't show alert for userInfo errors as they're not critical
+ }
+ };
- Qonversion.getSharedInstance().setPromoPurchasesDelegate({
- onPromoPurchaseReceived: async (productId: any, promoPurchaseExecutor: any) => {
+ useEffect(() => {
+ // Initialize Qonversion SDK only once per session
+ if (!state.isQonversionInitialized) {
+ const initializeQonversion = async () => {
try {
- const entitlements = await promoPurchaseExecutor(productId);
- console.log('Promo purchase completed. Entitlements: ', entitlements);
- outerClassRef.handleEntitlements(entitlements);
- } catch (e) {
- console.log('Promo purchase failed.');
+ console.log('🔄 [Qonversion] Starting SDK initialization...');
+ dispatch({
+ type: 'SET_QONVERSION_INIT_STATUS',
+ payload: 'initializing',
+ });
+
+ console.log('🔄 [Qonversion] Building config...');
+ const config = new QonversionConfigBuilder(
+ ProjectKey,
+ LaunchMode.SUBSCRIPTION_MANAGEMENT
+ )
+ .setEnvironment(Environment.SANDBOX)
+ .setEntitlementsCacheLifetime(EntitlementsCacheLifetime.MONTH)
+ .setEntitlementsUpdateListener({
+ onEntitlementsUpdated(entitlements: any) {
+ console.log(
+ '📡 [Qonversion] Entitlements updated via listener:',
+ entitlements
+ );
+ dispatch({ type: 'SET_ENTITLEMENTS', payload: entitlements });
+ },
+ })
+ .setProxyURL('api-eu.qonversion.io')
+ .build();
+ console.log('✅ [Qonversion] Config built successfully:', config);
+
+ console.log('🔄 [Qonversion] Initializing SDK...');
+ Qonversion.initialize(config);
+ console.log('✅ [Qonversion] SDK initialized successfully');
+ dispatch({ type: 'SET_QONVERSION_INIT_STATUS', payload: 'success' });
+ dispatch({ type: 'SET_QONVERSION_INITIALIZED', payload: true });
+
+ // Load initial user info asynchronously
+ loadUserInfo();
+ } catch (error: any) {
+ console.error('❌ [Qonversion] SDK initialization failed:', error);
+ dispatch({ type: 'SET_QONVERSION_INIT_STATUS', payload: 'error' });
+ Alert.alert(
+ 'Initialization Error',
+ error.message || 'Failed to initialize Qonversion SDK'
+ );
}
- },
- });
- Qonversion.getSharedInstance().checkEntitlements().then((entitlements: any) => {
- this.handleEntitlements(entitlements);
- });
- }
+ };
- handleEntitlements(entitlements: any) {
- let checkActiveEntitlementsButtonHidden = this.state.checkEntitlementsHidden;
- if (entitlements.size > 0) {
- const entitlementsValues = Array.from(entitlements.values() as any[]);
- checkActiveEntitlementsButtonHidden = !entitlementsValues.some((item: any) => item.isActive === true);
+ initializeQonversion();
}
- Qonversion.getSharedInstance().products().then((products: any) => {
- let inAppTitle = this.state.inAppButtonTitle;
- let subscriptionButtonTitle = this.state.subscriptionButtonTitle;
-
- const inApp = products.get(InAppProductId);
- if (inApp) {
- inAppTitle = 'Buy for ' + inApp.prettyPrice;
-
- const entitlement = entitlements.get('Test Entitlement');
- if (entitlement) {
- inAppTitle = entitlement.isActive ? 'Purchased' : inAppTitle;
- }
- }
-
- const subscription = products.get(SubscriptionProductId);
- if (subscription) {
- subscriptionButtonTitle = 'Subscribe for '
- + subscription.prettyPrice
- + ' / '
- + subscription.subscriptionPeriod.unitCount
- + ' '
- + subscription.subscriptionPeriod.unit;
-
- const entitlement = entitlements.get('plus');
- if (entitlement) {
- subscriptionButtonTitle = entitlement.isActive ? 'Purchased' : subscriptionButtonTitle;
- }
- }
-
- this.setState({
- loading: false,
- inAppButtonTitle: inAppTitle,
- subscriptionButtonTitle: subscriptionButtonTitle,
- checkEntitlementsHidden: checkActiveEntitlementsButtonHidden,
- subscriptionProduct: subscription,
- inAppProduct: inApp,
- });
- });
- }
-
- render() {
- return (
-
-
-
- {'Build in-app\nsubscriptions'}
-
-
- {'without server code'}
-
- {this.state.loading &&
-
- }
-
- Start with 3
- days free trial
- {
- if (!this.state.subscriptionProduct) {
- Alert.alert(
- 'Error',
- 'Purchasing product not found - id ' + SubscriptionProductId,
- [
- {text: 'OK'},
- ],
- {cancelable: true}
- );
- return;
- }
- this.setState({loading: true});
- Qonversion.getSharedInstance().purchaseProduct(this.state.subscriptionProduct).then(() => {
- this.setState({loading: false, subscriptionButtonTitle: 'Purchased'});
- }).catch(error => {
- this.setState({loading: false});
-
- if (!error.userCanceled) {
- Alert.alert(
- 'Error',
- error.message,
- [
- {text: 'OK'},
- ],
- {cancelable: true}
- );
- }
- });
- }}
- >
- {this.state.subscriptionButtonTitle}
-
- {
- if (!this.state.inAppProduct) {
- Alert.alert(
- 'Error',
- 'Purchasing product not found - id ' + InAppProductId,
- [
- {text: 'OK'},
- ],
- {cancelable: true}
- );
- return;
- }
- this.setState({loading: true});
- Qonversion.getSharedInstance().purchaseProduct(this.state.inAppProduct).then(() => {
- this.setState({loading: false, inAppButtonTitle: 'Purchased'});
- }).catch(error => {
- this.setState({loading: false});
-
- if (!error.userCanceled) {
- Alert.alert(
- 'Error',
- error.message,
- [
- {text: 'OK'},
- ],
- {cancelable: true}
- );
- }
- });
- }}
- >
- {this.state.inAppButtonTitle}
-
- {
- this.setState({loading: true});
- Qonversion.getSharedInstance().restore().then((entitlements: any) => {
- this.setState({loading: false});
-
- let checkActiveEntitlementsButtonHidden = this.state.checkEntitlementsHidden;
- let inAppTitle = this.state.inAppButtonTitle;
- let subscriptionButtonTitle = this.state.subscriptionButtonTitle;
- if (entitlements.size > 0) {
- const entitlementsValues = Array.from(entitlements.values() as any[]);
- checkActiveEntitlementsButtonHidden = entitlementsValues.some((item: any) => item.isActive === true);
-
- const standartEntitlement = entitlements.get('Test Entitlement');
- if (standartEntitlement && standartEntitlement.isActive) {
- inAppTitle = 'Restored';
- }
-
- const plusEntitlement = entitlements.get('plus');
- if (plusEntitlement && plusEntitlement.isActive) {
- subscriptionButtonTitle = 'Restored';
- }
- } else {
- Alert.alert(
- 'Error',
- 'No purchases to restore',
- [
- {text: 'OK'},
- ],
- {cancelable: true}
- );
+ }, [state.isQonversionInitialized]);
+
+ const renderScreen = () => {
+ const currentScreen = getCurrentScreen(state);
+ switch (currentScreen) {
+ case 'main':
+ return ;
+ case 'products':
+ return ;
+ case 'productDetail':
+ return state.selectedProduct ? (
+
+ ) : (
+
+ );
+ case 'entitlements':
+ return ;
+ case 'entitlementDetail':
+ return state.selectedEntitlement ? (
+
+ ) : (
+
+ );
+ case 'offerings':
+ return ;
+ case 'remoteConfigs':
+ return ;
+ case 'user':
+ return ;
+ case 'noCodes':
+ return ;
+ case 'other':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+
+ {getCurrentScreen(state) !== 'main' && (
+ {
+ if (getCurrentScreen(state) === 'productDetail') {
+ dispatch({ type: 'SET_SELECTED_PRODUCT', payload: null });
}
- this.setState({
- inAppButtonTitle: inAppTitle,
- subscriptionButtonTitle: subscriptionButtonTitle,
- checkEntitlementsHidden: checkActiveEntitlementsButtonHidden,
- });
- }).catch(error => {
- this.setState({loading: false});
- Alert.alert(
- 'Error',
- error.message,
- [
- {text: 'OK'},
- ],
- {cancelable: true}
- );
- });
- }}
- >
- Restore purchases
-
- {
- if (this.state.checkEntitlementsHidden) {
- return;
- }
- this.setState({loading: true});
- Qonversion.getSharedInstance().checkEntitlements().then((entitlements: any) => {
- this.setState({loading: false});
- if (entitlements.size > 0) {
- const entitlementsValues = Array.from(entitlements.values() as any[]);
- const activeEntitlements = entitlementsValues.filter((item: any) => item.isActive === true);
- if (activeEntitlements.length > 0) {
- Alert.alert(
- 'Active entitlements',
- activeEntitlements.map((item: any) => item.entitlementId).join('\n'),
- [
- {text: 'OK'},
- ],
- {cancelable: true}
- );
- } else {
- Alert.alert(
- 'Active entitlements',
- 'No active entitlements',
- [
- {text: 'OK'},
- ],
- {cancelable: true}
- );
- }
- } else {
- Alert.alert(
- 'Active entitlements',
- 'No entitlements',
- [
- {text: 'OK'},
- ],
- {cancelable: true}
- );
+ if (getCurrentScreen(state) === 'entitlementDetail') {
+ dispatch({ type: 'SET_SELECTED_ENTITLEMENT', payload: null });
}
- }).catch(error => {
- this.setState({loading: false});
- Alert.alert(
- 'Error',
- error.message,
- [
- {text: 'OK'},
- ],
- {cancelable: true}
- );
- });
- }}
- >
- Check active entitlements
-
- {
- NoCodes.getSharedInstance().showScreen(NoCodeScreenContextKey as any)
- }}
- >
- Show NoCodes Screen
-
+ dispatch({ type: 'POP_SCREEN' });
+ }}
+ >
+ ← Back
+
+ )}
+
+ {getCurrentScreen(state) === 'main'
+ ? 'Qonversion SDK Demo'
+ : getCurrentScreen(state).charAt(0).toUpperCase() +
+ getCurrentScreen(state).slice(1)}
+
-
- );
- }
-}
+ {renderScreen()}
+
+
+ );
+};
const styles = StyleSheet.create({
- subscriptionButton: {
- backgroundColor: '#0f0f0f',
- borderRadius: 8,
- marginHorizontal: 16,
- marginTop: 16,
- height: 56,
- justifyContent: 'center',
- alignItems: 'center',
+ safeArea: {
+ flex: 1,
+ backgroundColor: '#f5f5f5',
},
- inAppButton: {
- backgroundColor: '#ffffff',
- borderRadius: 8,
- marginHorizontal: 16,
- marginTop: 16,
- height: 56,
- justifyContent: 'center',
+ header: {
+ flexDirection: 'row',
alignItems: 'center',
- borderWidth: 1,
- borderColor: '#0f0f0f',
- },
- additionalButton: {
+ padding: 16,
backgroundColor: '#ffffff',
- borderRadius: 8,
- marginHorizontal: 16,
- marginTop: 16,
- height: 56,
- justifyContent: 'center',
- alignItems: 'center',
- borderWidth: 1,
- borderColor: '#0f0f0f',
+ borderBottomWidth: 1,
+ borderBottomColor: '#e0e0e0',
},
- buttonTitle: {
- color: '#ffffff',
- fontSize: 17,
- fontWeight: '600',
+ backButton: {
+ marginRight: 16,
},
- inAppButtonTitle: {
- color: '#0f0f0f',
- fontSize: 17,
- fontWeight: '600',
+ backButtonText: {
+ fontSize: 16,
+ color: '#007AFF',
},
- additionalButtonsText: {
- color: '#0f0f0f',
- fontSize: 17,
- fontWeight: '600',
+ headerTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: '#000000',
},
});
-export default class App extends Component {
- render() {
- return (
- <>
-
-
-
- >
- );
- }
-}
+export default App;
diff --git a/example/src/components/ProductCard/index.tsx b/example/src/components/ProductCard/index.tsx
new file mode 100644
index 00000000..dec379cb
--- /dev/null
+++ b/example/src/components/ProductCard/index.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { Text, TouchableOpacity } from 'react-native';
+import { Product } from '@qonversion/react-native-sdk';
+import { AppContext } from '../../store/AppStore';
+import styles from './styles';
+
+interface ProductCardProps {
+ product: Product;
+}
+
+const ProductCard: React.FC = ({ product }) => {
+ const context = React.useContext(AppContext);
+ if (!context) return null;
+ const { dispatch } = context;
+
+ const handlePress = () => {
+ dispatch({ type: 'SET_SELECTED_PRODUCT', payload: product });
+ dispatch({ type: 'PUSH_SCREEN', payload: 'productDetail' });
+ };
+
+ return (
+
+ {product.qonversionId}
+ Store ID: {product.storeId}
+
+ Base Plan ID: {product.basePlanId}
+
+ Type: {product.type}
+
+ Price: {product.prettyPrice || 'N/A'}
+
+
+ );
+};
+
+export default ProductCard;
diff --git a/example/src/components/ProductCard/styles.ts b/example/src/components/ProductCard/styles.ts
new file mode 100644
index 00000000..4f337780
--- /dev/null
+++ b/example/src/components/ProductCard/styles.ts
@@ -0,0 +1,28 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ listItem: {
+ backgroundColor: '#ffffff',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 12,
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ borderWidth: 1,
+ borderColor: '#e0e0e0',
+ },
+ listItemTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginBottom: 4,
+ color: '#000000',
+ },
+ listItemSubtitle: {
+ fontSize: 14,
+ color: '#666666',
+ marginBottom: 4,
+ },
+});
diff --git a/example/src/components/SkeletonLoader.tsx b/example/src/components/SkeletonLoader.tsx
new file mode 100644
index 00000000..7c476677
--- /dev/null
+++ b/example/src/components/SkeletonLoader.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { View, StyleSheet } from 'react-native';
+
+const SkeletonLoader: React.FC = () => (
+
+
+
+
+
+);
+
+const styles = StyleSheet.create({
+ skeletonContainer: {
+ flex: 1,
+ padding: 16,
+ },
+ skeletonItem: {
+ height: 60,
+ backgroundColor: '#e0e0e0',
+ borderRadius: 8,
+ marginBottom: 12,
+ },
+});
+
+export default SkeletonLoader;
diff --git a/example/src/components/UserInfoSection/index.tsx b/example/src/components/UserInfoSection/index.tsx
new file mode 100644
index 00000000..ed3eeda9
--- /dev/null
+++ b/example/src/components/UserInfoSection/index.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { Text, View, TouchableOpacity } from 'react-native';
+import Clipboard from '@react-native-clipboard/clipboard';
+import Snackbar from 'react-native-snackbar';
+import { User } from '@qonversion/react-native-sdk';
+import styles from './styles';
+
+interface UserInfoSectionProps {
+ userInfo: User | null;
+ title?: string;
+ showCopyButtons?: boolean;
+}
+
+const UserInfoSection: React.FC = ({
+ userInfo,
+ title = 'User Information:',
+ showCopyButtons = true,
+}) => {
+ if (!userInfo) {
+ return null;
+ }
+
+ const copyToClipboard = (text: string, label: string) => {
+ Clipboard.setString(text);
+ Snackbar.show({
+ text: `${label} copied to clipboard`,
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ };
+
+ return (
+
+ {title}
+
+ {showCopyButtons ? (
+ <>
+
+ copyToClipboard(userInfo?.qonversionId || '', 'Qonversion ID')
+ }
+ >
+ Qonversion ID:
+
+ {userInfo?.qonversionId || 'Not available'}
+
+
+
+ copyToClipboard(userInfo?.identityId || '', 'Identity ID')
+ }
+ >
+ Identity ID:
+
+ {userInfo?.identityId || 'Anonymous'}
+
+
+ >
+ ) : (
+ <>
+
+ ID: {userInfo?.identityId || 'Anonymous'}
+
+
+ Qonversion ID: {userInfo?.qonversionId || 'Not available'}
+
+ >
+ )}
+
+ );
+};
+
+export default UserInfoSection;
diff --git a/example/src/components/UserInfoSection/styles.ts b/example/src/components/UserInfoSection/styles.ts
new file mode 100644
index 00000000..4d07e863
--- /dev/null
+++ b/example/src/components/UserInfoSection/styles.ts
@@ -0,0 +1,47 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ userInfoContainer: {
+ backgroundColor: '#ffffff',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 20,
+ },
+ userInfoTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ color: '#000000',
+ },
+ userInfoText: {
+ fontSize: 14,
+ color: '#666666',
+ marginBottom: 4,
+ },
+ userInfoRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: 8,
+ paddingHorizontal: 12,
+ backgroundColor: '#f8f9fa',
+ borderRadius: 6,
+ marginBottom: 8,
+ width: '100%',
+ },
+ userInfoLabel: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#333333',
+ flex: 1,
+ },
+ userInfoValue: {
+ fontSize: 14,
+ color: '#007AFF',
+ fontWeight: '500',
+ flex: 2,
+ textAlign: 'right',
+ marginRight: 8,
+ flexWrap: 'wrap',
+ },
+});
diff --git a/example/src/index.ts b/example/src/index.ts
new file mode 100644
index 00000000..c8aa9dec
--- /dev/null
+++ b/example/src/index.ts
@@ -0,0 +1,21 @@
+// Store exports
+export {
+ AppContext,
+ initialState,
+ appReducer,
+ type AppState,
+ type AppAction,
+} from './store/AppStore';
+
+// Component exports
+export { default as SkeletonLoader } from './components/SkeletonLoader';
+
+// Screen exports
+export { default as MainScreen } from './screens/MainScreen';
+export { default as ProductsScreen } from './screens/ProductsScreen';
+export { default as EntitlementsScreen } from './screens/EntitlementsScreen';
+export { default as OfferingsScreen } from './screens/OfferingsScreen';
+export { default as RemoteConfigsScreen } from './screens/RemoteConfigsScreen';
+export { default as UserScreen } from './screens/UserScreen';
+export { default as NoCodesScreen } from './screens/NoCodesScreen';
+export { default as OtherScreen } from './screens/OtherScreen';
diff --git a/example/src/screens/EntitlementDetailScreen/index.tsx b/example/src/screens/EntitlementDetailScreen/index.tsx
new file mode 100644
index 00000000..d4ba1c36
--- /dev/null
+++ b/example/src/screens/EntitlementDetailScreen/index.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import { Text, View, ScrollView } from 'react-native';
+import styles from './styles';
+
+interface EntitlementDetailScreenProps {
+ entitlement: any;
+}
+
+const EntitlementDetailScreen: React.FC = ({
+ entitlement,
+}) => {
+ const formatDate = (date: Date) => {
+ return date.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ });
+ };
+
+ const renderField = (label: string, value: any) => {
+ return (
+
+ {label}:
+
+ {value !== null && value !== undefined ? String(value) : '-'}
+
+
+ );
+ };
+
+ const renderDateField = (label: string, date: Date | undefined) => {
+ return (
+
+ {label}:
+ {date ? formatDate(date) : '-'}
+
+ );
+ };
+
+ return (
+
+
+ {entitlement.id}
+
+
+ {entitlement.isActive ? 'Active' : 'Inactive'}
+
+
+
+
+
+ Basic Information
+ {renderField('ID', entitlement.id)}
+ {renderField('Product ID', entitlement.productId)}
+ {renderField('Renew State', entitlement.renewState)}
+ {renderField('Source', entitlement.source)}
+ {renderField('Grant Type', entitlement.grantType)}
+ {renderField('Renews Count', entitlement.renewsCount)}
+
+
+
+ Dates
+ {renderDateField('Started Date', entitlement.startedDate)}
+ {renderDateField('Expiration Date', entitlement.expirationDate)}
+ {renderDateField('Trial Start Date', entitlement.trialStartDate)}
+ {renderDateField('First Purchase Date', entitlement.firstPurchaseDate)}
+ {renderDateField('Last Purchase Date', entitlement.lastPurchaseDate)}
+ {renderDateField(
+ 'Auto Renew Disable Date',
+ entitlement.autoRenewDisableDate
+ )}
+
+
+
+ Additional Information
+ {renderField(
+ 'Last Activated Offer Code',
+ entitlement.lastActivatedOfferCode
+ )}
+ {entitlement.transactions && entitlement.transactions.length > 0 && (
+
+ Transactions:
+
+ {entitlement.transactions.length} transaction(s)
+
+
+ )}
+
+
+ );
+};
+
+export default EntitlementDetailScreen;
diff --git a/example/src/screens/EntitlementDetailScreen/styles.ts b/example/src/screens/EntitlementDetailScreen/styles.ts
new file mode 100644
index 00000000..89131a85
--- /dev/null
+++ b/example/src/screens/EntitlementDetailScreen/styles.ts
@@ -0,0 +1,87 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#f5f5f5',
+ },
+ contentContainer: {
+ padding: 16,
+ paddingBottom: 32,
+ },
+ header: {
+ backgroundColor: '#ffffff',
+ padding: 20,
+ borderRadius: 12,
+ marginBottom: 16,
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: '#000000',
+ flex: 1,
+ },
+ statusBadge: {
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ borderRadius: 16,
+ marginLeft: 12,
+ },
+ statusActive: {
+ backgroundColor: '#34C759',
+ },
+ statusInactive: {
+ backgroundColor: '#FF3B30',
+ },
+ statusText: {
+ color: '#ffffff',
+ fontSize: 12,
+ fontWeight: '600',
+ },
+ section: {
+ backgroundColor: '#ffffff',
+ padding: 16,
+ borderRadius: 12,
+ marginBottom: 16,
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: '#000000',
+ marginBottom: 12,
+ },
+ fieldContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ paddingVertical: 8,
+ borderBottomWidth: 1,
+ borderBottomColor: '#f0f0f0',
+ },
+ fieldLabel: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#666666',
+ flex: 1,
+ },
+ fieldValue: {
+ fontSize: 14,
+ color: '#000000',
+ flex: 2,
+ textAlign: 'right',
+ flexWrap: 'wrap',
+ },
+});
diff --git a/example/src/screens/EntitlementsScreen/index.tsx b/example/src/screens/EntitlementsScreen/index.tsx
new file mode 100644
index 00000000..14286a3c
--- /dev/null
+++ b/example/src/screens/EntitlementsScreen/index.tsx
@@ -0,0 +1,250 @@
+import React from 'react';
+import {
+ Text,
+ View,
+ TouchableOpacity,
+ Alert,
+ ScrollView,
+ Platform,
+} from 'react-native';
+import Qonversion, { Entitlement } from '@qonversion/react-native-sdk';
+import Snackbar from 'react-native-snackbar';
+import { AppContext } from '../../store/AppStore';
+import SkeletonLoader from '../../components/SkeletonLoader';
+import styles from './styles';
+
+const EntitlementsScreen: React.FC = () => {
+ const context = React.useContext(AppContext);
+ if (!context) return null;
+ const { state, dispatch } = context;
+
+ const loadEntitlements = async () => {
+ try {
+ console.log('🔄 [Qonversion] Starting checkEntitlements() call...');
+ dispatch({ type: 'SET_LOADING', payload: true });
+ const entitlements =
+ await Qonversion.getSharedInstance().checkEntitlements();
+ console.log(
+ '✅ [Qonversion] checkEntitlements() call successful:',
+ entitlements
+ );
+ dispatch({ type: 'SET_ENTITLEMENTS', payload: entitlements });
+ } catch (error: any) {
+ console.error('❌ [Qonversion] checkEntitlements() call failed:', error);
+ Alert.alert('Error', error.message);
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: false });
+ }
+ };
+
+ const setEntitlementsListener = () => {
+ console.log('🔄 [Qonversion] Setting entitlements update listener...');
+ Qonversion.getSharedInstance().setEntitlementsUpdateListener({
+ onEntitlementsUpdated(entitlements: Map) {
+ console.log(
+ '📡 [Qonversion] Entitlements updated via listener:',
+ Object.fromEntries(entitlements)
+ );
+ dispatch({ type: 'SET_ENTITLEMENTS', payload: entitlements });
+ },
+ });
+ console.log(
+ '✅ [Qonversion] Entitlements update listener set successfully'
+ );
+ Snackbar.show({
+ text: 'Entitlements listener set successfully!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ };
+
+ const restore = async () => {
+ try {
+ console.log('🔄 [Qonversion] Starting restore() call...');
+ dispatch({ type: 'SET_LOADING', payload: true });
+ const entitlements = await Qonversion.getSharedInstance().restore();
+ console.log('✅ [Qonversion] restore() call successful:', entitlements);
+ dispatch({ type: 'SET_ENTITLEMENTS', payload: entitlements });
+ Snackbar.show({
+ text: 'Purchases restored successfully!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ console.error('❌ [Qonversion] restore() call failed:', error);
+ Alert.alert('Error', error.message);
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: false });
+ }
+ };
+
+ const syncHistoricalData = async () => {
+ try {
+ console.log('🔄 [Qonversion] Starting syncHistoricalData() call...');
+ dispatch({ type: 'SET_LOADING', payload: true });
+ Qonversion.getSharedInstance().syncHistoricalData();
+ console.log('✅ [Qonversion] syncHistoricalData() call successful');
+ Snackbar.show({
+ text: 'Historical data synced successfully!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ console.error('❌ [Qonversion] syncHistoricalData() call failed:', error);
+ Alert.alert('Error', error.message);
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: false });
+ }
+ };
+
+ const syncStoreKit2Purchases = async () => {
+ if (Platform.OS !== 'ios') {
+ Alert.alert('Error', 'This method is iOS only');
+ return;
+ }
+ try {
+ Qonversion.getSharedInstance().syncStoreKit2Purchases();
+ Snackbar.show({
+ text: 'StoreKit 2 purchases synced!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ Alert.alert('Error', error.message);
+ }
+ };
+
+ const syncPurchases = async () => {
+ if (Platform.OS !== 'android') {
+ Alert.alert('Error', 'This method is Android only');
+ return;
+ }
+ try {
+ Qonversion.getSharedInstance().syncPurchases();
+ Snackbar.show({
+ text: 'Purchases synced!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ Alert.alert('Error', error.message);
+ }
+ };
+
+ const formatDate = (date: Date) => {
+ return date.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ };
+
+ const renderContent = () => {
+ return (
+
+
+ Load Entitlements
+
+
+
+
+ Set Entitlements Updated Listener
+
+
+
+
+ Restore
+
+
+
+ Sync Historical Data
+
+
+
+
+ Sync StoreKit 2 Purchases (iOS Only)
+
+
+
+
+ Sync Purchases (Android Only)
+
+
+ {/* Entitlements Status Section */}
+
+ {state.loading ? (
+ // Loading state - show skeleton only for entitlements section
+
+ Your Entitlements
+
+
+ ) : state.entitlements === null ? (
+ // Not loaded state
+
+ No Entitlements Loaded
+
+ Tap "Load Entitlements" to fetch your current entitlements from
+ the server.
+
+
+ ) : state.entitlements.size === 0 ? (
+ // Empty entitlements state
+
+ No Entitlements Found
+
+ You don't have any active entitlements. Try restoring purchases
+ or check your subscription status.
+
+
+ ) : (
+ // Entitlements list
+
+ Your Entitlements
+ {Array.from(state.entitlements.entries()).map(
+ ([id, entitlement]) => (
+ {
+ dispatch({
+ type: 'SET_SELECTED_ENTITLEMENT',
+ payload: entitlement,
+ });
+ dispatch({
+ type: 'PUSH_SCREEN',
+ payload: 'entitlementDetail',
+ });
+ }}
+ >
+ {entitlement.id}
+
+ Status: {entitlement.isActive ? 'Active' : 'Inactive'}
+
+
+ Started: {formatDate(entitlement.startedDate)}
+
+ {entitlement.expirationDate && (
+
+ Expires: {formatDate(entitlement.expirationDate)}
+
+ )}
+
+ )
+ )}
+
+ )}
+
+
+ );
+ };
+
+ return renderContent();
+};
+
+export default EntitlementsScreen;
diff --git a/example/src/screens/EntitlementsScreen/styles.ts b/example/src/screens/EntitlementsScreen/styles.ts
new file mode 100644
index 00000000..bab9a169
--- /dev/null
+++ b/example/src/screens/EntitlementsScreen/styles.ts
@@ -0,0 +1,82 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#f5f5f5',
+ },
+ contentContainer: {
+ padding: 16,
+ paddingBottom: 32,
+ },
+ button: {
+ backgroundColor: '#007AFF',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 12,
+ alignItems: 'center',
+ },
+ buttonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ listContainer: {
+ marginTop: 20,
+ },
+ listItem: {
+ backgroundColor: '#ffffff',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 12,
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ },
+ listItemTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginBottom: 4,
+ color: '#000000',
+ },
+ listItemSubtitle: {
+ fontSize: 14,
+ color: '#666666',
+ marginBottom: 4,
+ },
+ statusContainer: {
+ marginTop: 20,
+ },
+ emptyStateContainer: {
+ backgroundColor: '#ffffff',
+ padding: 24,
+ borderRadius: 12,
+ alignItems: 'center',
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ },
+ emptyStateTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: '#000000',
+ marginBottom: 8,
+ textAlign: 'center',
+ },
+ emptyStateText: {
+ fontSize: 14,
+ color: '#666666',
+ textAlign: 'center',
+ lineHeight: 20,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: '#000000',
+ marginBottom: 12,
+ },
+});
diff --git a/example/src/screens/MainScreen/index.tsx b/example/src/screens/MainScreen/index.tsx
new file mode 100644
index 00000000..83f2dd13
--- /dev/null
+++ b/example/src/screens/MainScreen/index.tsx
@@ -0,0 +1,116 @@
+import React from 'react';
+import { Text, View, Image, TouchableOpacity, ScrollView } from 'react-native';
+import Clipboard from '@react-native-clipboard/clipboard';
+import Snackbar from 'react-native-snackbar';
+import { AppContext } from '../../store/AppStore';
+import styles from './styles';
+
+const MainScreen: React.FC = () => {
+ const context = React.useContext(AppContext);
+ if (!context) {
+ console.error('AppContext is null in MainScreen');
+ return (
+
+ Error: AppContext not available
+
+ );
+ }
+
+ const { state, dispatch } = context;
+
+ const menuItems = [
+ { id: 'products', title: 'Products' },
+ { id: 'entitlements', title: 'Entitlements' },
+ { id: 'offerings', title: 'Offerings' },
+ { id: 'remoteConfigs', title: 'Remote Configs' },
+ { id: 'user', title: 'User' },
+ { id: 'noCodes', title: 'No-Codes' },
+ { id: 'other', title: 'Other' },
+ ];
+
+ return (
+
+
+ Qonversion SDK Demo
+
+
+
+
+ {state.qonversionInitStatus === 'not_initialized' &&
+ 'Initialization not completed'}
+ {state.qonversionInitStatus === 'initializing' && 'Initializing...'}
+ {state.qonversionInitStatus === 'success' &&
+ 'Initialization successful'}
+ {state.qonversionInitStatus === 'error' && 'Initialization error'}
+
+
+
+ {state.userInfo && (
+
+ Current User:
+ {
+ Clipboard.setString(state.userInfo?.qonversionId || '');
+ Snackbar.show({
+ text: 'Qonversion ID copied to clipboard',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ }}
+ >
+ Qonversion ID:
+
+ {state.userInfo?.qonversionId || 'Not available'}
+
+
+ {
+ Clipboard.setString(state.userInfo?.identityId || '');
+ Snackbar.show({
+ text: 'Identity ID copied to clipboard',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ }}
+ >
+ Identity ID:
+
+ {state.userInfo?.identityId || 'Anonymous'}
+
+
+
+ )}
+
+
+ {menuItems.map((item) => (
+ dispatch({ type: 'PUSH_SCREEN', payload: item.id })}
+ >
+ {item.title}
+
+ ))}
+
+
+ );
+};
+
+export default MainScreen;
diff --git a/example/src/screens/MainScreen/styles.ts b/example/src/screens/MainScreen/styles.ts
new file mode 100644
index 00000000..33563426
--- /dev/null
+++ b/example/src/screens/MainScreen/styles.ts
@@ -0,0 +1,135 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 16,
+ },
+ contentContainer: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingBottom: 32,
+ },
+ logo: {
+ width: 100,
+ height: 100,
+ alignSelf: 'center',
+ marginVertical: 20,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ textAlign: 'center',
+ marginBottom: 20,
+ color: '#000000',
+ },
+ userInfoContainer: {
+ backgroundColor: '#ffffff',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 20,
+ },
+ userInfoTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ color: '#000000',
+ },
+ userInfoText: {
+ fontSize: 14,
+ color: '#666666',
+ marginBottom: 4,
+ },
+ menuContainer: {
+ marginTop: 20,
+ width: '100%',
+ },
+ menuItem: {
+ backgroundColor: '#ffffff',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 12,
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ },
+ menuItemText: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#000000',
+ },
+ errorText: {
+ fontSize: 14,
+ color: '#FF3B30',
+ textAlign: 'center',
+ marginBottom: 20,
+ },
+ button: {
+ backgroundColor: '#007AFF',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 12,
+ alignItems: 'center',
+ minWidth: 120,
+ },
+ buttonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ initIndicatorContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: 20,
+ paddingHorizontal: 16,
+ },
+ initIndicator: {
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ marginRight: 8,
+ },
+ initIndicatorGray: {
+ backgroundColor: '#999999',
+ },
+ initIndicatorGreen: {
+ backgroundColor: '#34C759',
+ },
+ initIndicatorRed: {
+ backgroundColor: '#FF3B30',
+ },
+ initIndicatorText: {
+ fontSize: 14,
+ color: '#666666',
+ fontWeight: '500',
+ },
+ userInfoRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: 8,
+ paddingHorizontal: 12,
+ backgroundColor: '#f8f9fa',
+ borderRadius: 6,
+ marginBottom: 8,
+ width: '100%',
+ },
+ userInfoLabel: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#333333',
+ flex: 1,
+ },
+ userInfoValue: {
+ fontSize: 14,
+ color: '#007AFF',
+ fontWeight: '500',
+ flex: 2,
+ textAlign: 'right',
+ marginRight: 8,
+ flexWrap: 'wrap',
+ },
+});
diff --git a/example/src/screens/NoCodesScreen/index.tsx b/example/src/screens/NoCodesScreen/index.tsx
new file mode 100644
index 00000000..fb97c837
--- /dev/null
+++ b/example/src/screens/NoCodesScreen/index.tsx
@@ -0,0 +1,208 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Text,
+ View,
+ TouchableOpacity,
+ Alert,
+ ScrollView,
+ TextInput,
+ Platform,
+} from 'react-native';
+import {
+ NoCodesAction,
+ NoCodesConfigBuilder,
+ ScreenPresentationStyle,
+ ScreenPresentationConfig,
+ NoCodes,
+} from '@qonversion/react-native-sdk';
+import type NoCodesError from '../../../../src/dto/NoCodesError';
+import { AppContext } from '../../store/AppStore';
+import styles from './styles';
+import Snackbar from 'react-native-snackbar';
+
+const ProjectKey = 'PV77YHL7qnGvsdmpTs7gimsxUvY-Znl2';
+
+const NoCodesScreen: React.FC = () => {
+ const context = React.useContext(AppContext);
+ if (!context) return null;
+ const { state, dispatch } = context;
+
+ const [contextKey, setContextKey] = useState('kamo_test');
+ const [presentationStyle, setPresentationStyle] = useState(
+ ScreenPresentationStyle.FULL_SCREEN
+ );
+ const [animated, setAnimated] = useState(false);
+
+ useEffect(() => {
+ // Initialize No-Codes SDK once
+ const initializeNoCodes = () => {
+ console.log('🔄 [NoCodes] Starting SDK initialization...');
+ const noCodesConfig = new NoCodesConfigBuilder(ProjectKey)
+ .setNoCodesListener({
+ onScreenShown: (id: string) => {
+ const event = `Screen shown: ${id}`;
+ console.log('📡 [NoCodes] Screen shown event:', id);
+ dispatch({ type: 'ADD_NOCODES_EVENT', payload: event });
+ },
+ onActionStartedExecuting: (action: NoCodesAction) => {
+ const event = `Action started: ${action.type}`;
+ console.log('📡 [NoCodes] Action started event:', action);
+ dispatch({ type: 'ADD_NOCODES_EVENT', payload: event });
+ },
+ onActionFailedToExecute: (action: NoCodesAction) => {
+ const event = `Action failed: ${action.type}`;
+ console.log('📡 [NoCodes] Action failed event:', action);
+ dispatch({ type: 'ADD_NOCODES_EVENT', payload: event });
+ },
+ onActionFinishedExecuting: (action: NoCodesAction) => {
+ const event = `Action finished: ${action.type}`;
+ console.log('📡 [NoCodes] Action finished event:', action);
+ dispatch({ type: 'ADD_NOCODES_EVENT', payload: event });
+ },
+ onFinished: () => {
+ const event = 'Flow finished';
+ console.log('📡 [NoCodes] Flow finished event');
+ dispatch({ type: 'ADD_NOCODES_EVENT', payload: event });
+ },
+ onScreenFailedToLoad: (error: NoCodesError) => {
+ const event = `Screen failed to load: ${error.description || error.code}`;
+ console.log('📡 [NoCodes] Screen failed to load event:', error);
+ dispatch({ type: 'ADD_NOCODES_EVENT', payload: event });
+ NoCodes.getSharedInstance().close();
+ },
+ })
+ .build();
+ console.log('✅ [NoCodes] Config built successfully:', noCodesConfig);
+
+ console.log('🔄 [NoCodes] Initializing SDK...');
+ NoCodes.initialize(noCodesConfig);
+ console.log('✅ [NoCodes] SDK initialized successfully');
+ };
+
+ initializeNoCodes();
+ }, []);
+
+ const showScreen = () => {
+ try {
+ console.log(
+ '🔄 [NoCodes] Starting showScreen() call with contextKey:',
+ contextKey
+ );
+ NoCodes.getSharedInstance().showScreen(contextKey);
+ console.log('✅ [NoCodes] showScreen() call successful');
+ } catch (error: any) {
+ console.error('❌ [NoCodes] showScreen() call failed:', error);
+ Alert.alert('Error', error.message);
+ }
+ };
+
+ const setPresentationConfig = () => {
+ try {
+ console.log(
+ '🔄 [NoCodes] Starting setScreenPresentationConfig() call with contextKey:',
+ contextKey,
+ 'config:',
+ { presentationStyle, animated }
+ );
+ const config = new ScreenPresentationConfig(presentationStyle, animated);
+
+ NoCodes.getSharedInstance().setScreenPresentationConfig(
+ config,
+ contextKey
+ );
+ console.log('✅ [NoCodes] setScreenPresentationConfig() call successful');
+ Snackbar.show({
+ text: 'Presentation config set successfully!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ console.error(
+ '❌ [NoCodes] setScreenPresentationConfig() call failed:',
+ error
+ );
+ Alert.alert('Error', error.message);
+ }
+ };
+
+ const close = () => {
+ try {
+ console.log('🔄 [NoCodes] Starting close() call...');
+ NoCodes.getSharedInstance().close();
+ console.log('✅ [NoCodes] close() call successful');
+ Snackbar.show({
+ text: 'No-Codes screen closed!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ console.error('❌ [NoCodes] close() call failed:', error);
+ Alert.alert('Error', error.message);
+ }
+ };
+
+ return (
+
+
+ Context Key:
+
+
+ Show No-Code Screen
+
+
+
+
+ Presentation Config
+ Screen Presentation Style:
+ {Object.values(ScreenPresentationStyle).map((style) => (
+ setPresentationStyle(style)}
+ >
+ {style}
+
+ ))}
+
+ {Platform.OS === 'ios' && (
+ setAnimated(!animated)}
+ >
+ Animated (iOS only)
+
+ )}
+
+
+ Set Presentation Config
+
+
+
+
+ Close
+
+
+
+ No-Codes Events:
+
+ {state.noCodesEvents.map((event, index) => (
+
+ {event}
+
+ ))}
+
+
+
+ );
+};
+
+export default NoCodesScreen;
diff --git a/example/src/screens/NoCodesScreen/styles.ts b/example/src/screens/NoCodesScreen/styles.ts
new file mode 100644
index 00000000..440c10c4
--- /dev/null
+++ b/example/src/screens/NoCodesScreen/styles.ts
@@ -0,0 +1,98 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 16,
+ paddingBottom: 32,
+ },
+ contentContainer: {
+ paddingBottom: 32,
+ },
+ inputContainer: {
+ marginBottom: 20,
+ },
+ inputLabel: {
+ fontSize: 14,
+ fontWeight: '600',
+ marginBottom: 8,
+ color: '#000000',
+ },
+ textInput: {
+ backgroundColor: '#ffffff',
+ borderWidth: 1,
+ borderColor: '#e0e0e0',
+ borderRadius: 8,
+ padding: 12,
+ marginBottom: 12,
+ fontSize: 16,
+ },
+ button: {
+ backgroundColor: '#007AFF',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 12,
+ alignItems: 'center',
+ },
+ buttonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ marginBottom: 12,
+ color: '#000000',
+ },
+ radioButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 12,
+ backgroundColor: '#ffffff',
+ borderRadius: 8,
+ marginBottom: 8,
+ borderWidth: 1,
+ borderColor: '#e0e0e0',
+ },
+ radioButtonSelected: {
+ borderColor: '#007AFF',
+ backgroundColor: '#f0f8ff',
+ },
+ radioButtonText: {
+ fontSize: 14,
+ color: '#000000',
+ },
+ checkbox: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 12,
+ backgroundColor: '#ffffff',
+ borderRadius: 8,
+ marginBottom: 8,
+ borderWidth: 1,
+ borderColor: '#e0e0e0',
+ },
+ checkboxSelected: {
+ borderColor: '#007AFF',
+ backgroundColor: '#f0f8ff',
+ },
+ checkboxText: {
+ fontSize: 14,
+ color: '#000000',
+ },
+ eventsContainer: {
+ marginTop: 20,
+ },
+ eventsList: {
+ maxHeight: 200,
+ backgroundColor: '#ffffff',
+ borderRadius: 8,
+ padding: 12,
+ },
+ eventText: {
+ fontSize: 12,
+ color: '#666666',
+ marginBottom: 4,
+ },
+});
diff --git a/example/src/screens/OfferingsScreen/index.tsx b/example/src/screens/OfferingsScreen/index.tsx
new file mode 100644
index 00000000..d4eea491
--- /dev/null
+++ b/example/src/screens/OfferingsScreen/index.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import { Text, View, TouchableOpacity, Alert, ScrollView } from 'react-native';
+import Qonversion from '@qonversion/react-native-sdk';
+import { AppContext } from '../../store/AppStore';
+import SkeletonLoader from '../../components/SkeletonLoader';
+import ProductCard from '../../components/ProductCard';
+import styles from './styles';
+
+const OfferingsScreen: React.FC = () => {
+ const context = React.useContext(AppContext);
+ if (!context) return null;
+ const { state, dispatch } = context;
+
+ const loadOfferings = async () => {
+ try {
+ console.log('🔄 [Qonversion] Starting offerings() call...');
+ dispatch({ type: 'SET_LOADING', payload: true });
+ const offerings = await Qonversion.getSharedInstance().offerings();
+ console.log('✅ [Qonversion] offerings() call successful:', offerings);
+ if (offerings) {
+ dispatch({ type: 'SET_OFFERINGS', payload: offerings });
+ }
+ } catch (error: any) {
+ console.error('❌ [Qonversion] offerings() call failed:', error);
+ Alert.alert('Error', error.message);
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: false });
+ }
+ };
+
+ if (state.loading) {
+ return ;
+ }
+
+ return (
+
+
+ Load Offerings
+
+
+ {state.offerings ? (
+
+
+ Main Offering ID: {state.offerings.main?.id}
+
+
+ {state.offerings.availableOffering.map((offering) => (
+
+ {offering.id}
+ Tag: {offering.tag}
+
+ {offering.products.length > 0 ? (
+
+ Products:
+ {offering.products.map((product) => (
+
+ ))}
+
+ ) : (
+
+ No products in this offering
+
+ )}
+
+ ))}
+
+ ) : (
+
+ No Offerings Loaded
+
+ Tap the button above to load available offerings
+
+
+ )}
+
+ );
+};
+
+export default OfferingsScreen;
diff --git a/example/src/screens/OfferingsScreen/styles.ts b/example/src/screens/OfferingsScreen/styles.ts
new file mode 100644
index 00000000..f5140077
--- /dev/null
+++ b/example/src/screens/OfferingsScreen/styles.ts
@@ -0,0 +1,113 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 16,
+ paddingBottom: 32,
+ },
+ contentContainer: {
+ paddingBottom: 32,
+ },
+ button: {
+ backgroundColor: '#007AFF',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 12,
+ alignItems: 'center',
+ },
+ buttonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ listContainer: {
+ marginTop: 20,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ marginBottom: 12,
+ color: '#000000',
+ },
+ listItem: {
+ backgroundColor: '#ffffff',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 12,
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ },
+ listItemTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginBottom: 4,
+ color: '#000000',
+ },
+ listItemSubtitle: {
+ fontSize: 14,
+ color: '#666666',
+ marginBottom: 4,
+ },
+ emptyState: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingVertical: 40,
+ },
+ emptyStateTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: '#666666',
+ marginBottom: 8,
+ textAlign: 'center',
+ },
+ emptyStateSubtitle: {
+ fontSize: 14,
+ color: '#999999',
+ textAlign: 'center',
+ lineHeight: 20,
+ },
+ offeringContainer: {
+ backgroundColor: '#ffffff',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 16,
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ borderWidth: 1,
+ borderColor: '#e0e0e0',
+ },
+ offeringTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ marginBottom: 4,
+ color: '#000000',
+ },
+ offeringSubtitle: {
+ fontSize: 14,
+ color: '#666666',
+ marginBottom: 12,
+ },
+ productsContainer: {
+ marginTop: 8,
+ },
+ productsTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginBottom: 8,
+ color: '#333333',
+ },
+ noProductsText: {
+ fontSize: 14,
+ color: '#999999',
+ fontStyle: 'italic',
+ marginTop: 8,
+ },
+});
diff --git a/example/src/screens/OtherScreen/index.tsx b/example/src/screens/OtherScreen/index.tsx
new file mode 100644
index 00000000..bc2003ba
--- /dev/null
+++ b/example/src/screens/OtherScreen/index.tsx
@@ -0,0 +1,172 @@
+import React, { useState } from 'react';
+import {
+ Text,
+ View,
+ TouchableOpacity,
+ Alert,
+ ScrollView,
+ Platform,
+} from 'react-native';
+import Qonversion from '@qonversion/react-native-sdk';
+import { AppContext } from '../../store/AppStore';
+import SkeletonLoader from '../../components/SkeletonLoader';
+import styles from './styles';
+import Snackbar from 'react-native-snackbar';
+
+const OtherScreen: React.FC = () => {
+ const context = React.useContext(AppContext);
+ if (!context) return null;
+ const { state, dispatch } = context;
+
+ const [fallbackAccessible, setFallbackAccessible] = useState(
+ null
+ );
+
+ const checkFallbackFile = async () => {
+ try {
+ console.log(
+ '🔄 [Qonversion] Starting isFallbackFileAccessible() call...'
+ );
+ dispatch({ type: 'SET_LOADING', payload: true });
+ const accessible =
+ await Qonversion.getSharedInstance().isFallbackFileAccessible();
+ console.log(
+ '✅ [Qonversion] isFallbackFileAccessible() call successful:',
+ accessible
+ );
+ setFallbackAccessible(accessible);
+ } catch (error: any) {
+ console.error(
+ '❌ [Qonversion] isFallbackFileAccessible() call failed:',
+ error
+ );
+ Alert.alert('Error', error.message);
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: false });
+ }
+ };
+
+ const collectAdvertisingId = () => {
+ if (Platform.OS !== 'ios') {
+ Alert.alert('Error', 'This method is iOS only');
+ return;
+ }
+ try {
+ console.log('🔄 [Qonversion] Starting collectAdvertisingId() call...');
+ Qonversion.getSharedInstance().collectAdvertisingId();
+ console.log('✅ [Qonversion] collectAdvertisingId() call successful');
+ Snackbar.show({
+ text: 'Advertising ID collected!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ console.error(
+ '❌ [Qonversion] collectAdvertisingId() call failed:',
+ error
+ );
+ Alert.alert('Error', error.message);
+ }
+ };
+
+ const collectAppleSearchAdsAttribution = () => {
+ if (Platform.OS !== 'ios') {
+ Alert.alert('Error', 'This method is iOS only');
+ return;
+ }
+ try {
+ console.log(
+ '🔄 [Qonversion] Starting collectAppleSearchAdsAttribution() call...'
+ );
+ Qonversion.getSharedInstance().collectAppleSearchAdsAttribution();
+ console.log(
+ '✅ [Qonversion] collectAppleSearchAdsAttribution() call successful'
+ );
+ Snackbar.show({
+ text: 'Apple Search Ads attribution collected!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ console.error(
+ '❌ [Qonversion] collectAppleSearchAdsAttribution() call failed:',
+ error
+ );
+ Alert.alert('Error', error.message);
+ }
+ };
+
+ const presentCodeRedemptionSheet = () => {
+ if (Platform.OS !== 'ios') {
+ Alert.alert('Error', 'This method is iOS only');
+ return;
+ }
+ try {
+ console.log(
+ '🔄 [Qonversion] Starting presentCodeRedemptionSheet() call...'
+ );
+ Qonversion.getSharedInstance().presentCodeRedemptionSheet();
+ console.log(
+ '✅ [Qonversion] presentCodeRedemptionSheet() call successful'
+ );
+ Snackbar.show({
+ text: 'Code redemption sheet presented!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ console.error(
+ '❌ [Qonversion] presentCodeRedemptionSheet() call failed:',
+ error
+ );
+ Alert.alert('Error', error.message);
+ }
+ };
+
+ if (state.loading) {
+ return ;
+ }
+
+ return (
+
+
+ Check Fallback File Accessibility
+
+
+
+ Accessibility:
+
+
+
+
+ iOS Only Methods:
+
+ Collect Advertising ID
+
+
+
+ Collect Apple Search Ads Attribution
+
+
+
+ Present Code Redemption Sheet
+
+
+
+ );
+};
+
+export default OtherScreen;
diff --git a/example/src/screens/OtherScreen/styles.ts b/example/src/screens/OtherScreen/styles.ts
new file mode 100644
index 00000000..c8b4edfd
--- /dev/null
+++ b/example/src/screens/OtherScreen/styles.ts
@@ -0,0 +1,58 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 16,
+ paddingBottom: 32,
+ },
+ contentContainer: {
+ paddingBottom: 32,
+ },
+ button: {
+ backgroundColor: '#007AFF',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 12,
+ alignItems: 'center',
+ },
+ buttonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ indicatorContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 20,
+ },
+ indicatorLabel: {
+ fontSize: 14,
+ fontWeight: '600',
+ marginRight: 12,
+ color: '#000000',
+ },
+ indicator: {
+ width: 20,
+ height: 20,
+ borderRadius: 10,
+ },
+ indicatorGray: {
+ backgroundColor: '#cccccc',
+ },
+ indicatorGreen: {
+ backgroundColor: '#34C759',
+ },
+ indicatorRed: {
+ backgroundColor: '#FF3B30',
+ },
+ iosOnlyContainer: {
+ marginTop: 20,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ marginBottom: 12,
+ color: '#000000',
+ },
+});
diff --git a/example/src/screens/ProductDetailScreen/index.tsx b/example/src/screens/ProductDetailScreen/index.tsx
new file mode 100644
index 00000000..a398d3cf
--- /dev/null
+++ b/example/src/screens/ProductDetailScreen/index.tsx
@@ -0,0 +1,130 @@
+import React from 'react';
+import { Text, View, TouchableOpacity, Alert, ScrollView } from 'react-native';
+import Snackbar from 'react-native-snackbar';
+import Qonversion, { Product } from '@qonversion/react-native-sdk';
+import { AppContext } from '../../store/AppStore';
+import SkeletonLoader from '../../components/SkeletonLoader';
+import styles from './styles';
+
+interface ProductDetailScreenProps {
+ product: Product;
+}
+
+const ProductDetailScreen: React.FC = ({
+ product,
+}) => {
+ const context = React.useContext(AppContext);
+ if (!context) return null;
+ const { state, dispatch } = context;
+
+ const purchaseProduct = async () => {
+ try {
+ console.log(
+ '🔄 [Qonversion] Starting purchaseProduct() call with product:',
+ product
+ );
+ dispatch({ type: 'SET_LOADING', payload: true });
+ const entitlements =
+ await Qonversion.getSharedInstance().purchaseProduct(product);
+ console.log(
+ '✅ [Qonversion] purchaseProduct() call successful:',
+ entitlements
+ );
+ dispatch({ type: 'SET_ENTITLEMENTS', payload: entitlements });
+ Snackbar.show({
+ text: 'Product purchased successfully!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ console.error('❌ [Qonversion] purchaseProduct() call failed:', error);
+ if (!error.userCanceled) {
+ Alert.alert('Error', error.message);
+ }
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: false });
+ }
+ };
+
+ if (state.loading) {
+ return ;
+ }
+
+ const renderField = (label: string, value: any) => {
+ if (value === null || value === undefined) return null;
+ return (
+
+ {label}:
+ {String(value)}
+
+ );
+ };
+
+ return (
+
+
+
+ {product.storeTitle || product.qonversionId}
+
+ {product.prettyPrice && (
+ {product.prettyPrice}
+ )}
+
+
+
+ Basic Information
+ {renderField('Qonversion ID', product.qonversionId)}
+ {renderField('Store ID', product.storeId)}
+ {renderField('Base Plan ID', product.basePlanId)}
+ {renderField('Type', product.type)}
+ {renderField('Offering ID', product.offeringId)}
+
+
+
+ Pricing
+ {renderField('Pretty Price', product.prettyPrice)}
+ {renderField('Price', product.price)}
+ {renderField('Currency Code', product.currencyCode)}
+ {renderField(
+ 'Pretty Introductory Price',
+ product.prettyIntroductoryPrice
+ )}
+
+
+
+ Store Information
+ {renderField('Store Title', product.storeTitle)}
+ {renderField('Store Description', product.storeDescription)}
+
+
+
+ Subscription Details
+ {product.subscriptionPeriod && (
+
+ Subscription Period:
+
+ {product.subscriptionPeriod.unitCount}{' '}
+ {product.subscriptionPeriod.unit}
+
+
+ )}
+ {product.trialPeriod && (
+
+ Trial Period:
+
+ {product.trialPeriod.unitCount} {product.trialPeriod.unit}
+
+
+ )}
+
+
+
+ Purchase Product
+
+
+ );
+};
+
+export default ProductDetailScreen;
diff --git a/example/src/screens/ProductDetailScreen/styles.ts b/example/src/screens/ProductDetailScreen/styles.ts
new file mode 100644
index 00000000..6c6746e7
--- /dev/null
+++ b/example/src/screens/ProductDetailScreen/styles.ts
@@ -0,0 +1,59 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 16,
+ },
+ contentContainer: {
+ paddingBottom: 32,
+ },
+ header: {
+ marginBottom: 20,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ color: '#000000',
+ },
+ price: {
+ fontSize: 18,
+ color: '#007AFF',
+ fontWeight: '600',
+ },
+ section: {
+ marginBottom: 24,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ marginBottom: 12,
+ color: '#000000',
+ },
+ fieldContainer: {
+ marginBottom: 8,
+ },
+ fieldLabel: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#666666',
+ marginBottom: 2,
+ },
+ fieldValue: {
+ fontSize: 16,
+ color: '#000000',
+ },
+ purchaseButton: {
+ backgroundColor: '#007AFF',
+ padding: 16,
+ borderRadius: 8,
+ alignItems: 'center',
+ marginTop: 20,
+ },
+ purchaseButtonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+});
diff --git a/example/src/screens/ProductsScreen/index.tsx b/example/src/screens/ProductsScreen/index.tsx
new file mode 100644
index 00000000..4188d44a
--- /dev/null
+++ b/example/src/screens/ProductsScreen/index.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { Text, View, TouchableOpacity, Alert, ScrollView } from 'react-native';
+import Qonversion from '@qonversion/react-native-sdk';
+import { AppContext } from '../../store/AppStore';
+import SkeletonLoader from '../../components/SkeletonLoader';
+import ProductCard from '../../components/ProductCard';
+import styles from './styles';
+
+const ProductsScreen: React.FC = () => {
+ const context = React.useContext(AppContext);
+ if (!context) return null;
+ const { state, dispatch } = context;
+
+ const loadProducts = async () => {
+ try {
+ console.log('🔄 [Qonversion] Starting products() call...');
+ dispatch({ type: 'SET_LOADING', payload: true });
+ const products = await Qonversion.getSharedInstance().products();
+ console.log('✅ [Qonversion] products() call successful:', products);
+ dispatch({ type: 'SET_PRODUCTS', payload: products });
+ } catch (error: any) {
+ console.error('❌ [Qonversion] products() call failed:', error);
+ Alert.alert('Error', error.message);
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: false });
+ }
+ };
+
+ if (state.loading) {
+ return ;
+ }
+
+ return (
+
+
+ Load Products
+
+
+ {state.products && (
+
+ {Array.from(state.products.entries()).map(([id, product]) => (
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default ProductsScreen;
diff --git a/example/src/screens/ProductsScreen/styles.ts b/example/src/screens/ProductsScreen/styles.ts
new file mode 100644
index 00000000..0ac93214
--- /dev/null
+++ b/example/src/screens/ProductsScreen/styles.ts
@@ -0,0 +1,51 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#f5f5f5',
+ },
+ contentContainer: {
+ padding: 16,
+ paddingBottom: 32,
+ },
+ button: {
+ backgroundColor: '#007AFF',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 12,
+ alignItems: 'center',
+ },
+ buttonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ listContainer: {
+ marginTop: 20,
+ },
+ listItem: {
+ backgroundColor: '#ffffff',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 12,
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ borderWidth: 1,
+ borderColor: '#e0e0e0',
+ },
+ listItemTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginBottom: 4,
+ color: '#000000',
+ },
+ listItemSubtitle: {
+ fontSize: 14,
+ color: '#666666',
+ marginBottom: 4,
+ },
+});
diff --git a/example/src/screens/RemoteConfigsScreen/index.tsx b/example/src/screens/RemoteConfigsScreen/index.tsx
new file mode 100644
index 00000000..cd33f6c0
--- /dev/null
+++ b/example/src/screens/RemoteConfigsScreen/index.tsx
@@ -0,0 +1,219 @@
+import React, { useState } from 'react';
+import {
+ Text,
+ View,
+ TouchableOpacity,
+ Alert,
+ ScrollView,
+ TextInput,
+} from 'react-native';
+import Qonversion, { RemoteConfigList } from '@qonversion/react-native-sdk';
+import { AppContext } from '../../store/AppStore';
+import SkeletonLoader from '../../components/SkeletonLoader';
+import styles from './styles';
+import Snackbar from 'react-native-snackbar';
+
+const RemoteConfigsScreen: React.FC = () => {
+ const context = React.useContext(AppContext);
+ if (!context) return null;
+ const { state, dispatch } = context;
+
+ const [contextKeys, setContextKeys] = useState('');
+ const [singleContextKey, setSingleContextKey] = useState('');
+ const [experimentId, setExperimentId] = useState('');
+ const [groupId, setGroupId] = useState('');
+
+ const loadRemoteConfigList = async () => {
+ try {
+ console.log('🔄 [Qonversion] Starting remoteConfigList call...');
+ dispatch({ type: 'SET_LOADING', payload: true });
+ let remoteConfigs;
+ if (contextKeys.trim()) {
+ const keys = contextKeys.split(',').map((k) => k.trim());
+ console.log(
+ '🔄 [Qonversion] Calling remoteConfigListForContextKeys with keys:',
+ keys
+ );
+ remoteConfigs =
+ await Qonversion.getSharedInstance().remoteConfigListForContextKeys(
+ keys,
+ true
+ );
+ console.log(
+ '✅ [Qonversion] remoteConfigListForContextKeys call successful:',
+ remoteConfigs
+ );
+ } else {
+ console.log('🔄 [Qonversion] Calling remoteConfigList...');
+ remoteConfigs = await Qonversion.getSharedInstance().remoteConfigList();
+ console.log(
+ '✅ [Qonversion] remoteConfigList call successful:',
+ remoteConfigs
+ );
+ }
+ dispatch({ type: 'SET_REMOTE_CONFIGS', payload: remoteConfigs });
+ } catch (error: any) {
+ console.error('❌ [Qonversion] Remote config list call failed:', error);
+ Alert.alert('Error', error.message);
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: false });
+ }
+ };
+
+ const loadSingleRemoteConfig = async () => {
+ try {
+ console.log(
+ '🔄 [Qonversion] Starting remoteConfig call with contextKey:',
+ singleContextKey
+ );
+ dispatch({ type: 'SET_LOADING', payload: true });
+ const config = await Qonversion.getSharedInstance().remoteConfig(
+ singleContextKey || undefined
+ );
+ console.log('✅ [Qonversion] remoteConfig call successful:', config);
+ // Create a RemoteConfigList with a single config
+ const remoteConfigList = new RemoteConfigList([config]);
+ dispatch({ type: 'SET_REMOTE_CONFIGS', payload: remoteConfigList });
+ } catch (error: any) {
+ console.error('❌ [Qonversion] remoteConfig call failed:', error);
+ Alert.alert('Error', error.message);
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: false });
+ }
+ };
+
+ const attachToExperiment = async () => {
+ try {
+ console.log(
+ '🔄 [Qonversion] Starting attachUserToExperiment call with experimentId:',
+ experimentId,
+ 'groupId:',
+ groupId
+ );
+ await Qonversion.getSharedInstance().attachUserToExperiment(
+ experimentId,
+ groupId
+ );
+ console.log('✅ [Qonversion] attachUserToExperiment call successful');
+ Snackbar.show({
+ text: 'User attached to experiment!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ console.error(
+ '❌ [Qonversion] attachUserToExperiment call failed:',
+ error
+ );
+ Alert.alert('Error', error.message);
+ }
+ };
+
+ const detachFromExperiment = async () => {
+ try {
+ console.log(
+ '🔄 [Qonversion] Starting detachUserFromExperiment call with experimentId:',
+ experimentId
+ );
+ await Qonversion.getSharedInstance().detachUserFromExperiment(
+ experimentId
+ );
+ console.log('✅ [Qonversion] detachUserFromExperiment call successful');
+ Snackbar.show({
+ text: 'User detached from experiment!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ console.error(
+ '❌ [Qonversion] detachUserFromExperiment call failed:',
+ error
+ );
+ Alert.alert('Error', error.message);
+ }
+ };
+
+ if (state.loading) {
+ return ;
+ }
+
+ return (
+
+
+ Context Keys (comma-separated):
+
+
+ Get Remote Config List
+
+
+
+
+ Single Context Key:
+
+
+ Get Remote Config
+
+
+
+
+ Experiment ID:
+
+ Group ID:
+
+
+ Attach To Experiment
+
+
+ Detach From Experiment
+
+
+
+ {state.remoteConfigs && (
+
+ {state.remoteConfigs.remoteConfigs.map((config, index) => (
+
+
+ Context Key: {config.source.contextKey || 'empty'}
+
+
+ Source: {config.source.name}
+
+
+ Type: {config.source.type}
+
+
+ Payload: {JSON.stringify(config.payload)}
+
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default RemoteConfigsScreen;
diff --git a/example/src/screens/RemoteConfigsScreen/styles.ts b/example/src/screens/RemoteConfigsScreen/styles.ts
new file mode 100644
index 00000000..d1eb7c2c
--- /dev/null
+++ b/example/src/screens/RemoteConfigsScreen/styles.ts
@@ -0,0 +1,67 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 16,
+ paddingBottom: 32,
+ },
+ contentContainer: {
+ paddingBottom: 32,
+ },
+ inputContainer: {
+ marginBottom: 20,
+ },
+ inputLabel: {
+ fontSize: 14,
+ fontWeight: '600',
+ marginBottom: 8,
+ color: '#000000',
+ },
+ textInput: {
+ backgroundColor: '#ffffff',
+ borderWidth: 1,
+ borderColor: '#e0e0e0',
+ borderRadius: 8,
+ padding: 12,
+ marginBottom: 12,
+ fontSize: 16,
+ },
+ button: {
+ backgroundColor: '#007AFF',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 12,
+ alignItems: 'center',
+ },
+ buttonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ listContainer: {
+ marginTop: 20,
+ },
+ listItem: {
+ backgroundColor: '#ffffff',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 12,
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ },
+ listItemTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginBottom: 4,
+ color: '#000000',
+ },
+ listItemSubtitle: {
+ fontSize: 14,
+ color: '#666666',
+ marginBottom: 4,
+ },
+});
diff --git a/example/src/screens/UserScreen/index.tsx b/example/src/screens/UserScreen/index.tsx
new file mode 100644
index 00000000..cf3ca6e7
--- /dev/null
+++ b/example/src/screens/UserScreen/index.tsx
@@ -0,0 +1,377 @@
+import React, { useState } from 'react';
+import {
+ Text,
+ View,
+ TouchableOpacity,
+ Alert,
+ ScrollView,
+ TextInput,
+} from 'react-native';
+import Qonversion, {
+ UserPropertyKey,
+ AttributionProvider,
+} from '@qonversion/react-native-sdk';
+import { AppContext } from '../../store/AppStore';
+import SkeletonLoader from '../../components/SkeletonLoader';
+import UserInfoSection from '../../components/UserInfoSection';
+import styles from './styles';
+import Snackbar from 'react-native-snackbar';
+
+const UserScreen: React.FC = () => {
+ const [identityId, setIdentityId] = useState('');
+ const [propertyKey, setPropertyKey] = useState('');
+ const [propertyValue, setPropertyValue] = useState('');
+ const [attributionData, setAttributionData] = useState('');
+ const [userProperties, setUserProperties] = useState(null);
+
+ // New state for radio button selection
+ const [selectedPropertyKey, setSelectedPropertyKey] = useState<
+ UserPropertyKey | 'custom'
+ >('custom');
+ const [selectedAttributionProvider, setSelectedAttributionProvider] =
+ useState(AttributionProvider.APPSFLYER);
+
+ const context = React.useContext(AppContext);
+ if (!context) return null;
+ const { state, dispatch } = context;
+
+ const identify = async () => {
+ try {
+ console.log(
+ '🔄 [Qonversion] Starting identify() call with identityId:',
+ identityId
+ );
+ dispatch({ type: 'SET_LOADING', payload: true });
+ const userInfo =
+ await Qonversion.getSharedInstance().identify(identityId);
+ console.log('✅ [Qonversion] identify() call successful:', userInfo);
+ dispatch({ type: 'SET_USER_INFO', payload: userInfo });
+ Snackbar.show({
+ text: 'User identified successfully!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ console.error('❌ [Qonversion] identify() call failed:', error);
+ Alert.alert('Error', error.message);
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: false });
+ }
+ };
+
+ const logout = async () => {
+ try {
+ console.log('🔄 [Qonversion] Starting logout() call...');
+ Qonversion.getSharedInstance().logout();
+ console.log('✅ [Qonversion] logout() call successful');
+
+ console.log('🔄 [Qonversion] Starting userInfo() call after logout...');
+ const userInfo = await Qonversion.getSharedInstance().userInfo();
+ console.log(
+ '✅ [Qonversion] userInfo() call after logout successful:',
+ userInfo
+ );
+ dispatch({ type: 'SET_USER_INFO', payload: userInfo });
+ Snackbar.show({
+ text: 'User logged out successfully!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ console.error('❌ [Qonversion] Logout process failed:', error);
+ Alert.alert('Error', error.message);
+ }
+ };
+
+ const loadUserInfo = async () => {
+ try {
+ console.log('🔄 [Qonversion] Starting userInfo() call...');
+ dispatch({ type: 'SET_LOADING', payload: true });
+ const userInfo = await Qonversion.getSharedInstance().userInfo();
+ console.log('✅ [Qonversion] userInfo() call successful:', userInfo);
+ dispatch({ type: 'SET_USER_INFO', payload: userInfo });
+ } catch (error: any) {
+ console.error('❌ [Qonversion] userInfo() call failed:', error);
+ Alert.alert('Error', error.message);
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: false });
+ }
+ };
+
+ const loadUserProperties = async () => {
+ try {
+ console.log('🔄 [Qonversion] Starting userProperties() call...');
+ dispatch({ type: 'SET_LOADING', payload: true });
+ const properties = await Qonversion.getSharedInstance().userProperties();
+ console.log(
+ '✅ [Qonversion] userProperties() call successful:',
+ properties
+ );
+ setUserProperties(properties);
+ Snackbar.show({
+ text: 'User properties loaded successfully!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ } catch (error: any) {
+ console.error('❌ [Qonversion] userProperties() call failed:', error);
+ Alert.alert('Error', error.message);
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: false });
+ }
+ };
+
+ const setUserProperty = async () => {
+ try {
+ if (propertyValue) {
+ if (selectedPropertyKey === 'custom') {
+ // For custom properties, use setCustomUserProperty
+ if (propertyKey && propertyValue) {
+ console.log(
+ '🔄 [Qonversion] Starting setCustomUserProperty() call with key:',
+ propertyKey,
+ 'value:',
+ propertyValue
+ );
+ Qonversion.getSharedInstance().setCustomUserProperty(
+ propertyKey,
+ propertyValue
+ );
+ console.log(
+ '✅ [Qonversion] setCustomUserProperty() call successful'
+ );
+ Snackbar.show({
+ text: 'Custom user property set successfully!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ }
+ } else {
+ // For predefined properties, use setUserProperty
+ console.log(
+ '🔄 [Qonversion] Starting setUserProperty() call with key:',
+ selectedPropertyKey,
+ 'value:',
+ propertyValue
+ );
+ Qonversion.getSharedInstance().setUserProperty(
+ selectedPropertyKey as UserPropertyKey,
+ propertyValue
+ );
+ console.log('✅ [Qonversion] setUserProperty() call successful');
+ Snackbar.show({
+ text: 'User property set successfully!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ }
+ }
+ } catch (error: any) {
+ console.error('❌ [Qonversion] Property setting failed:', error);
+ Alert.alert('Error', error.message);
+ }
+ };
+
+ const sendAttribution = async () => {
+ try {
+ if (attributionData && selectedAttributionProvider) {
+ const data = JSON.parse(attributionData);
+ console.log(
+ '🔄 [Qonversion] Starting attribution() call with provider:',
+ selectedAttributionProvider,
+ 'data:',
+ data
+ );
+ Qonversion.getSharedInstance().attribution(
+ data,
+ selectedAttributionProvider as AttributionProvider
+ );
+ console.log('✅ [Qonversion] attribution() call successful');
+ Snackbar.show({
+ text: 'Attribution data sent successfully!',
+ duration: Snackbar.LENGTH_SHORT,
+ });
+ }
+ } catch (error: any) {
+ console.error('❌ [Qonversion] attribution() call failed:', error);
+ Alert.alert('Error', error.message);
+ }
+ };
+
+ const renderRadioButton = (
+ label: string,
+ value: string,
+ selectedValue: string,
+ onSelect: (value: any) => void
+ ) => (
+ onSelect(value)}
+ >
+
+ {selectedValue === value && }
+
+ {label}
+
+ );
+
+ if (state.loading) {
+ return ;
+ }
+
+ const canIdentify = !state.userInfo?.identityId;
+ const canLogout = !!state.userInfo?.identityId;
+
+ return (
+
+
+
+
+ Identity ID:
+
+
+ Identify
+
+
+ Logout
+
+
+ User Info
+
+
+
+
+ Properties
+
+ User Properties
+
+ {userProperties && (
+
+ User Properties:
+
+
+ Key
+
+
+ Value
+
+
+
+ {userProperties.properties.map((property: any, index: number) => (
+
+
+ {property.key}
+
+
+ {property.value}
+
+
+ ))}
+
+
+ )}
+
+ Property Key:
+
+ {/* UserPropertyKey Radio Buttons */}
+
+ {Object.values(UserPropertyKey)
+ .filter((key) => key !== UserPropertyKey.CUSTOM)
+ .map((key) =>
+ renderRadioButton(
+ key,
+ key,
+ selectedPropertyKey,
+ setSelectedPropertyKey
+ )
+ )}
+ {renderRadioButton(
+ 'Custom (Manual Input)',
+ 'custom',
+ selectedPropertyKey,
+ setSelectedPropertyKey
+ )}
+
+
+ {selectedPropertyKey === 'custom' && (
+
+ )}
+
+ Property Value:
+
+
+ Set Property
+
+
+
+
+ Attribution
+ Attribution Data (JSON):
+
+
+ Attribution Provider:
+
+ {/* AttributionProvider Radio Buttons */}
+
+ {Object.values(AttributionProvider).map((provider) =>
+ renderRadioButton(
+ provider,
+ provider,
+ selectedAttributionProvider,
+ setSelectedAttributionProvider
+ )
+ )}
+
+
+
+ Send Attribution
+
+
+
+ );
+};
+
+export default UserScreen;
diff --git a/example/src/screens/UserScreen/styles.ts b/example/src/screens/UserScreen/styles.ts
new file mode 100644
index 00000000..9003d0f8
--- /dev/null
+++ b/example/src/screens/UserScreen/styles.ts
@@ -0,0 +1,194 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 16,
+ paddingBottom: 32,
+ },
+ contentContainer: {
+ paddingBottom: 32,
+ },
+ userInfoContainer: {
+ backgroundColor: '#ffffff',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 20,
+ },
+ userInfoTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ color: '#000000',
+ },
+ userInfoText: {
+ fontSize: 14,
+ color: '#666666',
+ marginBottom: 4,
+ },
+ inputContainer: {
+ marginBottom: 20,
+ },
+ inputLabel: {
+ fontSize: 14,
+ fontWeight: '600',
+ marginBottom: 8,
+ color: '#000000',
+ },
+ textInput: {
+ backgroundColor: '#ffffff',
+ borderWidth: 1,
+ borderColor: '#e0e0e0',
+ borderRadius: 8,
+ padding: 12,
+ marginBottom: 12,
+ fontSize: 16,
+ },
+ multilineInput: {
+ height: 80,
+ textAlignVertical: 'top',
+ },
+ button: {
+ backgroundColor: '#007AFF',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 12,
+ alignItems: 'center',
+ },
+ buttonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ disabledButton: {
+ backgroundColor: '#cccccc',
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ marginBottom: 12,
+ color: '#000000',
+ },
+ userPropertiesContainer: {
+ backgroundColor: '#ffffff',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 20,
+ borderWidth: 1,
+ borderColor: '#e0e0e0',
+ },
+ userPropertiesTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginBottom: 12,
+ color: '#000000',
+ },
+ userPropertyItem: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ paddingVertical: 8,
+ borderBottomWidth: 1,
+ borderBottomColor: '#f0f0f0',
+ },
+ userPropertyKey: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#333333',
+ flex: 1,
+ },
+ userPropertyValue: {
+ fontSize: 14,
+ color: '#666666',
+ flex: 2,
+ textAlign: 'right',
+ flexWrap: 'wrap',
+ },
+ // Radio button styles
+ radioGroup: {
+ marginBottom: 12,
+ },
+ radioButtonContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 8,
+ paddingHorizontal: 4,
+ },
+ radioButton: {
+ width: 20,
+ height: 20,
+ borderRadius: 10,
+ borderWidth: 2,
+ borderColor: '#007AFF',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginRight: 12,
+ },
+ radioButtonSelected: {
+ width: 10,
+ height: 10,
+ borderRadius: 5,
+ backgroundColor: '#007AFF',
+ },
+ radioButtonLabel: {
+ fontSize: 14,
+ color: '#000000',
+ flex: 1,
+ },
+ // Table styles
+ tableHeader: {
+ flexDirection: 'row',
+ backgroundColor: '#f8f9fa',
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderBottomWidth: 1,
+ borderBottomColor: '#e0e0e0',
+ borderRadius: 8,
+ marginBottom: 8,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ tableHeaderKey: {
+ flex: 2,
+ fontSize: 14,
+ fontWeight: 'bold',
+ color: '#000000',
+ textAlignVertical: 'center',
+ },
+ tableHeaderValue: {
+ flex: 3,
+ fontSize: 14,
+ fontWeight: 'bold',
+ color: '#000000',
+ textAlignVertical: 'center',
+ },
+ tableContainer: {
+ // Removed maxHeight to allow natural expansion
+ },
+ tableRow: {
+ flexDirection: 'row',
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderBottomWidth: 1,
+ borderBottomColor: '#f0f0f0',
+ alignItems: 'center',
+ justifyContent: 'center',
+ minHeight: 50,
+ },
+ tableRowEven: {
+ backgroundColor: '#f8f9fa',
+ },
+ tableCellKey: {
+ flex: 2,
+ fontSize: 14,
+ color: '#333333',
+ fontWeight: '500',
+ textAlignVertical: 'center',
+ },
+ tableCellValue: {
+ flex: 3,
+ fontSize: 14,
+ color: '#666666',
+ textAlignVertical: 'center',
+ },
+});
diff --git a/example/src/store/AppStore.ts b/example/src/store/AppStore.ts
new file mode 100644
index 00000000..9f2d5c6f
--- /dev/null
+++ b/example/src/store/AppStore.ts
@@ -0,0 +1,126 @@
+import React from 'react';
+import {
+ Offerings,
+ Product,
+ RemoteConfigList,
+ User,
+} from '@qonversion/react-native-sdk';
+import Entitlement from '../../../src/dto/Entitlement';
+
+// Global Store (Redux-like pattern)
+export interface AppState {
+ products: Map | null;
+ offerings: Offerings | null;
+ entitlements: Map | null;
+ remoteConfigs: RemoteConfigList | null;
+ userInfo: User | null;
+ loading: boolean;
+ navigationStack: string[];
+ noCodesEvents: string[];
+ qonversionInitStatus:
+ | 'not_initialized'
+ | 'initializing'
+ | 'success'
+ | 'error';
+ selectedProduct: Product | null;
+ selectedEntitlement: Entitlement | null;
+ isQonversionInitialized: boolean;
+}
+
+export type AppAction =
+ | { type: 'SET_PRODUCTS'; payload: Map }
+ | { type: 'SET_OFFERINGS'; payload: Offerings }
+ | { type: 'SET_ENTITLEMENTS'; payload: Map }
+ | { type: 'SET_REMOTE_CONFIGS'; payload: RemoteConfigList }
+ | { type: 'SET_USER_INFO'; payload: User }
+ | { type: 'SET_LOADING'; payload: boolean }
+ | { type: 'PUSH_SCREEN'; payload: string }
+ | { type: 'POP_SCREEN' }
+ | { type: 'REPLACE_SCREEN'; payload: string }
+ | { type: 'ADD_NOCODES_EVENT'; payload: string }
+ | {
+ type: 'SET_QONVERSION_INIT_STATUS';
+ payload: 'not_initialized' | 'initializing' | 'success' | 'error';
+ }
+ | { type: 'SET_SELECTED_PRODUCT'; payload: Product | null }
+ | { type: 'SET_SELECTED_ENTITLEMENT'; payload: Entitlement | null }
+ | { type: 'SET_QONVERSION_INITIALIZED'; payload: boolean };
+
+export const initialState: AppState = {
+ products: null,
+ offerings: null,
+ entitlements: null,
+ remoteConfigs: null,
+ userInfo: null,
+ loading: false,
+ navigationStack: ['main'],
+ noCodesEvents: [],
+ qonversionInitStatus: 'not_initialized',
+ selectedProduct: null,
+ selectedEntitlement: null,
+ isQonversionInitialized: false,
+};
+
+export function appReducer(state: AppState, action: AppAction): AppState {
+ switch (action.type) {
+ case 'SET_PRODUCTS':
+ return { ...state, products: action.payload };
+ case 'SET_OFFERINGS':
+ return { ...state, offerings: action.payload };
+ case 'SET_ENTITLEMENTS':
+ return { ...state, entitlements: action.payload };
+ case 'SET_REMOTE_CONFIGS':
+ return { ...state, remoteConfigs: action.payload };
+ case 'SET_USER_INFO':
+ return { ...state, userInfo: action.payload };
+ case 'SET_LOADING':
+ return { ...state, loading: action.payload };
+ case 'PUSH_SCREEN':
+ return {
+ ...state,
+ navigationStack: [...state.navigationStack, action.payload],
+ };
+ case 'POP_SCREEN':
+ return {
+ ...state,
+ navigationStack:
+ state.navigationStack.length > 1
+ ? state.navigationStack.slice(0, -1)
+ : state.navigationStack,
+ };
+ case 'REPLACE_SCREEN':
+ return {
+ ...state,
+ navigationStack:
+ state.navigationStack.length > 0
+ ? [...state.navigationStack.slice(0, -1), action.payload]
+ : [action.payload],
+ };
+ case 'ADD_NOCODES_EVENT':
+ return {
+ ...state,
+ noCodesEvents: [...state.noCodesEvents, action.payload],
+ };
+ case 'SET_QONVERSION_INIT_STATUS':
+ return { ...state, qonversionInitStatus: action.payload };
+ case 'SET_SELECTED_PRODUCT':
+ return { ...state, selectedProduct: action.payload };
+ case 'SET_SELECTED_ENTITLEMENT':
+ return { ...state, selectedEntitlement: action.payload };
+ case 'SET_QONVERSION_INITIALIZED':
+ return { ...state, isQonversionInitialized: action.payload };
+ default:
+ return state;
+ }
+}
+
+// Helper function to get current screen from navigation stack
+export const getCurrentScreen = (state: AppState): string => {
+ return state.navigationStack[state.navigationStack.length - 1] || 'main';
+};
+
+// Global store context
+export const AppContext = React.createContext<{
+ state: AppState;
+ dispatch: React.Dispatch;
+} | null>(null);
diff --git a/example/yarn.lock b/example/yarn.lock
index a29fcdd9..ea77c58a 100644
--- a/example/yarn.lock
+++ b/example/yarn.lock
@@ -1779,6 +1779,7 @@ __metadata:
"@babel/core": ^7.25.2
"@babel/preset-env": ^7.25.3
"@babel/runtime": ^7.25.0
+ "@react-native-clipboard/clipboard": ^1.16.3
"@react-native-community/cli": 19.0.0
"@react-native-community/cli-platform-android": 19.0.0
"@react-native-community/cli-platform-ios": 19.0.0
@@ -1789,9 +1790,27 @@ __metadata:
react: 19.1.0
react-native: 0.80.1
react-native-builder-bob: 0.40.13
+ react-native-snackbar: ^2.9.0
languageName: unknown
linkType: soft
+"@react-native-clipboard/clipboard@npm:^1.16.3":
+ version: 1.16.3
+ resolution: "@react-native-clipboard/clipboard@npm:1.16.3"
+ peerDependencies:
+ react: ">= 16.9.0"
+ react-native: ">= 0.61.5"
+ react-native-macos: ">= 0.61.0"
+ react-native-windows: ">= 0.61.0"
+ peerDependenciesMeta:
+ react-native-macos:
+ optional: true
+ react-native-windows:
+ optional: true
+ checksum: 3d9944fc2c4acbecf917e752cc36ec92c4dcdb590c94c81c78c24df9ddd4b02448e252eb39e0949000b01046cfdfe2b03e1676c5e3ac0fe7eb3bf6b649970c27
+ languageName: node
+ linkType: hard
+
"@react-native-community/cli-clean@npm:19.0.0":
version: 19.0.0
resolution: "@react-native-community/cli-clean@npm:19.0.0"
@@ -5738,6 +5757,16 @@ __metadata:
languageName: node
linkType: hard
+"react-native-snackbar@npm:^2.9.0":
+ version: 2.9.0
+ resolution: "react-native-snackbar@npm:2.9.0"
+ peerDependencies:
+ react: ">=16"
+ react-native: ">=0.60"
+ checksum: ea80b525306fd207e390c7852cf8a4d8d0345c1c48c56713316e1f28e4676f3307f381ed3b80ff966e4ecaa6db1041079e92f9816100df7d4f39ca4f365cc9d5
+ languageName: node
+ linkType: hard
+
"react-native@npm:0.80.1":
version: 0.80.1
resolution: "react-native@npm:0.80.1"
diff --git a/yarn.lock b/yarn.lock
index df26e71c..392d1a1f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2301,6 +2301,7 @@ __metadata:
"@babel/core": ^7.25.2
"@babel/preset-env": ^7.25.3
"@babel/runtime": ^7.25.0
+ "@react-native-clipboard/clipboard": ^1.16.3
"@react-native-community/cli": 19.0.0
"@react-native-community/cli-platform-android": 19.0.0
"@react-native-community/cli-platform-ios": 19.0.0
@@ -2311,6 +2312,7 @@ __metadata:
react: 19.1.0
react-native: 0.80.1
react-native-builder-bob: 0.40.13
+ react-native-snackbar: ^2.9.0
languageName: unknown
linkType: soft
@@ -2345,6 +2347,23 @@ __metadata:
languageName: unknown
linkType: soft
+"@react-native-clipboard/clipboard@npm:^1.16.3":
+ version: 1.16.3
+ resolution: "@react-native-clipboard/clipboard@npm:1.16.3"
+ peerDependencies:
+ react: ">= 16.9.0"
+ react-native: ">= 0.61.5"
+ react-native-macos: ">= 0.61.0"
+ react-native-windows: ">= 0.61.0"
+ peerDependenciesMeta:
+ react-native-macos:
+ optional: true
+ react-native-windows:
+ optional: true
+ checksum: 3d9944fc2c4acbecf917e752cc36ec92c4dcdb590c94c81c78c24df9ddd4b02448e252eb39e0949000b01046cfdfe2b03e1676c5e3ac0fe7eb3bf6b649970c27
+ languageName: node
+ linkType: hard
+
"@react-native-community/cli-clean@npm:15.0.0-alpha.2":
version: 15.0.0-alpha.2
resolution: "@react-native-community/cli-clean@npm:15.0.0-alpha.2"
@@ -9214,6 +9233,16 @@ __metadata:
languageName: node
linkType: hard
+"react-native-snackbar@npm:^2.9.0":
+ version: 2.9.0
+ resolution: "react-native-snackbar@npm:2.9.0"
+ peerDependencies:
+ react: ">=16"
+ react-native: ">=0.60"
+ checksum: ea80b525306fd207e390c7852cf8a4d8d0345c1c48c56713316e1f28e4676f3307f381ed3b80ff966e4ecaa6db1041079e92f9816100df7d4f39ca4f365cc9d5
+ languageName: node
+ linkType: hard
+
"react-native@npm:0.79.2":
version: 0.79.2
resolution: "react-native@npm:0.79.2"