Few things are more frustrating for both developers and users than completing a purchase only to find the app still showing the free tier. The App Store or Stripe dashboard confirms the transaction went through, but your app refuses to acknowledge it. This is one of the most common support requests for subscription-based Rork apps, and it usually comes down to one of a handful of specific implementation issues.
This guide walks through the most common reasons why purchase status fails to sync after payment in Rork apps using RevenueCat, along with concrete code fixes for each scenario.
Understanding How RevenueCat Delivers Status Updates
Before diving into fixes, it helps to understand how RevenueCat's customerInfo actually works. It's not a real-time push system — it combines a local cache with asynchronous server fetches.
When a user completes a purchase, the flow looks like this:
- User taps the purchase button
- App Store processes the transaction (takes a few seconds)
- RevenueCat SDK detects the completed transaction
- SDK notifies RevenueCat's servers, which update the Entitlement
customerInforeturns the updated state
The timing gap between steps 3 and 5 is where most sync issues occur. If your code checks subscription status before step 5 completes, it will return stale data.
Issue 1: Checking Status Immediately After purchasePackage
This is the most frequent cause. Calling getCustomerInfo right after purchasePackage often returns cached data that hasn't reflected the new purchase yet.
// ❌ Common mistake — fetching customerInfo separately after purchase
const handlePurchase = async () => {
await Purchases.shared.purchasePackage(selectedPackage);
// getCustomerInfo here may return stale cache
const info = await Purchases.shared.getCustomerInfo();
if (info.entitlements.active["premium"]) {
unlockPremiumFeatures(); // May not trigger because status isn't updated yet
}
};The fix is simple: use the return value from purchasePackage itself. It returns the freshest CustomerInfo available at the moment the purchase completes.
// ✅ Correct approach — use the return value from purchasePackage
const handlePurchase = async () => {
try {
const { customerInfo } = await Purchases.shared.purchasePackage(selectedPackage);
// customerInfo here reflects the completed purchase
if (customerInfo.entitlements.active["Premium"]) {
unlockPremiumFeatures();
}
} catch (error) {
if (error.code \!== PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
console.error("Purchase failed:", error.message);
showErrorAlert(error.message);
}
}
};Issue 2: Entitlement ID Mismatch
This one catches many developers off guard. RevenueCat Entitlement IDs are case-sensitive. If you set up "Premium" in the RevenueCat dashboard but reference "premium" in your code, the check will always fail silently.
// ❌ Wrong — "premium" vs "Premium" in the dashboard
if (info.entitlements.active["premium"]) { ... }
// ✅ Correct — must match the dashboard exactly
if (info.entitlements.active["Premium"]) { ... }When debugging, always log the actual active entitlements to confirm the exact key:
const info = await Purchases.shared.getCustomerInfo();
console.log("Active entitlements:", JSON.stringify(info.entitlements.active));
// This shows you exactly what keys are presentIssue 3: Sandbox Environment Delays
During development and testing with Sandbox accounts, RevenueCat's server processing can be slower than in production. This is especially noticeable in two situations:
- Right after the first Sandbox purchase
- During Sandbox subscription renewal (iOS compresses renewal intervals for testing)
If something seems broken in testing, wait 5–10 seconds and retry before assuming there's a code issue. The same behavior often doesn't occur in production.
Also check whether you're using the correct API key. RevenueCat typically requires separate API keys for Sandbox and production:
// Toggle API key based on environment
const revenueCatApiKey = __DEV__
? "appl_YOUR_SANDBOX_API_KEY"
: "appl_YOUR_PRODUCTION_API_KEY";
await Purchases.configure({ apiKey: revenueCatApiKey });Confirm in the RevenueCat Dashboard under Sandbox purchases that the transaction is actually arriving. If it's not there, the SDK configuration is the issue.
Issue 4: Missing customerInfo Update Listener
RevenueCat provides a listener that fires whenever customerInfo changes — whether triggered by a foreground purchase, a webhook, or a background renewal. Without this listener, background subscription updates never reach your UI.
// ✅ Set up listener at app initialization
import Purchases, { CustomerInfo } from "react-native-purchases";
import { useEffect, useState } from "react";
const useSubscriptionStatus = () => {
const [isPremium, setIsPremium] = useState(false);
useEffect(() => {
// Initial fetch on mount
Purchases.shared.getCustomerInfo().then((info) => {
setIsPremium(\!\!info.entitlements.active["Premium"]);
});
// Listen for future updates
const listener = Purchases.addCustomerInfoUpdateListener(
(info: CustomerInfo) => {
setIsPremium(\!\!info.entitlements.active["Premium"]);
}
);
return () => {
listener.remove();
};
}, []);
return isPremium;
};Using this hook in your app ensures that subscription status stays current across purchases, renewals, and cancellations — without requiring the user to restart the app.
Issue 5: Subscription State Resets on App Restart
If you're storing subscription status only in component state (e.g., useState), it resets every time the app restarts. Users who purchased a subscription will appear as free users until they purchase again.
Always fetch fresh customerInfo at app startup:
// ✅ Check subscription status on every app launch
const initializeRevenueCat = async () => {
try {
await Purchases.configure({ apiKey: revenueCatApiKey });
const info = await Purchases.shared.getCustomerInfo();
const isPremium = \!\!info.entitlements.active["Premium"];
// Update your global state or context here
setGlobalPremiumStatus(isPremium);
} catch (error) {
// On error, default to cached state if available
console.warn("Failed to fetch subscription status:", error.message);
}
};
// Call this in App.tsx or your root component
useEffect(() => {
initializeRevenueCat();
}, []);Quick Debugging Checklist
When subscription status isn't reflecting after purchase, work through these steps in order:
Step 1: Check RevenueCat Dashboard Navigate to Customers and search for the user. If the purchase isn't showing up, the problem is in your SDK configuration, not your UI logic.
Step 2: Log active entitlements
Print JSON.stringify(info.entitlements.active) to confirm the exact Entitlement ID keys. Compare these against what your code is checking.
Step 3: Use the purchasePackage return value
Stop calling getCustomerInfo separately after purchase. The purchasePackage return value has the most up-to-date customerInfo.
Step 4: Add the update listener
Make sure addCustomerInfoUpdateListener is registered early in your app's lifecycle.
Step 5: Fetch status on app launch
Add a getCustomerInfo call in your app initialization so returning users see correct status immediately.
Final Thoughts
Subscription sync bugs are particularly damaging because they occur at the exact moment a user is trying to engage with your app's premium features. Getting this right protects both your revenue and your ratings.
The combination of using purchasePackage's return value for immediate updates, setting up the update listener for async changes, and fetching fresh status on launch covers the vast majority of sync scenarios. If you're still seeing issues after working through these fixes, enable RevenueCat's debug logging with Purchases.setLogLevel(LOG_LEVEL.DEBUG) to trace exactly what the SDK is doing during the purchase flow.
Handling Restore Purchases Correctly
One scenario that often gets overlooked: users who reinstall the app or switch to a new device expect to restore their previous purchases. If your restore flow has the same timing issues as the initial purchase flow, users will hit the same wall.
// ✅ Correct restore purchases implementation
const handleRestorePurchases = async () => {
try {
setIsLoading(true);
const info = await Purchases.shared.restorePurchases();
// restorePurchases also returns fresh customerInfo
if (info.entitlements.active["Premium"]) {
setIsPremium(true);
showSuccessAlert("Your subscription has been restored.");
} else {
showInfoAlert("No active subscription found for this account.");
}
} catch (error) {
console.error("Restore failed:", error.message);
showErrorAlert("Restore failed. Please try again.");
} finally {
setIsLoading(false);
}
};A common mistake is calling getCustomerInfo after restorePurchases instead of using the return value — exactly the same pattern as the initial purchase issue.
Handling the App Returning to Foreground
When a user leaves your app mid-purchase — to complete authentication in Safari, for example — the subscription state when they return is often stale. Setting up an AppState listener to refresh customerInfo when the app comes back to the foreground handles this gracefully.
import { AppState, AppStateStatus } from "react-native";
useEffect(() => {
const handleAppStateChange = async (nextState: AppStateStatus) => {
if (nextState === "active") {
// App just came to foreground — refresh subscription status
try {
const info = await Purchases.shared.getCustomerInfo();
setIsPremium(\!\!info.entitlements.active["Premium"]);
} catch (error) {
console.warn("Failed to refresh on foreground:", error.message);
}
}
};
const subscription = AppState.addEventListener("change", handleAppStateChange);
return () => {
subscription.remove();
};
}, []);This pattern is especially important for apps where users authenticate via an external service (Google, Apple, etc.) before completing a purchase. The external redirect means your app will return from background right as the purchase completes.
Testing Your Implementation End-to-End
Before shipping, verify your subscription flow handles all these cases correctly using StoreKit's built-in testing tools in Xcode. Create a StoreKit configuration file and use it in your scheme to simulate purchases without real transactions.
For automated testing, RevenueCat provides a staging environment you can use to simulate purchases, renewals, and cancellations without App Store involvement. This is worth setting up before any major release.
A minimal test checklist for subscription sync:
- Purchase completes and premium features unlock immediately (no app restart required)
- App restart preserves premium status
- Restore purchases works for reinstalls and device transfers
- Cancellation is reflected within a reasonable time after the subscription expires
- Sandbox renewal behavior (compressed intervals) matches expected behavior
Getting all five of these working correctly puts you in a solid position before submitting to the App Store.