Skip to main content
Question

Flutter - Restore Purchases Not Working on iOS

  • September 21, 2026
  • 1 reply
  • 19 views

Forum|alt.badge.img

Hi,

I added a one-time lifetime purchase to my app using RevenueCat. Technically, this is a non-consumable in-app purchase. The app is not currently available on the app stores; I am testing it through internal testing (Play Store) and TestFlight (App Store), and I plan to release it once everything is complete.

Users who purchase lifetime access will receive certain benefits, such as an ad-free experience.

However, I am experiencing the following issue on iOS:

  • The user installs the app.
  • The user purchases lifetime access.
  • The user deletes the app.
  • The user reinstalls it through TestFlight.
  • The user taps the Restore Purchases button on the purchase screen.

After tapping the button, it enters a loading state, but nothing happens afterward. There is no response or confirmation.

However, if the user taps the purchase button again, iOS displays the following message:

“You’ve already purchased this. It has been restored for free.”

I am not sure whether the same issue occurs on Android because the purchase is restored automatically after reinstalling the app. This does not happen on iOS, and the Restore Purchases button does not appear to work.

What could be causing this issue, and how can I fix it?

Note: I manage the paywall UI through the RevenueCat dashboard. My Flutter app uses purchases_flutter 9.16.1 and purchases_ui_flutter 9.16.1.

My code:

class RevenueCatConfig {
RevenueCatConfig._();

static const String _androidApiKey = 'goog_XXXXXXXXXXXX';
static const String _iosApiKey = 'appl_XXXXXXXXXXXX';

static const String entitlementId = 'xxxxxxx_pro';

static String get apiKey => Platform.isIOS ? _iosApiKey : _androidApiKey;
}

Provider, configured once during app startup before runApp:

class SubscriptionProvider extends ChangeNotifier {
bool _isPremium = false;
bool get isPremium => _isPremium;

Future<void> init() async {
await Purchases.configure(
PurchasesConfiguration(RevenueCatConfig.apiKey),
);

Purchases.addCustomerInfoUpdateListener(_onCustomerInfoUpdate);

try {
final customerInfo = await Purchases.getCustomerInfo();
_applyCustomerInfo(customerInfo);
} catch (e, stack) {
debugPrint('Failed to load customer info: $e\n$stack');
}
}

void _onCustomerInfoUpdate(CustomerInfo customerInfo) {
_applyCustomerInfo(customerInfo);
}

void _applyCustomerInfo(CustomerInfo customerInfo) {
final isPremium = customerInfo.entitlements.active.containsKey(
RevenueCatConfig.entitlementId,
);

if (isPremium == _isPremium) return;
_isPremium = isPremium;
notifyListeners();
}
}

Paywall screen:

class _SubscriptionScreenState extends State<SubscriptionScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _presentPaywall());
}

Future<void> _presentPaywall() async {
try {
await RevenueCatUI.presentPaywallIfNeeded(
RevenueCatConfig.entitlementId,
);
} catch (e, stack) {
// Reported to Crashlytics
}

if (mounted) Navigator.of(context).pop();
}

@override
Widget build(BuildContext context) => const Scaffold(
body: SizedBox.shrink(),
);
}

Thank you in advance for your help.

1 reply

Forum|alt.badge.img+8
  • RevenueCat Staff
  • September 21, 2026

Hi, iOS keeps this lifetime purchase on the App Store receipt, but it does not send that receipt until Restore or a purchase attempt finishes.

Your paywall Restore button is set up correctly, and com.speedev.holnivo.premium.lifetime is a non-consumable, so Restore can attach it to the new anonymous user created after reinstall. RevenueCatUI.presentPaywallIfNeeded stays open until the entitlement you pass in is active. If Restore never finishes, the paywall stays on the spinner and your code never gets a result.

Please try Restore again and look for an App Store sign-in sheet behind the paywall. Then tap the purchase button. The message “You’ve already purchased this. It has been restored for free.” comes from StoreKit, and that is the path that should unlock access.

If the paywall is still open after that message, the entitlement identifier in presentPaywallIfNeeded might not match the entitlement attached to this product. If it does and it still does not work, please provide me with RevenueCat debug logs here with log level verbose when you reproduce this, the entitlement identifier and an App User ID from a restore that did not unlock, and you can send this either via a Community post DM to me or a Zendesk ticket and then reply here to let me know about it.