Testing & Troubleshooting

Documentation Unreal Engine Testing Troubleshooting

14-phase test plan, edge cases, logging tips, and common issues for RevenueCat Bridge.


Before You Start Testing

Make sure all of this is in place:
  • RevenueCat dashboard: project created, API keys generated
  • Google Play Console: app created, internal testing track set up, license testers added
  • App Store Connect (if iOS): sandbox testers created in Users & Access
  • Products created in the store console AND linked in RevenueCat dashboard
  • At least one Offering with packages configured in RevenueCat
  • Entitlement(s) configured and linked to products (e.g. "pro")
  • Plugin settings filled: AndroidApiKey, IosApiKey
  • bEnableDebugLogs = true in Project Settings > Plugins > RevenueCat
  • Build deployed to the device via internal testing track (Android) or TestFlight/Xcode (iOS)
Purchases don't work on emulators or in the editor. You need a real device.

Quick Smoke Test

If you're short on time, run these in order for a minimal confidence check:
  1. App launches, SDK configures (check logs for [RevenueCat] SDK configured successfully.)
  2. Fetch offerings, verify products show up with correct prices
  3. Purchase a subscription, verify entitlement becomes active
  4. Cancel a purchase, verify the cancelled callback fires
  5. Fetch customer info
  6. Kill app, relaunch, verify entitlement persists without re-purchasing
  7. Clear app data, restore purchases
  8. Airplane mode, verify error handling
  9. Log in / log out cycle
If all 9 pass, you're in good shape.

Full Test Plan

Phase 1 - SDK Initialization

Launch the app from a cold start (first install or cleared data).
#TestExpectedLog to check
1.1App launches on deviceNo crash, game loads[RevenueCat] SDK configured successfully.
1.2Check IsConfigured() after launchReturns true-
1.3Check GetAppUserID()Returns a non-empty anonymous ID (starts with $RCAnonymousID:)Log the returned string
1.4Check IsAnonymous()Returns true-
If it goes wrong:
  • SDK configured log never appears - wrong API key, no internet, or ConfigureSDK not being called
  • Crash on startup - JNI or Obj-C bridge issue, check Logcat or Xcode console for native exceptions

Phase 2 - Fetch Offerings

#TestExpectedLog to check
2.1Call FetchOfferings()OnSuccess fires[RevenueCat] Fetched N offerings. where N > 0
2.2Inspect returned offeringsAt least one offering existsLog OfferingID, DisplayName, package count
2.3Check bIsCurrent flagExactly one offering has bIsCurrent = true-
2.4Inspect packagesEach package has a non-empty PackageID, PackageType, and valid ProductLog each package's ID and type
2.5Inspect product detailsProductID, Title, PriceString, PriceMicros, CurrencyCode all populatedLog all fields
2.6Check subscription fieldsSubscriptionPeriod populated for subs (e.g. "P1M"), empty for one-time purchases-
2.7Call GetCachedOfferings() after fetchReturns the same data as the delegate-
2.8Call GetCurrentOffering()Returns true and matches the one with bIsCurrent-
If it goes wrong:
  • 0 offerings - products not linked to an offering in RevenueCat dashboard, or store products not approved yet
  • Product fields empty - store listing incomplete (title/description not set in the store console)
  • OnFailure fires - network error, invalid API key, or products misconfigured

Phase 3 - Purchase (Subscription)

Pick the monthly subscription package from the current offering.
#TestExpectedLog to check
3.1Call PurchasePackage() with monthly packageStore purchase dialog appears-
3.2Complete the purchase (use test card)OnSuccess fires with FRevenueCatCustomerInfo[RevenueCat] Purchase completed with result: 0 (Success)
3.3Inspect CustomerInfo.ActiveEntitlementsContains your entitlement (e.g. "pro") with bIsActive = trueLog entitlement ID and active status
3.4Check IsEntitlementActive("pro")Returns true-
3.5Inspect entitlement detailsProductID matches, ExpirationDate in the future, correct Store, bIsSandbox = trueLog all fields
3.6Check ActiveSubscriptionsContains the subscription product ID-

Phase 4 - Purchase (One-Time / Non-Consumable)

