From 1969b475a7c4e16117dc71939ca4fdab28e6b763 Mon Sep 17 00:00:00 2001 From: Sam Mejl Date: Sat, 18 Jul 2026 18:43:20 +0100 Subject: [PATCH 1/6] chore: deprecate public Offerings API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Offerings are a legacy Qonversion feature superseded by Remote Configs. Mark the public `offerings()` method as `@deprecated` on both the `QonversionApi` interface and its `QonversionInternal` implementation, and point the doc-comment `@see` link to the migration guide. Behavior is unchanged — this is a documentation-only deprecation to guide integrators toward Remote Configs. Co-Authored-By: Claude Fable 5 --- src/QonversionApi.ts | 3 ++- src/internal/QonversionInternal.ts | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) 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/internal/QonversionInternal.ts b/src/internal/QonversionInternal.ts index f5f9877f..cb52e5e1 100644 --- a/src/internal/QonversionInternal.ts +++ b/src/internal/QonversionInternal.ts @@ -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); From 5fc342434d618870ed2d1d3730beb5e4207d65d2 Mon Sep 17 00:00:00 2001 From: Sam Mejl Date: Sun, 19 Jul 2026 13:03:45 +0100 Subject: [PATCH 2/6] chore(example): remove legacy Offerings flow from sample app Offerings are being sunset in favor of Remote Configs. Drop the Offerings catalog from the example app so it no longer demonstrates the legacy flow. Products are now surfaced only via Qonversion.getSharedInstance().products(). - Delete OfferingsScreen (screen + styles) and its route/navigation - Remove offerings state, action and reducer branch from AppStore - Remove the Offerings menu item from MainScreen - Drop the Offering ID field from ProductDetailScreen - Remove the OfferingsScreen export from the example index See: https://documentation.qonversion.io/docs/migrate-offerings-to-remote-configs Co-Authored-By: Claude Fable 5 --- example/src/App.tsx | 3 - example/src/index.ts | 1 - example/src/screens/MainScreen/index.tsx | 1 - example/src/screens/OfferingsScreen/index.tsx | 82 ------------- example/src/screens/OfferingsScreen/styles.ts | 113 ------------------ .../src/screens/ProductDetailScreen/index.tsx | 1 - example/src/store/AppStore.ts | 6 - 7 files changed, 207 deletions(-) delete mode 100644 example/src/screens/OfferingsScreen/index.tsx delete mode 100644 example/src/screens/OfferingsScreen/styles.ts 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': From 9f405c476c747845c16f8d3c13d5dc788f791dde Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Tue, 21 Jul 2026 18:24:42 +0300 Subject: [PATCH 3/6] DEV-1215: expose purchaseOptionId on Product (React Native) Add a dedicated purchaseOptionId field for one-time products, parsed from the bridge map in Mapper.convertProduct, mirroring basePlanId. basePlanId now carries the subscription base plan only (null for one-time); purchaseOptionId carries the one-time purchase option (null for subscriptions). Co-Authored-By: Claude Opus 4.8 --- src/dto/Product.ts | 11 ++++++++++- src/internal/Mapper.ts | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) 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/internal/Mapper.ts b/src/internal/Mapper.ts index 0ee2d367..a010ae11 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; @@ -622,6 +623,7 @@ class Mapper { product.id, product.storeId, product.basePlanId ?? null, + product.purchaseOptionId ?? null, storeDetails, skProduct, offeringId, From 7dfaf25f4199cabc118330c9caa62a674ab0aa2c Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Tue, 21 Jul 2026 18:47:45 +0300 Subject: [PATCH 4/6] DEV-1215: expose purchaseOptionId on ProductStoreDetails too Carry the split down to product.storeDetails for consistency with product: add a purchaseOptionId field parsed from the bridge map, mirroring basePlanId (which the native side now reports as null for one-time products). Co-Authored-By: Claude Opus 4.8 --- src/dto/storeProducts/ProductStoreDetails.ts | 12 ++++++++++-- src/internal/Mapper.ts | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) 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 a010ae11..4c4d37b0 100644 --- a/src/internal/Mapper.ts +++ b/src/internal/Mapper.ts @@ -75,6 +75,7 @@ export type QProduct = { type QProductStoreDetails = { basePlanId?: string | null, + purchaseOptionId?: string | null, productId: string, name: string, title: string @@ -879,6 +880,7 @@ class Mapper { return new ProductStoreDetails( productStoreDetails.basePlanId ?? null, + productStoreDetails.purchaseOptionId ?? null, productStoreDetails.productId, productStoreDetails.name, productStoreDetails.title, From 4a0fdadee688bd099d77ab8d447ec4833a317bea Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Tue, 21 Jul 2026 20:33:05 +0300 Subject: [PATCH 5/6] chore(deps): bump QonversionSandwich to 7.11.0 (DEV-1215 purchaseOptionId) Co-Authored-By: Claude Opus 4.8 --- android/build.gradle | 2 +- qonversion-react-native-sdk.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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 From 5f3140aacd9e338d66254af2ea881580027e6106 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 21 Jul 2026 17:54:03 +0000 Subject: [PATCH 6/6] Bump version to 10.10.0 --- package.json | 2 +- src/internal/QonversionInternal.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/src/internal/QonversionInternal.ts b/src/internal/QonversionInternal.ts index cb52e5e1..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 {