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.