If you have a lifetime unlock or non-consumable product.
#TestExpected
4.1Call PurchasePackage() with the lifetime packageStore dialog appears
4.2Complete the purchaseOnSuccess fires
4.3Inspect entitlementbIsActive = true, ExpirationDate empty (lifetime), bWillRenew = false
4.4Try purchasing the same product againAlreadyOwned result, or store shows "already owned"

Phase 5 - Purchase Cancellation

#TestExpectedLog to check
5.1Call PurchasePackage()Store dialog appears-
5.2Press Back / CancelOnCancelled fires (async node), or OnPurchaseCompleted with UserCancelled result[RevenueCat] Purchase completed with result: 1
5.3Verify entitlements unchangedSame as before the purchase attempt-

Phase 6 - Restore Purchases

Simulates the "I reinstalled the app" scenario.
#TestExpectedLog to check
6.1Make a purchase, then clear app data / reinstallApp starts fresh, IsEntitlementActive() returns false-
6.2Call RestorePurchases()OnSuccess fires[RevenueCat] Purchases restored.
6.3Check IsEntitlementActive()Returns true - entitlement recovered-

Phase 7 - Sync Purchases

#TestExpected
7.1Call SyncPurchases()OnSuccess fires with updated customer info
7.2Compare with FetchCustomerInfo() resultShould match

Phase 8 - Fetch Customer Info

#TestExpected
8.1Call FetchCustomerInfo()OnSuccess fires
8.2Compare with GetCachedCustomerInfo()Data matches
8.3Check OriginalAppUserIDNon-empty, matches GetAppUserID() for anonymous users
8.4Check FirstSeenNon-empty ISO 8601 date
8.5Check ManagementURLNon-empty if the user has active subscriptions

Phase 9 - User Identity (Log In / Log Out)

#TestExpected
9.1Call LogIn("test_user_123")OnSuccess fires
9.2Check GetAppUserID()Returns "test_user_123"
9.3Check IsAnonymous()Returns false
9.4Check entitlementsPrevious purchases are associated with this user ID
9.5Call LogOut()OnSuccess fires with new anonymous customer info
9.6Check GetAppUserID()Returns a new $RCAnonymousID:...
9.7Check IsAnonymous()Returns true

Phase 10 - Subscriber Attributes

These don't have callbacks. Verify the values in the RevenueCat dashboard under the customer's attributes.
#TestWhere to verify
10.1SetEmail("test@example.com")Dashboard > Customer > Attributes
10.2SetDisplayName("TestPlayer")Dashboard
10.3SetPhoneNumber("+1234567890")Dashboard
10.4SetAttributes({"level": "42", "region": "EU"})Dashboard > Custom attributes
10.5SetFirebaseAppInstanceId("...")Dashboard
10.6CollectDeviceIdentifiers()Dashboard > GAID/IDFA populated

Phase 11 - Attribution

Same as subscriber attributes - fire-and-forget, verify in the dashboard.
#TestWhere to verify
11.1SetMediaSource("facebook_ads")Dashboard
11.2SetCampaign("summer_sale")Dashboard
11.3SetAdGroup("group_a")Dashboard
11.4SetCreative("banner_1")Dashboard
11.5SetKeyword("rpg_game")Dashboard

Phase 12 - Subscription Management

#TestExpected
12.1Call ShowManageSubscriptions() after a subscription purchaseOpens Play Store or App Store subscription management page
12.2Call ShowManageSubscriptions() with no active subscriptionNo crash, may be a no-op

Phase 13 - Startup Entitlement Check (Cold Launch)

Tests the real-world flow: player opens your game, and you need to know immediately if they're a subscriber.
#TestExpected
13.1Have an active subscription, force-close the app-
13.2Launch the appSDK configures, customer info updates on startup
13.3Check IsEntitlementActive() after OnCustomerInfoUpdated firesReturns true without manual fetch
13.4Your UI reflects the subscription statePro features unlocked, no paywall shown

Phase 14 - Error Handling

#TestExpected
14.1Turn off WiFi + mobile data, call FetchOfferings()OnFailure fires with a network error
14.2No network, call PurchasePackage()OnFailure fires
14.3No network, call FetchCustomerInfo()OnFailure fires or returns cached data
14.4No network, call RestorePurchases()OnFailure fires
14.5Restore network, retryOnSuccess fires normally

Edge Cases

Run these after the happy path is done.

Purchase Edge Cases

