From 917184c021af78ec7d63172a87031aa73d0c062c Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Wed, 27 Aug 2025 00:13:39 +0300 Subject: [PATCH 1/3] Sample app recreated. --- .../java/io/qonversion/sample/MainActivity.kt | 2 +- .../app/src/main/res/values/strings.xml | 2 +- example/package.json | 4 +- example/src/App.tsx | 543 +++++------------- example/src/components/ProductCard/index.tsx | 36 ++ example/src/components/ProductCard/styles.ts | 28 + example/src/components/SkeletonLoader.tsx | 25 + .../src/components/UserInfoSection/index.tsx | 66 +++ .../src/components/UserInfoSection/styles.ts | 47 ++ example/src/index.ts | 15 + .../screens/EntitlementDetailScreen/index.tsx | 95 +++ .../screens/EntitlementDetailScreen/styles.ts | 87 +++ .../src/screens/EntitlementsScreen/index.tsx | 218 +++++++ .../src/screens/EntitlementsScreen/styles.ts | 82 +++ example/src/screens/MainScreen/index.tsx | 108 ++++ example/src/screens/MainScreen/styles.ts | 135 +++++ example/src/screens/NoCodesScreen/index.tsx | 187 ++++++ example/src/screens/NoCodesScreen/styles.ts | 98 ++++ example/src/screens/OfferingsScreen/index.tsx | 81 +++ example/src/screens/OfferingsScreen/styles.ts | 113 ++++ example/src/screens/OtherScreen/index.tsx | 133 +++++ example/src/screens/OtherScreen/styles.ts | 58 ++ .../src/screens/ProductDetailScreen/index.tsx | 118 ++++ .../src/screens/ProductDetailScreen/styles.ts | 59 ++ example/src/screens/ProductsScreen/index.tsx | 56 ++ example/src/screens/ProductsScreen/styles.ts | 52 ++ .../src/screens/RemoteConfigsScreen/index.tsx | 168 ++++++ .../src/screens/RemoteConfigsScreen/styles.ts | 67 +++ example/src/screens/UserScreen/index.tsx | 297 ++++++++++ example/src/screens/UserScreen/styles.ts | 194 +++++++ example/src/store/AppStore.ts | 109 ++++ example/yarn.lock | 29 + 32 files changed, 2917 insertions(+), 395 deletions(-) create mode 100644 example/src/components/ProductCard/index.tsx create mode 100644 example/src/components/ProductCard/styles.ts create mode 100644 example/src/components/SkeletonLoader.tsx create mode 100644 example/src/components/UserInfoSection/index.tsx create mode 100644 example/src/components/UserInfoSection/styles.ts create mode 100644 example/src/index.ts create mode 100644 example/src/screens/EntitlementDetailScreen/index.tsx create mode 100644 example/src/screens/EntitlementDetailScreen/styles.ts create mode 100644 example/src/screens/EntitlementsScreen/index.tsx create mode 100644 example/src/screens/EntitlementsScreen/styles.ts create mode 100644 example/src/screens/MainScreen/index.tsx create mode 100644 example/src/screens/MainScreen/styles.ts create mode 100644 example/src/screens/NoCodesScreen/index.tsx create mode 100644 example/src/screens/NoCodesScreen/styles.ts create mode 100644 example/src/screens/OfferingsScreen/index.tsx create mode 100644 example/src/screens/OfferingsScreen/styles.ts create mode 100644 example/src/screens/OtherScreen/index.tsx create mode 100644 example/src/screens/OtherScreen/styles.ts create mode 100644 example/src/screens/ProductDetailScreen/index.tsx create mode 100644 example/src/screens/ProductDetailScreen/styles.ts create mode 100644 example/src/screens/ProductsScreen/index.tsx create mode 100644 example/src/screens/ProductsScreen/styles.ts create mode 100644 example/src/screens/RemoteConfigsScreen/index.tsx create mode 100644 example/src/screens/RemoteConfigsScreen/styles.ts create mode 100644 example/src/screens/UserScreen/index.tsx create mode 100644 example/src/screens/UserScreen/styles.ts create mode 100644 example/src/store/AppStore.ts diff --git a/example/android/app/src/main/java/io/qonversion/sample/MainActivity.kt b/example/android/app/src/main/java/io/qonversion/sample/MainActivity.kt index 07ae383a..2ee18112 100644 --- a/example/android/app/src/main/java/io/qonversion/sample/MainActivity.kt +++ b/example/android/app/src/main/java/io/qonversion/sample/MainActivity.kt @@ -11,7 +11,7 @@ class MainActivity : ReactActivity() { * Returns the name of the main component registered from JavaScript. This is used to schedule * rendering of the component. */ - override fun getMainComponentName(): String = "ReactNativeSdkExample" + override fun getMainComponentName(): String = "ReactNativeExample" /** * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] diff --git a/example/android/app/src/main/res/values/strings.xml b/example/android/app/src/main/res/values/strings.xml index 9a4c824b..e79194b5 100644 --- a/example/android/app/src/main/res/values/strings.xml +++ b/example/android/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - ReactNativeSdkExample + ReactNativeExample diff --git a/example/package.json b/example/package.json index 7c591435..adbca351 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..987cff7d 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,414 +1,173 @@ -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..84fa5081 --- /dev/null +++ b/example/src/components/ProductCard/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { + Text, + View, + 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..ff942642 --- /dev/null +++ b/example/src/components/UserInfoSection/index.tsx @@ -0,0 +1,66 @@ +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..cdc13da4 --- /dev/null +++ b/example/src/index.ts @@ -0,0 +1,15 @@ +// 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..0c71b3b1 --- /dev/null +++ b/example/src/screens/EntitlementDetailScreen/index.tsx @@ -0,0 +1,95 @@ +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..d85a8355 --- /dev/null +++ b/example/src/screens/EntitlementsScreen/index.tsx @@ -0,0 +1,218 @@ +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..9b163024 --- /dev/null +++ b/example/src/screens/MainScreen/index.tsx @@ -0,0 +1,108 @@ +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..6dbc82a5 --- /dev/null +++ b/example/src/screens/NoCodesScreen/index.tsx @@ -0,0 +1,187 @@ +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..3a6c383f --- /dev/null +++ b/example/src/screens/OfferingsScreen/index.tsx @@ -0,0 +1,81 @@ +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..9c21357b --- /dev/null +++ b/example/src/screens/OtherScreen/index.tsx @@ -0,0 +1,133 @@ +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..b532b0a4 --- /dev/null +++ b/example/src/screens/ProductDetailScreen/index.tsx @@ -0,0 +1,118 @@ +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..f1f5c82d --- /dev/null +++ b/example/src/screens/ProductsScreen/index.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { + Text, + View, + TouchableOpacity, + Alert, + ScrollView, +} from 'react-native'; +import Qonversion, { Product } 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..c59d6bfc --- /dev/null +++ b/example/src/screens/ProductsScreen/styles.ts @@ -0,0 +1,52 @@ +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..30929539 --- /dev/null +++ b/example/src/screens/RemoteConfigsScreen/index.tsx @@ -0,0 +1,168 @@ +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..bbf9da14 --- /dev/null +++ b/example/src/screens/UserScreen/index.tsx @@ -0,0 +1,297 @@ +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 context = React.useContext(AppContext); + if (!context) return null; + const { state, dispatch } = context; + + 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('custom'); + const [selectedAttributionProvider, setSelectedAttributionProvider] = useState(AttributionProvider.APPSFLYER); + + 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..055b73f7 --- /dev/null +++ b/example/src/store/AppStore.ts @@ -0,0 +1,109 @@ +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" From cda421e0971114cc082af84148124b1c7e6606f5 Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Wed, 27 Aug 2025 00:24:06 +0300 Subject: [PATCH 2/3] linter fixes --- example/src/App.tsx | 51 ++++-- example/src/components/ProductCard/index.tsx | 14 +- .../src/components/UserInfoSection/index.tsx | 40 +++-- example/src/index.ts | 8 +- .../screens/EntitlementDetailScreen/index.tsx | 43 +++-- .../src/screens/EntitlementsScreen/index.tsx | 96 +++++++---- example/src/screens/MainScreen/index.tsx | 52 +++--- example/src/screens/NoCodesScreen/index.tsx | 47 ++++-- example/src/screens/OfferingsScreen/index.tsx | 25 +-- example/src/screens/OtherScreen/index.tsx | 73 ++++++-- .../src/screens/ProductDetailScreen/index.tsx | 42 +++-- example/src/screens/ProductsScreen/index.tsx | 15 +- example/src/screens/ProductsScreen/styles.ts | 1 - .../src/screens/RemoteConfigsScreen/index.tsx | 89 +++++++--- example/src/screens/UserScreen/index.tsx | 156 +++++++++++++----- example/src/store/AppStore.ts | 51 ++++-- 16 files changed, 555 insertions(+), 248 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 987cff7d..3a179f14 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -7,7 +7,12 @@ import { SafeAreaView, Alert, } from 'react-native'; -import { AppContext, initialState, appReducer, getCurrentScreen } from './store/AppStore'; +import { + AppContext, + initialState, + appReducer, + getCurrentScreen, +} from './store/AppStore'; import Qonversion, { QonversionConfigBuilder, LaunchMode, @@ -49,22 +54,31 @@ const App: React.FC = () => { const initializeQonversion = async () => { try { console.log('🔄 [Qonversion] Starting SDK initialization...'); - dispatch({ type: 'SET_QONVERSION_INIT_STATUS', payload: 'initializing' }); - + dispatch({ + type: 'SET_QONVERSION_INIT_STATUS', + payload: 'initializing', + }); + console.log('🔄 [Qonversion] Building config...'); - const config = new QonversionConfigBuilder(ProjectKey, LaunchMode.SUBSCRIPTION_MANAGEMENT) + 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); + console.log( + '📡 [Qonversion] Entitlements updated via listener:', + entitlements + ); dispatch({ type: 'SET_ENTITLEMENTS', payload: entitlements }); }, }) - .setProxyURL("api-eu.qonversion.io") + .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'); @@ -73,11 +87,13 @@ const App: React.FC = () => { // 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'); + Alert.alert( + 'Initialization Error', + error.message || 'Failed to initialize Qonversion SDK' + ); } }; @@ -93,11 +109,19 @@ const App: React.FC = () => { case 'products': return ; case 'productDetail': - return state.selectedProduct ? : ; + return state.selectedProduct ? ( + + ) : ( + + ); case 'entitlements': return ; case 'entitlementDetail': - return state.selectedEntitlement ? : ; + return state.selectedEntitlement ? ( + + ) : ( + + ); case 'offerings': return ; case 'remoteConfigs': @@ -134,7 +158,10 @@ const App: React.FC = () => { )} - {getCurrentScreen(state) === 'main' ? 'Qonversion SDK Demo' : getCurrentScreen(state).charAt(0).toUpperCase() + getCurrentScreen(state).slice(1)} + {getCurrentScreen(state) === 'main' + ? 'Qonversion SDK Demo' + : getCurrentScreen(state).charAt(0).toUpperCase() + + getCurrentScreen(state).slice(1)} {renderScreen()} diff --git a/example/src/components/ProductCard/index.tsx b/example/src/components/ProductCard/index.tsx index 84fa5081..dec379cb 100644 --- a/example/src/components/ProductCard/index.tsx +++ b/example/src/components/ProductCard/index.tsx @@ -1,9 +1,5 @@ import React from 'react'; -import { - Text, - View, - TouchableOpacity, -} from 'react-native'; +import { Text, TouchableOpacity } from 'react-native'; import { Product } from '@qonversion/react-native-sdk'; import { AppContext } from '../../store/AppStore'; import styles from './styles'; @@ -26,9 +22,13 @@ const ProductCard: React.FC = ({ product }) => { {product.qonversionId} Store ID: {product.storeId} - Base Plan ID: {product.basePlanId} + + Base Plan ID: {product.basePlanId} + Type: {product.type} - Price: {product.prettyPrice || 'N/A'} + + Price: {product.prettyPrice || 'N/A'} + ); }; diff --git a/example/src/components/UserInfoSection/index.tsx b/example/src/components/UserInfoSection/index.tsx index ff942642..ed3eeda9 100644 --- a/example/src/components/UserInfoSection/index.tsx +++ b/example/src/components/UserInfoSection/index.tsx @@ -1,9 +1,5 @@ import React from 'react'; -import { - Text, - View, - TouchableOpacity, -} from 'react-native'; +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'; @@ -15,10 +11,10 @@ interface UserInfoSectionProps { showCopyButtons?: boolean; } -const UserInfoSection: React.FC = ({ - userInfo, - title = 'User Information:', - showCopyButtons = true +const UserInfoSection: React.FC = ({ + userInfo, + title = 'User Information:', + showCopyButtons = true, }) => { if (!userInfo) { return null; @@ -35,28 +31,40 @@ const UserInfoSection: React.FC = ({ return ( {title} - + {showCopyButtons ? ( <> copyToClipboard(userInfo?.qonversionId || '', 'Qonversion ID')} + onPress={() => + copyToClipboard(userInfo?.qonversionId || '', 'Qonversion ID') + } > Qonversion ID: - {userInfo?.qonversionId || 'Not available'} + + {userInfo?.qonversionId || 'Not available'} + copyToClipboard(userInfo?.identityId || '', 'Identity ID')} + onPress={() => + copyToClipboard(userInfo?.identityId || '', 'Identity ID') + } > Identity ID: - {userInfo?.identityId || 'Anonymous'} + + {userInfo?.identityId || 'Anonymous'} + ) : ( <> - ID: {userInfo?.identityId || 'Anonymous'} - Qonversion ID: {userInfo?.qonversionId || 'Not available'} + + ID: {userInfo?.identityId || 'Anonymous'} + + + Qonversion ID: {userInfo?.qonversionId || 'Not available'} + )} diff --git a/example/src/index.ts b/example/src/index.ts index cdc13da4..c8aa9dec 100644 --- a/example/src/index.ts +++ b/example/src/index.ts @@ -1,5 +1,11 @@ // Store exports -export { AppContext, initialState, appReducer, type AppState, type AppAction } from './store/AppStore'; +export { + AppContext, + initialState, + appReducer, + type AppState, + type AppAction, +} from './store/AppStore'; // Component exports export { default as SkeletonLoader } from './components/SkeletonLoader'; diff --git a/example/src/screens/EntitlementDetailScreen/index.tsx b/example/src/screens/EntitlementDetailScreen/index.tsx index 0c71b3b1..d4ba1c36 100644 --- a/example/src/screens/EntitlementDetailScreen/index.tsx +++ b/example/src/screens/EntitlementDetailScreen/index.tsx @@ -1,16 +1,14 @@ import React from 'react'; -import { - Text, - View, - ScrollView, -} from 'react-native'; +import { Text, View, ScrollView } from 'react-native'; import styles from './styles'; interface EntitlementDetailScreenProps { entitlement: any; } -const EntitlementDetailScreen: React.FC = ({ entitlement }) => { +const EntitlementDetailScreen: React.FC = ({ + entitlement, +}) => { const formatDate = (date: Date) => { return date.toLocaleDateString('en-US', { year: 'numeric', @@ -37,21 +35,24 @@ const EntitlementDetailScreen: React.FC = ({ entit return ( {label}: - - {date ? formatDate(date) : '-'} - + {date ? formatDate(date) : '-'} ); }; return ( - + {entitlement.id} - + {entitlement.isActive ? 'Active' : 'Inactive'} @@ -75,16 +76,24 @@ const EntitlementDetailScreen: React.FC = ({ entit {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)} + {renderDateField( + 'Auto Renew Disable Date', + entitlement.autoRenewDisableDate + )} Additional Information - {renderField('Last Activated Offer Code', entitlement.lastActivatedOfferCode)} + {renderField( + 'Last Activated Offer Code', + entitlement.lastActivatedOfferCode + )} {entitlement.transactions && entitlement.transactions.length > 0 && ( Transactions: - {entitlement.transactions.length} transaction(s) + + {entitlement.transactions.length} transaction(s) + )} diff --git a/example/src/screens/EntitlementsScreen/index.tsx b/example/src/screens/EntitlementsScreen/index.tsx index d85a8355..14286a3c 100644 --- a/example/src/screens/EntitlementsScreen/index.tsx +++ b/example/src/screens/EntitlementsScreen/index.tsx @@ -22,8 +22,12 @@ const EntitlementsScreen: React.FC = () => { 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); + 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); @@ -37,11 +41,16 @@ const EntitlementsScreen: React.FC = () => { console.log('🔄 [Qonversion] Setting entitlements update listener...'); Qonversion.getSharedInstance().setEntitlementsUpdateListener({ onEntitlementsUpdated(entitlements: Map) { - console.log('📡 [Qonversion] Entitlements updated via listener:', Object.fromEntries(entitlements)); + console.log( + '📡 [Qonversion] Entitlements updated via listener:', + Object.fromEntries(entitlements) + ); dispatch({ type: 'SET_ENTITLEMENTS', payload: entitlements }); }, }); - console.log('✅ [Qonversion] Entitlements update listener set successfully'); + console.log( + '✅ [Qonversion] Entitlements update listener set successfully' + ); Snackbar.show({ text: 'Entitlements listener set successfully!', duration: Snackbar.LENGTH_SHORT, @@ -129,13 +138,21 @@ const EntitlementsScreen: React.FC = () => { const renderContent = () => { return ( - + Load Entitlements - - Set Entitlements Updated Listener + + + Set Entitlements Updated Listener + @@ -146,8 +163,13 @@ const EntitlementsScreen: React.FC = () => { Sync Historical Data - - Sync StoreKit 2 Purchases (iOS Only) + + + Sync StoreKit 2 Purchases (iOS Only) + @@ -167,7 +189,8 @@ const EntitlementsScreen: React.FC = () => { No Entitlements Loaded - Tap "Load Entitlements" to fetch your current entitlements from the server. + Tap "Load Entitlements" to fetch your current entitlements from + the server. ) : state.entitlements.size === 0 ? ( @@ -175,36 +198,45 @@ const EntitlementsScreen: React.FC = () => { No Entitlements Found - You don't have any active entitlements. Try restoring purchases or check your subscription status. + 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 && ( + {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'} + - Expires: {formatDate(entitlement.expirationDate)} + Started: {formatDate(entitlement.startedDate)} - )} - - ))} + {entitlement.expirationDate && ( + + Expires: {formatDate(entitlement.expirationDate)} + + )} + + ) + )} )} diff --git a/example/src/screens/MainScreen/index.tsx b/example/src/screens/MainScreen/index.tsx index 9b163024..83f2dd13 100644 --- a/example/src/screens/MainScreen/index.tsx +++ b/example/src/screens/MainScreen/index.tsx @@ -1,11 +1,5 @@ import React from 'react'; -import { - Text, - View, - Image, - TouchableOpacity, - ScrollView, -} from 'react-native'; +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'; @@ -21,7 +15,7 @@ const MainScreen: React.FC = () => { ); } - + const { state, dispatch } = context; const menuItems = [ @@ -35,29 +29,39 @@ const MainScreen: React.FC = () => { ]; return ( - + Qonversion SDK Demo - + - + - {state.qonversionInitStatus === 'not_initialized' && 'Initialization not completed'} + {state.qonversionInitStatus === 'not_initialized' && + 'Initialization not completed'} {state.qonversionInitStatus === 'initializing' && 'Initializing...'} - {state.qonversionInitStatus === 'success' && 'Initialization successful'} + {state.qonversionInitStatus === 'success' && + 'Initialization successful'} {state.qonversionInitStatus === 'error' && 'Initialization error'} - + {state.userInfo && ( Current User: @@ -72,7 +76,9 @@ const MainScreen: React.FC = () => { }} > Qonversion ID: - {state.userInfo?.qonversionId || 'Not available'} + + {state.userInfo?.qonversionId || 'Not available'} + { }} > Identity ID: - {state.userInfo?.identityId || 'Anonymous'} + + {state.userInfo?.identityId || 'Anonymous'} + )} diff --git a/example/src/screens/NoCodesScreen/index.tsx b/example/src/screens/NoCodesScreen/index.tsx index 6dbc82a5..fb97c837 100644 --- a/example/src/screens/NoCodesScreen/index.tsx +++ b/example/src/screens/NoCodesScreen/index.tsx @@ -28,7 +28,9 @@ const NoCodesScreen: React.FC = () => { const { state, dispatch } = context; const [contextKey, setContextKey] = useState('kamo_test'); - const [presentationStyle, setPresentationStyle] = useState(ScreenPresentationStyle.FULL_SCREEN); + const [presentationStyle, setPresentationStyle] = useState( + ScreenPresentationStyle.FULL_SCREEN + ); const [animated, setAnimated] = useState(false); useEffect(() => { @@ -67,11 +69,11 @@ const NoCodesScreen: React.FC = () => { 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'); @@ -82,7 +84,10 @@ const NoCodesScreen: React.FC = () => { const showScreen = () => { try { - console.log('🔄 [NoCodes] Starting showScreen() call with contextKey:', contextKey); + console.log( + '🔄 [NoCodes] Starting showScreen() call with contextKey:', + contextKey + ); NoCodes.getSharedInstance().showScreen(contextKey); console.log('✅ [NoCodes] showScreen() call successful'); } catch (error: any) { @@ -93,17 +98,28 @@ const NoCodesScreen: React.FC = () => { const setPresentationConfig = () => { try { - console.log('🔄 [NoCodes] Starting setScreenPresentationConfig() call with contextKey:', contextKey, 'config:', { presentationStyle, animated }); + console.log( + '🔄 [NoCodes] Starting setScreenPresentationConfig() call with contextKey:', + contextKey, + 'config:', + { presentationStyle, animated } + ); const config = new ScreenPresentationConfig(presentationStyle, animated); - - NoCodes.getSharedInstance().setScreenPresentationConfig(config, contextKey); + + 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); + console.error( + '❌ [NoCodes] setScreenPresentationConfig() call failed:', + error + ); Alert.alert('Error', error.message); } }; @@ -124,7 +140,10 @@ const NoCodesScreen: React.FC = () => { }; return ( - + Context Key: { key={style} style={[ styles.radioButton, - presentationStyle === style && styles.radioButtonSelected + presentationStyle === style && styles.radioButtonSelected, ]} onPress={() => setPresentationStyle(style)} > {style} ))} - + {Platform.OS === 'ios' && ( { Animated (iOS only) )} - + Set Presentation Config @@ -176,7 +195,9 @@ const NoCodesScreen: React.FC = () => { No-Codes Events: {state.noCodesEvents.map((event, index) => ( - {event} + + {event} + ))} diff --git a/example/src/screens/OfferingsScreen/index.tsx b/example/src/screens/OfferingsScreen/index.tsx index 3a6c383f..d4eea491 100644 --- a/example/src/screens/OfferingsScreen/index.tsx +++ b/example/src/screens/OfferingsScreen/index.tsx @@ -1,11 +1,5 @@ import React from 'react'; -import { - Text, - View, - TouchableOpacity, - Alert, - ScrollView, -} from 'react-native'; +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'; @@ -39,20 +33,25 @@ const OfferingsScreen: React.FC = () => { } return ( - + Load Offerings {state.offerings ? ( - Main Offering ID: {state.offerings.main?.id} - + + Main Offering ID: {state.offerings.main?.id} + + {state.offerings.availableOffering.map((offering) => ( {offering.id} Tag: {offering.tag} - + {offering.products.length > 0 ? ( Products: @@ -61,7 +60,9 @@ const OfferingsScreen: React.FC = () => { ))} ) : ( - No products in this offering + + No products in this offering + )} ))} diff --git a/example/src/screens/OtherScreen/index.tsx b/example/src/screens/OtherScreen/index.tsx index 9c21357b..bc2003ba 100644 --- a/example/src/screens/OtherScreen/index.tsx +++ b/example/src/screens/OtherScreen/index.tsx @@ -18,17 +18,28 @@ const OtherScreen: React.FC = () => { if (!context) return null; const { state, dispatch } = context; - const [fallbackAccessible, setFallbackAccessible] = useState(null); + const [fallbackAccessible, setFallbackAccessible] = useState( + null + ); const checkFallbackFile = async () => { try { - console.log('🔄 [Qonversion] Starting isFallbackFileAccessible() call...'); + 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); + 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); + console.error( + '❌ [Qonversion] isFallbackFileAccessible() call failed:', + error + ); Alert.alert('Error', error.message); } finally { dispatch({ type: 'SET_LOADING', payload: false }); @@ -49,7 +60,10 @@ const OtherScreen: React.FC = () => { duration: Snackbar.LENGTH_SHORT, }); } catch (error: any) { - console.error('❌ [Qonversion] collectAdvertisingId() call failed:', error); + console.error( + '❌ [Qonversion] collectAdvertisingId() call failed:', + error + ); Alert.alert('Error', error.message); } }; @@ -60,15 +74,22 @@ const OtherScreen: React.FC = () => { return; } try { - console.log('🔄 [Qonversion] Starting collectAppleSearchAdsAttribution() call...'); + console.log( + '🔄 [Qonversion] Starting collectAppleSearchAdsAttribution() call...' + ); Qonversion.getSharedInstance().collectAppleSearchAdsAttribution(); - console.log('✅ [Qonversion] collectAppleSearchAdsAttribution() call successful'); + 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); + console.error( + '❌ [Qonversion] collectAppleSearchAdsAttribution() call failed:', + error + ); Alert.alert('Error', error.message); } }; @@ -79,15 +100,22 @@ const OtherScreen: React.FC = () => { return; } try { - console.log('🔄 [Qonversion] Starting presentCodeRedemptionSheet() call...'); + console.log( + '🔄 [Qonversion] Starting presentCodeRedemptionSheet() call...' + ); Qonversion.getSharedInstance().presentCodeRedemptionSheet(); - console.log('✅ [Qonversion] presentCodeRedemptionSheet() call successful'); + 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); + console.error( + '❌ [Qonversion] presentCodeRedemptionSheet() call failed:', + error + ); Alert.alert('Error', error.message); } }; @@ -97,11 +125,14 @@ const OtherScreen: React.FC = () => { } return ( - + Check Fallback File Accessibility - + Accessibility: { Collect Advertising ID - - Collect Apple Search Ads Attribution + + + Collect Apple Search Ads Attribution + - + Present Code Redemption Sheet diff --git a/example/src/screens/ProductDetailScreen/index.tsx b/example/src/screens/ProductDetailScreen/index.tsx index b532b0a4..a398d3cf 100644 --- a/example/src/screens/ProductDetailScreen/index.tsx +++ b/example/src/screens/ProductDetailScreen/index.tsx @@ -1,11 +1,5 @@ import React from 'react'; -import { - Text, - View, - TouchableOpacity, - Alert, - ScrollView, -} from 'react-native'; +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'; @@ -16,17 +10,26 @@ interface ProductDetailScreenProps { product: Product; } -const ProductDetailScreen: React.FC = ({ 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); + 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); + 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!', @@ -57,9 +60,14 @@ const ProductDetailScreen: React.FC = ({ product }) => }; return ( - + - {product.storeTitle || product.qonversionId} + + {product.storeTitle || product.qonversionId} + {product.prettyPrice && ( {product.prettyPrice} )} @@ -79,7 +87,10 @@ const ProductDetailScreen: React.FC = ({ product }) => {renderField('Pretty Price', product.prettyPrice)} {renderField('Price', product.price)} {renderField('Currency Code', product.currencyCode)} - {renderField('Pretty Introductory Price', product.prettyIntroductoryPrice)} + {renderField( + 'Pretty Introductory Price', + product.prettyIntroductoryPrice + )} @@ -94,7 +105,8 @@ const ProductDetailScreen: React.FC = ({ product }) => Subscription Period: - {product.subscriptionPeriod.unitCount} {product.subscriptionPeriod.unit} + {product.subscriptionPeriod.unitCount}{' '} + {product.subscriptionPeriod.unit} )} diff --git a/example/src/screens/ProductsScreen/index.tsx b/example/src/screens/ProductsScreen/index.tsx index f1f5c82d..4188d44a 100644 --- a/example/src/screens/ProductsScreen/index.tsx +++ b/example/src/screens/ProductsScreen/index.tsx @@ -1,12 +1,6 @@ import React from 'react'; -import { - Text, - View, - TouchableOpacity, - Alert, - ScrollView, -} from 'react-native'; -import Qonversion, { Product } from '@qonversion/react-native-sdk'; +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'; @@ -37,7 +31,10 @@ const ProductsScreen: React.FC = () => { } return ( - + Load Products diff --git a/example/src/screens/ProductsScreen/styles.ts b/example/src/screens/ProductsScreen/styles.ts index c59d6bfc..0ac93214 100644 --- a/example/src/screens/ProductsScreen/styles.ts +++ b/example/src/screens/ProductsScreen/styles.ts @@ -48,5 +48,4 @@ export default StyleSheet.create({ color: '#666666', marginBottom: 4, }, - }); diff --git a/example/src/screens/RemoteConfigsScreen/index.tsx b/example/src/screens/RemoteConfigsScreen/index.tsx index 30929539..cd33f6c0 100644 --- a/example/src/screens/RemoteConfigsScreen/index.tsx +++ b/example/src/screens/RemoteConfigsScreen/index.tsx @@ -29,14 +29,27 @@ const RemoteConfigsScreen: React.FC = () => { 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); + 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); + console.log( + '✅ [Qonversion] remoteConfigList call successful:', + remoteConfigs + ); } dispatch({ type: 'SET_REMOTE_CONFIGS', payload: remoteConfigs }); } catch (error: any) { @@ -49,9 +62,14 @@ const RemoteConfigsScreen: React.FC = () => { const loadSingleRemoteConfig = async () => { try { - console.log('🔄 [Qonversion] Starting remoteConfig call with contextKey:', singleContextKey); + console.log( + '🔄 [Qonversion] Starting remoteConfig call with contextKey:', + singleContextKey + ); dispatch({ type: 'SET_LOADING', payload: true }); - const config = await Qonversion.getSharedInstance().remoteConfig(singleContextKey || undefined); + 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]); @@ -66,30 +84,49 @@ const RemoteConfigsScreen: React.FC = () => { const attachToExperiment = async () => { try { - console.log('🔄 [Qonversion] Starting attachUserToExperiment call with experimentId:', experimentId, 'groupId:', groupId); - await Qonversion.getSharedInstance().attachUserToExperiment(experimentId, groupId); + 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); + 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] 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); + console.error( + '❌ [Qonversion] detachUserFromExperiment call failed:', + error + ); Alert.alert('Error', error.message); } }; @@ -99,7 +136,10 @@ const RemoteConfigsScreen: React.FC = () => { } return ( - + Context Keys (comma-separated): { onChangeText={setSingleContextKey} placeholder="Enter context key" /> - + Get Remote Config @@ -153,10 +196,18 @@ const RemoteConfigsScreen: React.FC = () => { {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)} + + Context Key: {config.source.contextKey || 'empty'} + + + Source: {config.source.name} + + + Type: {config.source.type} + + + Payload: {JSON.stringify(config.payload)} + ))} diff --git a/example/src/screens/UserScreen/index.tsx b/example/src/screens/UserScreen/index.tsx index bbf9da14..cf3ca6e7 100644 --- a/example/src/screens/UserScreen/index.tsx +++ b/example/src/screens/UserScreen/index.tsx @@ -7,7 +7,10 @@ import { ScrollView, TextInput, } from 'react-native'; -import Qonversion, { UserPropertyKey, AttributionProvider } from '@qonversion/react-native-sdk'; +import Qonversion, { + UserPropertyKey, + AttributionProvider, +} from '@qonversion/react-native-sdk'; import { AppContext } from '../../store/AppStore'; import SkeletonLoader from '../../components/SkeletonLoader'; import UserInfoSection from '../../components/UserInfoSection'; @@ -15,25 +18,32 @@ import styles from './styles'; import Snackbar from 'react-native-snackbar'; const UserScreen: React.FC = () => { - const context = React.useContext(AppContext); - if (!context) return null; - const { state, dispatch } = context; - 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('custom'); - const [selectedAttributionProvider, setSelectedAttributionProvider] = useState(AttributionProvider.APPSFLYER); + 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); + console.log( + '🔄 [Qonversion] Starting identify() call with identityId:', + identityId + ); dispatch({ type: 'SET_LOADING', payload: true }); - const userInfo = await Qonversion.getSharedInstance().identify(identityId); + const userInfo = + await Qonversion.getSharedInstance().identify(identityId); console.log('✅ [Qonversion] identify() call successful:', userInfo); dispatch({ type: 'SET_USER_INFO', payload: userInfo }); Snackbar.show({ @@ -53,10 +63,13 @@ const UserScreen: React.FC = () => { 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); + console.log( + '✅ [Qonversion] userInfo() call after logout successful:', + userInfo + ); dispatch({ type: 'SET_USER_INFO', payload: userInfo }); Snackbar.show({ text: 'User logged out successfully!', @@ -88,7 +101,10 @@ const UserScreen: React.FC = () => { 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); + console.log( + '✅ [Qonversion] userProperties() call successful:', + properties + ); setUserProperties(properties); Snackbar.show({ text: 'User properties loaded successfully!', @@ -108,9 +124,19 @@ const UserScreen: React.FC = () => { 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'); + 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, @@ -118,8 +144,16 @@ const UserScreen: React.FC = () => { } } 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] 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!', @@ -133,14 +167,20 @@ const UserScreen: React.FC = () => { } }; - - 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] 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!', @@ -179,7 +219,10 @@ const UserScreen: React.FC = () => { const canLogout = !!state.userInfo?.identityId; return ( - + @@ -219,14 +262,36 @@ const UserScreen: React.FC = () => { User Properties: - Key - Value + + Key + + + Value + {userProperties.properties.map((property: any, index: number) => ( - - {property.key} - {property.value} + + + {property.key} + + + {property.value} + ))} @@ -234,17 +299,27 @@ const UserScreen: React.FC = () => { )} Property Key: - + {/* UserPropertyKey Radio Buttons */} {Object.values(UserPropertyKey) - .filter(key => key !== UserPropertyKey.CUSTOM) + .filter((key) => key !== UserPropertyKey.CUSTOM) .map((key) => - renderRadioButton(key, key, selectedPropertyKey, setSelectedPropertyKey) + renderRadioButton( + key, + key, + selectedPropertyKey, + setSelectedPropertyKey + ) )} - {renderRadioButton('Custom (Manual Input)', 'custom', selectedPropertyKey, setSelectedPropertyKey)} + {renderRadioButton( + 'Custom (Manual Input)', + 'custom', + selectedPropertyKey, + setSelectedPropertyKey + )} - + {selectedPropertyKey === 'custom' && ( { placeholder="Enter custom property key" /> )} - + Property Value: { placeholder='{"key": "value"}' multiline /> - + Attribution Provider: - + {/* AttributionProvider Radio Buttons */} {Object.values(AttributionProvider).map((provider) => - renderRadioButton(provider, provider, selectedAttributionProvider, setSelectedAttributionProvider) + renderRadioButton( + provider, + provider, + selectedAttributionProvider, + setSelectedAttributionProvider + ) )} - + Send Attribution diff --git a/example/src/store/AppStore.ts b/example/src/store/AppStore.ts index 055b73f7..9f2d5c6f 100644 --- a/example/src/store/AppStore.ts +++ b/example/src/store/AppStore.ts @@ -1,5 +1,10 @@ import React from 'react'; -import { Offerings, Product, RemoteConfigList, User } from '@qonversion/react-native-sdk'; +import { + Offerings, + Product, + RemoteConfigList, + User, +} from '@qonversion/react-native-sdk'; import Entitlement from '../../../src/dto/Entitlement'; // Global Store (Redux-like pattern) @@ -12,7 +17,11 @@ export interface AppState { loading: boolean; navigationStack: string[]; noCodesEvents: string[]; - qonversionInitStatus: 'not_initialized' | 'initializing' | 'success' | 'error'; + qonversionInitStatus: + | 'not_initialized' + | 'initializing' + | 'success' + | 'error'; selectedProduct: Product | null; selectedEntitlement: Entitlement | null; isQonversionInitialized: boolean; @@ -29,7 +38,10 @@ export type AppAction = | { 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_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 }; @@ -64,26 +76,31 @@ export function appReducer(state: AppState, action: AppAction): AppState { case 'SET_LOADING': return { ...state, loading: action.payload }; case 'PUSH_SCREEN': - return { - ...state, - navigationStack: [...state.navigationStack, action.payload] + return { + ...state, + navigationStack: [...state.navigationStack, action.payload], }; case 'POP_SCREEN': - return { - ...state, - navigationStack: state.navigationStack.length > 1 - ? state.navigationStack.slice(0, -1) - : state.navigationStack + 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] + 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] }; + return { + ...state, + noCodesEvents: [...state.noCodesEvents, action.payload], + }; case 'SET_QONVERSION_INIT_STATUS': return { ...state, qonversionInitStatus: action.payload }; case 'SET_SELECTED_PRODUCT': From 6d91f41ac782bf210fe6599e61193cf6c51b4d0a Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Wed, 27 Aug 2025 00:26:18 +0300 Subject: [PATCH 3/3] Lockfile --- yarn.lock | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) 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"