diff --git a/lib/screens/lightmeter_pro/components/offering/widget_offering_lightmeter_pro.dart b/lib/screens/lightmeter_pro/components/offering/widget_offering_lightmeter_pro.dart index 988dbdc..6511a3f 100644 --- a/lib/screens/lightmeter_pro/components/offering/widget_offering_lightmeter_pro.dart +++ b/lib/screens/lightmeter_pro/components/offering/widget_offering_lightmeter_pro.dart @@ -1,7 +1,4 @@ -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/theme.dart'; @@ -9,7 +6,12 @@ import 'package:lightmeter/screens/shared/button/widget_button_filled_large.dart import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; class LightmeterProOffering extends StatefulWidget { - const LightmeterProOffering({super.key}); + const LightmeterProOffering({ + super.key, + required this.onBuyProduct, + }); + + final ValueChanged onBuyProduct; @override State createState() => _LightmeterProOfferingState(); @@ -17,29 +19,12 @@ class LightmeterProOffering extends StatefulWidget { class _LightmeterProOfferingState extends State { late final Future> productsFuture; - bool _isLoading = true; - IAPProduct? monthly; - IAPProduct? yearly; - IAPProduct? lifetime; IAPProduct? selected; @override - void initState() { - super.initState(); - productsFuture = IAPProductsProvider.of(context).fetchProducts(); - productsFuture.then((products) async { - monthly = products.firstWhereOrNull((p) => p.type == PurchaseType.monthly); - yearly = products.firstWhereOrNull((p) => p.type == PurchaseType.yearly); - lifetime = products.firstWhereOrNull((p) => p.type == PurchaseType.lifetime); - selected = monthly ?? lifetime; - }).onError((e, __) { - SchedulerBinding.instance.addPostFrameCallback((_) { - _showSnackbar(e.toString()); - }); - }).whenComplete(() { - _isLoading = false; - if (mounted) setState(() {}); - }); + void didChangeDependencies() { + super.didChangeDependencies(); + selected = IAPProducts.of(context).monthly ?? IAPProducts.of(context).lifetime; } @override @@ -66,78 +51,29 @@ class _LightmeterProOfferingState extends State { if (!_isLifetimeOnly) Padding( padding: const EdgeInsets.only(bottom: Dimens.paddingS), - child: Stack( - alignment: Alignment.center, - children: [ - AnimatedOpacity( - duration: Dimens.durationM, - opacity: _isLoading ? Dimens.disabledOpacity : Dimens.enabledOpacity, - child: _Products( - monthly: monthly, - yearly: yearly, - lifetime: lifetime, - selected: selected, - onProductSelected: _selectProduct, - ), - ), - if (_isLoading) - const SizedBox( - height: 120, - child: Center(child: CircularProgressIndicator()), - ), - ], + child: _Products( + monthly: IAPProducts.of(context).monthly, + yearly: IAPProducts.of(context).yearly, + lifetime: IAPProducts.of(context).lifetime, + selected: selected, + onProductSelected: _selectProduct, ), ), FilledButtonLarge( title: S.of(context).continuePurchase, - onPressed: _isLoading || selected != null ? _buyPro : null, + onPressed: selected != null ? () => widget.onBuyProduct(selected!) : null, ), ], ), ); } - bool get _isLifetimeOnly => lifetime != null && yearly == null && monthly == null; + bool get _isLifetimeOnly => !IAPProducts.of(context).hasSubscriptions; void _selectProduct(IAPProduct product) { - if (!_isLoading) { - setState(() { - selected = product; - }); - } - } - - Future _buyPro() async { setState(() { - _isLoading = true; + selected = product; }); - try { - final isPro = await IAPProductsProvider.of(context).buyPro(selected!); - if (mounted && isPro) { - Navigator.of(context).pop(); - } - } on PlatformException catch (e) { - _showSnackbar(e.message ?? ''); - } catch (e) { - _showSnackbar(e.toString()); - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } - - void _showSnackbar(String message) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - behavior: SnackBarBehavior.floating, - ), - ); - } } } diff --git a/lib/screens/lightmeter_pro/screen_lightmeter_pro.dart b/lib/screens/lightmeter_pro/screen_lightmeter_pro.dart index a69b25f..0edc800 100644 --- a/lib/screens/lightmeter_pro/screen_lightmeter_pro.dart +++ b/lib/screens/lightmeter_pro/screen_lightmeter_pro.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:lightmeter/data/models/app_feature.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; @@ -8,21 +9,30 @@ import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; import 'package:lightmeter/utils/text_height.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; -class LightmeterProScreen extends StatelessWidget { +class LightmeterProScreen extends StatefulWidget { + const LightmeterProScreen({super.key}); + + @override + State createState() => _LightmeterProScreenState(); +} + +class _LightmeterProScreenState extends State { final features = defaultTargetPlatform == TargetPlatform.android ? AppFeature.androidFeatures : AppFeature.iosFeatures; - LightmeterProScreen({super.key}); + final _isRestoringPurchases = ValueNotifier(false); + final _isPurchasingProduct = ValueNotifier(false); @override Widget build(BuildContext context) { return SliverScreen( title: Text(S.of(context).proFeaturesTitle), appBarActions: [ - IconButton( - onPressed: IAPProductsProvider.of(context).restorePurchases, - icon: const Icon(Icons.restore), - tooltip: S.of(context).restorePurchases, + /// Restoration is working, but it does not trigger pop + /// add await of didChangeDependencies + _RestorePurchasesButton( + valueListenable: _isRestoringPurchases, + onPressed: _restorePurchases, ), ], slivers: [ @@ -65,7 +75,81 @@ class LightmeterProScreen extends StatelessWidget { ), ), ], - bottomNavigationBar: const LightmeterProOffering(), + bottomNavigationBar: LightmeterProOffering(onBuyProduct: _buyPro), + ); + } + + Future _restorePurchases() async { + _isRestoringPurchases.value = true; + try { + final isPro = await IAPProductsProvider.of(context).restorePurchases(); + if (mounted && isPro) { + Navigator.of(context).pop(); + } + } on PlatformException catch (e) { + _showSnackbar(e.message ?? ''); + } catch (e) { + _showSnackbar(e.toString()); + } finally { + _isRestoringPurchases.value = true; + } + } + + Future _buyPro(IAPProduct product) async { + _isPurchasingProduct.value = true; + try { + final isPro = await IAPProductsProvider.of(context).buyPro(product); + if (mounted && isPro) { + Navigator.of(context).pop(); + } + } on PlatformException catch (e) { + _showSnackbar(e.message ?? ''); + } catch (e) { + _showSnackbar(e.toString()); + } finally { + _isPurchasingProduct.value = false; + } + } + + void _showSnackbar(String message) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + behavior: SnackBarBehavior.floating, + ), + ); + } + } +} + +class _RestorePurchasesButton extends StatelessWidget { + const _RestorePurchasesButton({ + required this.valueListenable, + required this.onPressed, + }); + + final ValueNotifier valueListenable; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: valueListenable, + builder: (context, value, _) { + if (value) { + return const SizedBox.square( + dimension: Dimens.grid24 - Dimens.grid4, + child: CircularProgressIndicator(), + ); + } else { + return IconButton( + onPressed: onPressed, + icon: const Icon(Icons.restore), + tooltip: S.of(context).restorePurchases, + ); + } + }, ); } }