◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-04-14Intermediate

Purchase Status Not Updating in Rork — Debugging RevenueCat & StoreKit Sync Issues

Fix the frustrating issue of subscription status not reflecting after purchase in Rork apps. Step-by-step debugging guide covering RevenueCat customerInfo timing, Entitlement ID mismatches, Sandbox delays, and listener setup.

RevenueCat30StoreKit8Subscription23In-App Purchase7Troubleshooting43Rork585

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:

  1. User taps the purchase button
  2. App Store processes the transaction (takes a few seconds)
  3. RevenueCat SDK detects the completed transaction
  4. SDK notifies RevenueCat's servers, which update the Entitlement
  5. customerInfo returns 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 present

Issue 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.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-10-15
When EXPO_PUBLIC_ Values Read Fine in Dev but Come Back undefined in Production: Telling the Spellings Apart
The API URL reads fine while developing and turns undefined once the app is built. The cause was how process.env was written. I lined up babel-preset-expo's output for dev and production and tabulated which spellings get replaced and which are left behind.
⬡ Dev Tools2026-09-21
A build that stops with exit code 0 — the flag that silenced the log, and the check that let an empty file through
Two reports landed in the same week: eas build --local stopping with exit code 0 but produced no further output, and a zero-byte stub that never got embedded. Both came down to settings that reduce output. Here is how to audit your own scripts.
⬡ Dev Tools2026-06-17
Your Rork App Is Stuck on "Processing" in App Store Connect — A Field Guide to Getting It Unstuck
You uploaded a Rork-built app to App Store Connect, but the build sits on Processing for hours and never shows up in TestFlight or for submission. Here is the exact diagnostic order I follow, how to read the ITMS error codes, and a short script that reads the processing state from the API — with working code.