Hello! Thank you so much for replying.
I am testing on a physical device. All my product are currently not found.
07-26 12:32:29.824 20834 20916 I ReactNativeJS: [RevenueCat] ℹ️ Missing productDetails: UnfetchedProduct{productId='starwars1', productType='subs', statusCode=3}
I have double checked all play console setups.
I have deleted all imported products in RevenueCat and recreated them to ensure they are set to non-consumable.
I now have my ACTUAL subscription working as intended. Also on IOS, all products/subscriptions are working as intended.
I am signed in as a license tester. A Different account than my developer account and signed into the proper account on the physical device.
These are from my StoreScreen.js, I can share my RevenueCatManager.js as well if need be, let me know how you would like me to share that file if so. Btw, I this is since I’ve been testing direct purchases and not offer based, I attempted offer based purchases and couldn’t get them to work either.
// Main purchase method - handles both subscriptions and individual IAPs
async purchaseProductWithCallback(productId, onComplete) {
debugLog('PURCHASE', `Starting DIRECT purchase for: ${productId}`);
try {
if (!this.isInitialized) {
const initResult = await this.initialize();
if (!initResult) {
onComplete({ success: false, error: 'RevenueCat not initialized', cancelled: false });
return false;
}
}
// For subscriptions, use offering-based purchase (THESE WORK)
if (productId === PRODUCT_IDS.SUBSCRIPTION_MONTHLY) {
debugLog('PURCHASE', 'Subscription detected, using offering-based purchase...');
return await this.purchaseSubscriptionViaOffering(productId, onComplete);
}
// For individual IAPs, use direct product purchase (THESE DON'T WORK ON ANDROID)
debugLog('PURCHASE', 'Attempting DIRECT product purchase (bypassing offerings)...');
const { customerInfo } = await Purchases.purchaseProduct(productId);
debugLog('PURCHASE', 'Direct purchase successful!', {
productId,
newActiveEntitlements: Object.keys(customerInfo.entitlements.active)
});
this.customerInfo = customerInfo;
onComplete({ success: true, productId: productId });
return true;
} catch (error) {
debugLog('PURCHASE', '❌ Direct purchase failed', {
productId,
error: error.message,
code: error.code,
underlyingErrorMessage: error.userInfo?.underlyingErrorMessage
});
const isUserCancelled = error.code === Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED;
onComplete({
success: false,
error: error.message,
cancelled: isUserCancelled,
productId: productId
});
return false;
}
}
// Subscription purchase via offerings - WORKS PERFECTLY
async purchaseSubscriptionViaOffering(productId, onComplete) {
try {
const offerings = await Purchases.getOfferings();
if (!offerings?.current?.availablePackages) {
throw new Error('No subscription offerings available');
}
const monthlyPackage = offerings.current.availablePackages.find(pkg =>
pkg.packageType === 'MONTHLY' || pkg.storeProduct?.identifier === productId
);
if (!monthlyPackage) {
throw new Error('Monthly subscription package not found');
}
// This works perfectly on Android
const { customerInfo } = await Purchases.purchasePackage(monthlyPackage);
this.customerInfo = customerInfo;
onComplete({ success: true, productId: productId });
return true;
} catch (error) {
debugLog('PURCHASE', '❌ Subscription purchase failed:', error.message);
throw error;
}
}
ProductIDs:
export const PRODUCT_IDS = {
// Individual IAPs (these don't work on Android with direct purchase)
PACK_HARRYPOTTER: Platform.select({
ios: 'HarryPotter1',
android: 'harrypotter1'
}),
PACK_STARWARS: Platform.select({
ios: 'StarWars1',
android: 'starwars1'
}),
EVERYTHING_BUNDLE: Platform.select({
ios: 'EverythingBundle1',
android: 'everythingbundle1'
}),
// Subscription (this works fine)
SUBSCRIPTION_MONTHLY: Platform.select({
ios: 'TriviadareBase',
android: 'trivia_dare_monthly'
})
};
How They Are Initiaed in the UI:
const handlePurchase = async (packId, packType = 'trivia') => {
try {
setLoadingPurchases(prev => ({ ...prev, [packId]: true }));
let productId;
if (packType === 'trivia') {
productId = revenueCatManager.PACK_TO_PRODUCT_MAP[packId];
} else if (packType === 'bundle') {
productId = revenueCatManager.PRODUCT_IDS.EVERYTHING_BUNDLE;
}
if (!productId) {
Alert.alert('Error', 'Product not found');
return;
}
// This calls the main purchase method above
const success = await revenueCatManager.purchaseProductWithCallback(
productId,
(result) => {
setLoadingPurchases(prev => ({ ...prev, [packId]: false }));
if (result.success) {
loadPurchaseStates();
}
}
);
} catch (error) {
console.error('Purchase error:', error);
Alert.alert('Purchase Error', 'Something went wrong. Please try again.');
}
};
RevenueCat Initialization:
async initialize() {
if (this.isInitialized) {
return true;
}
try {
const apiKey = Platform.OS === 'ios'
? REVENUECAT_API_KEYS.ios
: REVENUECAT_API_KEYS.android;
await Purchases.configure({ apiKey });
Purchases.addCustomerInfoUpdateListener(this.handleCustomerInfoUpdate.bind(this));
this.customerInfo = await Purchases.getCustomerInfo();
this.isInitialized = true;
return true;
} catch (error) {
console.error('❌ RevenueCat initialization failed:', error);
this.isInitialized = false;
return false;
}
}
Here is some snippets from my RevenueCatManager.js
// DIRECT PURCHASE: Main purchase method using direct product purchases
async purchaseProductWithCallback(productId, onComplete) {
debugLog('PURCHASE', `Starting DIRECT purchase for: ${productId}`);
try {
if (!this.isInitialized) {
debugLog('PURCHASE', 'RevenueCat not initialized, initializing now...');
const initResult = await this.initialize();
if (!initResult) {
debugLog('PURCHASE', '❌ Initialization failed, aborting purchase');
onComplete({ success: false, error: 'RevenueCat not initialized', cancelled: false });
return false;
}
}
// For subscriptions, use offering-based purchase (they need special handling)
if (productId === PRODUCT_IDS.SUBSCRIPTION_MONTHLY) {
debugLog('PURCHASE', 'Subscription detected, using offering-based purchase...');
return await this.purchaseSubscriptionViaOffering(productId, onComplete);
}
debugLog('PURCHASE', 'Checking if already purchased...');
if (await this.isPurchased(productId)) {
debugLog('PURCHASE', '❌ Product already purchased, aborting');
onComplete({ success: false, error: 'Already purchased', cancelled: false });
Alert.alert('Already Purchased', 'You have already purchased this content.', [{ text: 'OK' }]);
return false;
}
debugLog('PURCHASE', 'Attempting DIRECT product purchase (bypassing offerings)...');
// 🎯 THIS IS THE LINE THAT FAILS ON ANDROID FOR INDIVIDUAL IAPS
const { customerInfo } = await Purchases.purchaseProduct(productId);
debugLog('PURCHASE', 'Direct purchase successful!', {
productId,
newActiveEntitlements: Object.keys(customerInfo.entitlements.active),
newNonSubscriptionTransactions: customerInfo.nonSubscriptionTransactions.length
});
this.customerInfo = customerInfo;
await this.trackPurchaseAchievements(productId);
onComplete({ success: true, productId: productId });
console.log('✅ Purchase successful:', productId);
const successMessage = this.getSuccessMessage(productId);
Alert.alert('Purchase Successful!', successMessage, [{ text: 'OK' }]);
return true;
} catch (error) {
debugLog('PURCHASE', '❌ Direct purchase failed', {
productId,
error: error.message,
code: error.code,
underlyingErrorMessage: error.userInfo?.underlyingErrorMessage
});
console.error('❌ Purchase failed:', error);
const isUserCancelled = error.code === Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED;
onComplete({
success: false,
error: error.message,
cancelled: isUserCancelled,
productId: productId
});
if (!isUserCancelled) {
this.handlePurchaseError(error);
}
return false;
}
}
// SUBSCRIPTION PURCHASE: Still use offerings for subscriptions
async purchaseSubscriptionViaOffering(productId, onComplete) {
debugLog('PURCHASE', `Purchasing subscription via offering: ${productId}`);
try {
const offerings = await Purchases.getOfferings();
if (!offerings?.current?.availablePackages) {
throw new Error('No subscription offerings available');
}
// Find monthly package
const monthlyPackage = offerings.current.availablePackages.find(pkg =>
pkg.packageType === 'MONTHLY' || pkg.storeProduct?.identifier === productId
);
if (!monthlyPackage) {
throw new Error('Monthly subscription package not found');
}
debugLog('PURCHASE', 'Found subscription package:', {
packageId: monthlyPackage.identifier,
packageType: monthlyPackage.packageType,
productId: monthlyPackage.storeProduct?.identifier
});
// THIS WORKS PERFECTLY ON ANDROID
const { customerInfo } = await Purchases.purchasePackage(monthlyPackage);
debugLog('PURCHASE', 'Subscription purchase successful!', {
packageId: monthlyPackage.identifier,
newActiveEntitlements: Object.keys(customerInfo.entitlements.active)
});
this.customerInfo = customerInfo;
await this.trackPurchaseAchievements(productId);
onComplete({ success: true, productId: productId });
console.log('✅ Subscription purchase successful:', productId);
Alert.alert('Subscription Activated!', 'You now have full access to all content.', [{ text: 'OK' }]);
return true;
} catch (error) {
debugLog('PURCHASE', '❌ Subscription purchase failed:', error.message);
throw error;
}
}
// Product IDs (keeping exact same IDs for compatibility)
export const PRODUCT_IDS = {
PACK_HARRYPOTTER: Platform.select({
ios: 'HarryPotter1',
android: 'harrypotter1'
}),
PACK_MARVELCINAMATICUNIVERSE: Platform.select({
ios: 'MarvelCinematicUniverse1',
android: 'marvelcinematicuniverse1'
}),
PACK_STARWARS: Platform.select({
ios: 'StarWars1',
android: 'starwars1'
}),
PACK_DISNEYANIMATEDMOVIES: Platform.select({
ios: 'DisneyAnimatedMovies1',
android: 'disneyanimatedmovies1'
}),
// ... more individual IAP products
EVERYTHING_BUNDLE: Platform.select({
ios: 'EverythingBundle1',
android: 'everythingbundle1'
}),
SUBSCRIPTION_MONTHLY: Platform.select({
ios: 'TriviadareBase',
android: 'trivia_dare_monthly'
})
};
//PROPER ENTITLEMENT ARCHITECTURE
export const ENTITLEMENTS = {
// Full access (subscription + everything bundle only)
FULL_ACCESS: 'TriviaDareFullAccess',
// Individual trivia pack entitlements (create these in RevenueCat Dashboard)
ANIME: 'Anime',
FRIENDS: 'Friends',
HARRY_POTTER: 'Harry Potter',
MARVEL: 'MarvelCinematicUniverse',
STAR_WARS: 'Star Wars',
DISNEY: 'DisneyAnimatedMovies',
// ... more individual entitlements
};
// Product to Individual Entitlement Mapping
export const PRODUCT_TO_ENTITLEMENT_MAP = {
// Trivia Packs → Individual Entitlements
[PRODUCT_IDS.PACK_ANIME]: ENTITLEMENTS.ANIME,
[PRODUCT_IDS.PACK_FRIENDS]: ENTITLEMENTS.FRIENDS,
[PRODUCT_IDS.PACK_HARRYPOTTER]: ENTITLEMENTS.HARRY_POTTER,
[PRODUCT_IDS.PACK_MARVELCINAMATICUNIVERSE]: ENTITLEMENTS.MARVEL,
[PRODUCT_IDS.PACK_STARWARS]: ENTITLEMENTS.STAR_WARS,
[PRODUCT_IDS.PACK_DISNEYANIMATEDMOVIES]: ENTITLEMENTS.DISNEY,
// ... more mappings
};
// CHECK PURCHASE STATUS: Updated for proper entitlement logic
async isPurchased(productId) {
debugLog('CHECK', `Checking purchase status for: ${productId}`);
try {
if (!this.customerInfo) {
debugLog('CHECK', 'No customer info, fetching...');
if (!this.isInitialized) {
await this.initialize();
}
this.customerInfo = await Purchases.getCustomerInfo();
}
// 1. Check if user has full access (ONLY from subscription or everything bundle)
const hasFullAccess = this.customerInfo.entitlements.active[ENTITLEMENTS.FULL_ACCESS];
if (hasFullAccess) {
debugLog('CHECK', '✅ Product available via full access entitlement (subscription or bundle)');
return true;
}
// 2. Check individual product entitlement
const individualEntitlement = PRODUCT_TO_ENTITLEMENT_MAP[productId];
if (individualEntitlement) {
const hasIndividualAccess = this.customerInfo.entitlements.active[individualEntitlement];
if (hasIndividualAccess) {
debugLog('CHECK', `✅ Product available via individual entitlement: ${individualEntitlement}`);
return true;
}
}
// 3. Fallback: Check direct product purchases (for legacy/edge cases)
const hasActiveSubscription = this.customerInfo.activeSubscriptions.includes(productId);
const hasNonSubscriptionTransaction = this.customerInfo.nonSubscriptionTransactions.some(t => t.productId === productId);
if (hasActiveSubscription || hasNonSubscriptionTransaction) {
debugLog('CHECK', '✅ Product found via direct purchase/subscription (fallback)');
return true;
}
debugLog('CHECK', '❌ Product not purchased', {
productId,
checkedFullAccess: !!hasFullAccess,
checkedIndividualEntitlement: individualEntitlement,
hasIndividualEntitlement: individualEntitlement ? !!this.customerInfo.entitlements.active[individualEntitlement] : false,
allActiveEntitlements: Object.keys(this.customerInfo.entitlements.active)
});
return false;
} catch (error) {
debugLog('CHECK', '❌ Error checking purchase status', {
productId,
error: error.message
});
return false;
}
}
// INITIALIZE: Simple and reliable
async initialize() {
debugLog('INIT', 'initialize() called', {
isInitialized: this.isInitialized,
platform: Platform.OS
});
if (this.isInitialized) {
debugLog('INIT', 'Already initialized, skipping');
return true;
}
try {
debugLog('INIT', 'Starting RevenueCat initialization...');
const apiKey = Platform.OS === 'ios'
? REVENUECAT_API_KEYS.ios
: REVENUECAT_API_KEYS.android;
debugLog('INIT', 'API Key selected', {
platform: Platform.OS,
keyExists: !!apiKey,
keyLength: apiKey ? apiKey.length : 0,
keyPrefix: apiKey ? apiKey.substring(0, 10) + '...' : 'none'
});
if (!apiKey || apiKey.includes('YOUR_') || apiKey.includes('_HERE')) {
debugLog('INIT', '❌ API key validation failed');
console.error('❌ RevenueCat API key not configured!');
return false;
}
debugLog('INIT', 'Configuring RevenueCat with API key...');
await Purchases.configure({ apiKey });
debugLog('INIT', 'RevenueCat configured, setting up listener...');
Purchases.addCustomerInfoUpdateListener(this.handleCustomerInfoUpdate.bind(this));
debugLog('INIT', 'Getting initial customer info...');
this.customerInfo = await Purchases.getCustomerInfo();
debugLog('INIT', 'Initial customer info retrieved', {
hasCustomerInfo: !!this.customerInfo,
originalAppUserId: this.customerInfo?.originalAppUserId,
activeEntitlements: this.customerInfo ? Object.keys(this.customerInfo.entitlements.active) : [],
activeSubscriptions: this.customerInfo?.activeSubscriptions || [],
nonSubscriptionTransactions: this.customerInfo?.nonSubscriptionTransactions?.length || 0
});
this.isInitialized = true;
debugLog('INIT', '✅ RevenueCat initialized successfully!');
return true;
} catch (error) {
debugLog('INIT', '❌ RevenueCat initialization failed', {
error: error.message,
code: error.code
});
console.error('❌ RevenueCat initialization failed:', error);
this.isInitialized = false;
return false;
}
}
- Why does
Purchases.purchasePackage(package) work for subscriptions but Purchases.purchaseProduct(productId) fail for individual IAPs on Android? - Are there Android-specific requirements for individual IAP products that I'm missing?
- Is there a difference in how Google Play handles subscriptions vs individual IAPs through RevenueCat?