#TestExpected
E1Purchase while already subscribed (same product)AlreadyOwned result or store handles it
E2Purchase, then kill the app mid-transactionOn next launch, SyncPurchases() or startup listener recovers it
E3Start a purchase, switch apps, come backDialog should still be there or result fires
E4Rapidly tap the purchase buttonOnly one flow starts, no duplicates
E5Purchase on a slow/unstable connectionShould succeed eventually or produce a clear error
E6Google Play pending purchase (slow payment method)PendingPurchase result, entitlement NOT active yet

Subscription Lifecycle

#TestExpected
E7Subscribe, cancel via store, wait for expirybIsActive becomes false, UnsubscribeDetectedAt populated
E8Subscribe with intro priceIntroPriceString and IntroPricePeriod populated
E9Subscribe with free trialFreeTrialPeriod on product, PeriodType = Trial on entitlement
E10Upgrade monthly to annual (Google Play proration)Entitlement stays active, ProductID changes
E11Downgrade annual to monthlyEntitlement stays active, change at renewal

Identity

#TestExpected
E12LogIn("") (empty string)Warning log, no crash
E13Log in as User A, purchase, log out, log in as User BUser B should NOT have User A's entitlements
E14Log in with same ID on two devicesBoth see the same entitlements
E15Log in, log out, log in again (same user)Entitlements restored

Network / Timing

#TestExpected
E16Call FetchOfferings() before SDK is configuredWarning log, no crash, no-op
E17Call PurchasePackage() with an empty packageStore error or no-op, no crash
E18Call two async actions of same type at onceBoth complete without crash (may share results - known limitation)
E19App goes to background during async operationCompletes normally when resumed

Platform-Specific

#TestPlatformExpected
E20Google Play "acknowledge" flowAndroidAuto-acknowledged by RevenueCat SDK, no 3-day refund
E21Sandbox tester purchaseiOSQuick completion, bIsSandbox = true
E22StoreKit 2 dialogiOS 15+Modern purchase sheet appears

Logging

Android (Logcat)

In Android Studio, filter Logcat:
tag~:Purchases|RevenueCat
This catches RevenueCat SDK logs (Purchases tag), the Java bridge (RevenueCatBridge tag), and C++ UE_LOG output (LogRevenueCat category).
Using adb from a terminal:
adb logcat | grep -iE "Purchases|RevenueCat"

iOS (Xcode)

In the Xcode Console, filter by RevenueCat.

In-Game Debug

Build a simple debug text overlay that calls each function and prints results to screen with timestamps. Useful for testing on device when you don't have a log viewer attached.

RevenueCat Dashboard

The Customer page in the dashboard shows real-time subscription status, events, and attribute updates. When in doubt, check the dashboard.

Common Issues

"SDK configured log never appears"

  • Wrong API key. Double-check Project Settings > Plugins > RevenueCat.
  • No internet on device.
  • If Android: check Logcat for Java exceptions during startup.

"FetchOfferings returns 0 offerings"

  • Products not linked to an Offering in the RevenueCat dashboard.
  • Store products not approved / not active yet.
  • Wrong API key for the platform (using iOS key on Android or vice versa).

"Purchase dialog never shows up"

  • Android: app not uploaded to at least the internal testing track in Play Console.
  • Android: your Google account is not a license tester.
  • iOS: sandbox account not set up in device Settings.
  • Building an unsigned or incorrectly signed build.

"IsEntitlementActive always returns false"

  • Entitlement not linked to the product in RevenueCat dashboard.
  • Typo in the entitlement ID string (it's case-sensitive).
  • Checking before OnPurchaseCompleted fires. The data is async - wait for the callback.

"OnError fires with a cryptic error code"

"Crashes on startup on Android/iOS"

  • Check the native logs (Logcat / Xcode Console), not just the UE log.
  • Make sure the RevenueCat SDK is being pulled in (Gradle dependency for Android, bundled xcframework for iOS). Check build logs for download/linking errors.
  • If it's a JNI error, the Java bridge class might not be getting packaged. Check that the UPL file is being picked up by the build system.

"Works on one platform but not the other"

  • Each platform is a completely separate code path (JNI vs Obj-C++). A bug on one doesn't necessarily affect the other.
  • Make sure both API keys are set. If only one is set, the other platform will fail to configure.