diff --git a/android/build.gradle b/android/build.gradle index 81a0a2e0..461defa9 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -80,7 +80,7 @@ def kotlin_version = getExtOrDefault("kotlinVersion") dependencies { implementation "com.facebook.react:react-android" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "io.qonversion:sandwich:7.10.1" + implementation "io.qonversion:sandwich:7.11.0" } if (isNewArchitectureEnabled()) { diff --git a/example/src/App.tsx b/example/src/App.tsx index ee2ade15..58ae41d6 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -24,7 +24,6 @@ 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'; @@ -113,8 +112,6 @@ const App: React.FC = () => { ) : ( ); - case 'offerings': - return ; case 'remoteConfigs': return ; case 'user': diff --git a/example/src/index.ts b/example/src/index.ts index c8aa9dec..e6464d8c 100644 --- a/example/src/index.ts +++ b/example/src/index.ts @@ -14,7 +14,6 @@ export { default as SkeletonLoader } from './components/SkeletonLoader'; 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'; diff --git a/example/src/screens/MainScreen/index.tsx b/example/src/screens/MainScreen/index.tsx index 83f2dd13..6ab0f21d 100644 --- a/example/src/screens/MainScreen/index.tsx +++ b/example/src/screens/MainScreen/index.tsx @@ -21,7 +21,6 @@ const MainScreen: React.FC = () => { 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' }, diff --git a/example/src/screens/OfferingsScreen/index.tsx b/example/src/screens/OfferingsScreen/index.tsx deleted file mode 100644 index d4eea491..00000000 --- a/example/src/screens/OfferingsScreen/index.tsx +++ /dev/null @@ -1,82 +0,0 @@ -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 deleted file mode 100644 index f5140077..00000000 --- a/example/src/screens/OfferingsScreen/styles.ts +++ /dev/null @@ -1,113 +0,0 @@ -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/ProductDetailScreen/index.tsx b/example/src/screens/ProductDetailScreen/index.tsx index 7a0e7e30..2804f1f6 100644 --- a/example/src/screens/ProductDetailScreen/index.tsx +++ b/example/src/screens/ProductDetailScreen/index.tsx @@ -101,7 +101,6 @@ const ProductDetailScreen: React.FC = ({ {renderField('Store ID', product.storeId)} {renderField('Base Plan ID', product.basePlanId)} {renderField('Type', product.type)} - {renderField('Offering ID', product.offeringId)} diff --git a/example/src/store/AppStore.ts b/example/src/store/AppStore.ts index 9f2d5c6f..62ddbd45 100644 --- a/example/src/store/AppStore.ts +++ b/example/src/store/AppStore.ts @@ -1,6 +1,5 @@ import React from 'react'; import { - Offerings, Product, RemoteConfigList, User, @@ -10,7 +9,6 @@ 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; @@ -29,7 +27,6 @@ export interface AppState { 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 } @@ -48,7 +45,6 @@ export type AppAction = export const initialState: AppState = { products: null, - offerings: null, entitlements: null, remoteConfigs: null, userInfo: null, @@ -65,8 +61,6 @@ 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': diff --git a/package.json b/package.json index 3af5c8ad..d0b1fe0e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@qonversion/react-native-sdk", "title": "React Native Qonversion", - "version": "10.9.0", + "version": "10.10.0", "description": "Qonversion provides full in-app purchases infrastructure, so you do not need to build your own server for receipt validation. Implement in-app subscriptions, validate user receipts, check subscription status, and provide access to your app features and content using our StoreKit wrapper and Google Play Billing wrapper.", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/qonversion-react-native-sdk.podspec b/qonversion-react-native-sdk.podspec index 7504df7b..e89e2698 100644 --- a/qonversion-react-native-sdk.podspec +++ b/qonversion-react-native-sdk.podspec @@ -16,6 +16,6 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,cpp,swift}" s.private_header_files = "ios/**/*.h" - s.dependency "QonversionSandwich", "7.10.1" + s.dependency "QonversionSandwich", "7.11.0" install_modules_dependencies(s) end diff --git a/src/QonversionApi.ts b/src/QonversionApi.ts index 8395e515..c4df2bbd 100644 --- a/src/QonversionApi.ts +++ b/src/QonversionApi.ts @@ -111,9 +111,10 @@ export interface QonversionApi { * set of products with discounts later on if a user has not converted. * Offerings allow changing the products offered remotely without releasing app updates. * + * @deprecated Offerings are deprecated. Manage paywall products with Remote Configs instead: https://documentation.qonversion.io/docs/migrate-offerings-to-remote-configs * @returns the promise with Qonversion offerings * - * @see [Offerings](https://qonversion.io/docs/offerings) for more details + * @see [Migrate Offerings to Remote Configs](https://documentation.qonversion.io/docs/migrate-offerings-to-remote-configs) for more details */ offerings(): Promise; diff --git a/src/dto/Product.ts b/src/dto/Product.ts index 7cd45035..dd5101e1 100644 --- a/src/dto/Product.ts +++ b/src/dto/Product.ts @@ -11,10 +11,17 @@ class Product { storeId: string | null; /** - * Identifier of the base plan for Google product. + * Identifier of the base plan for a Google Play subscription product. + * Android only. Null for one-time products - use {@link purchaseOptionId} for those. */ basePlanId: string | null; + /** + * Identifier of the Google Play purchase option for a one-time (in-app) product. + * Android only. Null for subscription products - use {@link basePlanId} for those. + */ + purchaseOptionId: string | null; + /** * Google Play Store details of this product. * Android only. Null for iOS, or if the product was not found. @@ -66,6 +73,7 @@ class Product { qonversionId: string, storeId: string, basePlanId: string | null, + purchaseOptionId: string | null, storeDetails: ProductStoreDetails | null, skProduct: SKProduct | null, offeringId: string | null, @@ -82,6 +90,7 @@ class Product { this.qonversionId = qonversionId; this.storeId = storeId; this.basePlanId = basePlanId; + this.purchaseOptionId = purchaseOptionId; this.storeDetails = storeDetails; this.skProduct = skProduct; this.offeringId = offeringId; diff --git a/src/dto/storeProducts/ProductStoreDetails.ts b/src/dto/storeProducts/ProductStoreDetails.ts index 0a2526fe..98636243 100644 --- a/src/dto/storeProducts/ProductStoreDetails.ts +++ b/src/dto/storeProducts/ProductStoreDetails.ts @@ -8,11 +8,17 @@ import ProductInAppDetails from "./ProductInAppDetails"; */ class ProductStoreDetails { /** - * Identifier of the base plan to which these details relate. - * Null for in-app products. + * Identifier of the base plan for a Google Play subscription product. + * Null for one-time products - use {@link purchaseOptionId} for those. */ basePlanId: string | null; + /** + * Identifier of the Google Play purchase option for a one-time (in-app) product. + * Null for subscription products - use {@link basePlanId} for those. + */ + purchaseOptionId: string | null; + /** * Identifier of the subscription or the in-app product. */ @@ -111,6 +117,7 @@ class ProductStoreDetails { constructor( basePlanId: string | null, + purchaseOptionId: string | null, productId: string, name: string, title: string, @@ -129,6 +136,7 @@ class ProductStoreDetails { isInstallment: boolean, ) { this.basePlanId = basePlanId; + this.purchaseOptionId = purchaseOptionId; this.productId = productId; this.name = name; this.title = title; diff --git a/src/internal/Mapper.ts b/src/internal/Mapper.ts index 0ee2d367..4c4d37b0 100644 --- a/src/internal/Mapper.ts +++ b/src/internal/Mapper.ts @@ -63,6 +63,7 @@ export type QProduct = { id: string; storeId: string; basePlanId?: string | null; + purchaseOptionId?: string | null; type: string; subscriptionPeriod?: QSubscriptionPeriod | null; trialPeriod?: QSubscriptionPeriod | null; @@ -74,6 +75,7 @@ export type QProduct = { type QProductStoreDetails = { basePlanId?: string | null, + purchaseOptionId?: string | null, productId: string, name: string, title: string @@ -622,6 +624,7 @@ class Mapper { product.id, product.storeId, product.basePlanId ?? null, + product.purchaseOptionId ?? null, storeDetails, skProduct, offeringId, @@ -877,6 +880,7 @@ class Mapper { return new ProductStoreDetails( productStoreDetails.basePlanId ?? null, + productStoreDetails.purchaseOptionId ?? null, productStoreDetails.productId, productStoreDetails.name, productStoreDetails.title, diff --git a/src/internal/QonversionInternal.ts b/src/internal/QonversionInternal.ts index f5f9877f..f4599a45 100644 --- a/src/internal/QonversionInternal.ts +++ b/src/internal/QonversionInternal.ts @@ -25,7 +25,7 @@ import PromotionalOffer from '../dto/PromotionalOffer'; import RNQonversion from './specs/NativeQonversionModule'; import type { QPromoOfferDetails } from './specs/NativeQonversionModule'; -export const sdkVersion = "10.9.0"; +export const sdkVersion = "10.10.0"; export const sdkSource = "rn"; export default class QonversionInternal implements QonversionApi { @@ -246,6 +246,10 @@ export default class QonversionInternal implements QonversionApi { return mappedProducts; } + /** + * @deprecated Offerings are deprecated. Manage paywall products with Remote Configs instead: https://documentation.qonversion.io/docs/migrate-offerings-to-remote-configs + * @see [Migrate Offerings to Remote Configs](https://documentation.qonversion.io/docs/migrate-offerings-to-remote-configs) for more details + */ async offerings(): Promise { let offerings = await RNQonversion.offerings(); const mappedOfferings = Mapper.convertOfferings(offerings);