RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
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.

RevenueCat28StoreKit8Subscription23In-App Purchase7Troubleshooting38Rork515

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-05-04
Why Your Rork App Gets Rejected for Broken Restore Purchases — Fixing App Store Guideline 3.1.1
If your Rork-built subscription app is getting rejected for Guideline 3.1.1, the Restore Purchases button is almost always the cause. Learn how to wire it up correctly with expo-in-app-purchases or react-native-iap and pass review on the next try.
Dev Tools2026-07-15
A Yellow Warning in Dev, a Crash on Resume — Functions in Route Params
Rork generated navigation code that put a function and a Date into route params. In development it was only a warning. After the OS reclaimed the process, resuming the app crashed with params.onFavorite is not a function. Here is the cause and the fix.
Dev Tools2026-06-25
Why Paying Members See a Paywall in Airplane Mode — Keeping RevenueCat Entitlements Alive Offline
Open the app on a weak connection and a paying subscriber sees a paywall flash for a second. Here is how RevenueCat's customerInfo wavers on an offline launch, and a cache design that keeps entitlements valid with a trust window — written as working code for an Expo app.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →