diff --git a/iap/lib/src/data/models/iap_product.dart b/iap/lib/src/data/models/iap_product.dart index 4ee2b24..e3bf360 100644 --- a/iap/lib/src/data/models/iap_product.dart +++ b/iap/lib/src/data/models/iap_product.dart @@ -1,34 +1,13 @@ -enum IAPProductStatus { - purchasable, - pending, - purchased, -} - -enum IAPProductType { paidFeatures } - class IAPProduct { final String storeId; - final IAPProductStatus status; + final PurchaseType type; final String price; const IAPProduct({ required this.storeId, - this.status = IAPProductStatus.purchasable, + required this.type, required this.price, }); - - IAPProduct copyWith({IAPProductStatus? status}) => IAPProduct( - storeId: storeId, - status: status ?? this.status, - price: price, - ); } -extension IAPProductTypeExtension on IAPProductType { - String get storeId { - switch (this) { - case IAPProductType.paidFeatures: - return ""; - } - } -} +enum PurchaseType { monthly, yearly, lifetime } diff --git a/iap/lib/src/providers/iap_products_provider.dart b/iap/lib/src/providers/iap_products_provider.dart index 936f296..494fd3b 100644 --- a/iap/lib/src/providers/iap_products_provider.dart +++ b/iap/lib/src/providers/iap_products_provider.dart @@ -2,10 +2,9 @@ import 'package:flutter/material.dart'; import 'package:m3_lightmeter_iap/src/data/models/iap_product.dart'; class IAPProductsProvider extends StatefulWidget { - final String apiUrl; final Widget child; - const IAPProductsProvider({required this.apiUrl, required this.child, super.key}); + const IAPProductsProvider({required this.child, super.key}); static IAPProductsProviderState of(BuildContext context) => IAPProductsProvider.maybeOf(context)!; @@ -21,52 +20,72 @@ class IAPProductsProviderState extends State { @override Widget build(BuildContext context) { return IAPProducts( - products: [ - IAPProduct( - storeId: IAPProductType.paidFeatures.storeId, - status: IAPProductStatus.purchased, - price: '0.0\$', - ) - ], + isPro: true, + lifetime: const IAPProduct( + storeId: '', + type: PurchaseType.lifetime, + price: '0.0\$', + ), + yearly: const IAPProduct( + storeId: '', + type: PurchaseType.yearly, + price: '0.0\$', + ), + monthly: const IAPProduct( + storeId: '', + type: PurchaseType.monthly, + price: '0.0\$', + ), child: widget.child, ); } - Future buy(IAPProductType type) async {} + Future> fetchProducts() async { + return []; + } - Future restorePurchases() async {} + Future buyPro(IAPProduct product) async { + return false; + } + + Future restorePurchases() async { + return false; + } + + Future checkIsPro() async { + return false; + } } -class IAPProducts extends InheritedModel { - final List products; +class IAPProducts extends InheritedWidget { + final IAPProduct? lifetime; + final IAPProduct? yearly; + final IAPProduct? monthly; + final bool _isPro; const IAPProducts({ - required this.products, + this.lifetime, + this.yearly, + this.monthly, + required bool isPro, required super.child, super.key, - }); + }) : _isPro = isPro; - static IAPProduct? productOf(BuildContext context, IAPProductType type) { - final IAPProducts? result = InheritedModel.inheritFrom(context, aspect: type); - return result!._findProduct(type); + static IAPProducts of(BuildContext context) { + return context.getInheritedWidgetOfExactType()!; } - static bool isPurchased(BuildContext context, IAPProductType type) { - final IAPProducts? result = InheritedModel.inheritFrom(context, aspect: type); - return result!._findProduct(type)?.status == IAPProductStatus.purchased; + static bool isPro(BuildContext context, {bool listen = true}) { + return (listen + ? context.dependOnInheritedWidgetOfExactType() + : context.getInheritedWidgetOfExactType()) + ?._isPro == + true; } + bool get hasSubscriptions => yearly != null || monthly != null; + @override - bool updateShouldNotify(IAPProducts oldWidget) => true; - - @override - bool updateShouldNotifyDependent(IAPProducts oldWidget, Set dependencies) => true; - - IAPProduct? _findProduct(IAPProductType type) { - try { - return products.firstWhere((element) => element.storeId == type.storeId); - } catch (_) { - return null; - } - } + bool updateShouldNotify(IAPProducts oldWidget) => oldWidget._isPro != _isPro; } diff --git a/integration_test/logbook_test.dart b/integration_test/logbook_test.dart index 373242b..332431f 100644 --- a/integration_test/logbook_test.dart +++ b/integration_test/logbook_test.dart @@ -100,7 +100,7 @@ extension on WidgetTester { Future openPickerAndSelect(String title, String valueToSelect) async { await tap(find.text(title)); await pumpAndSettle(); - final dialogFinder = find.byType(DialogPicker); + final dialogFinder = find.byType(DialogPicker>); final listTileFinder = find.text(valueToSelect); await scrollUntilVisible( listTileFinder, diff --git a/integration_test/mocks/iap_products_mock.dart b/integration_test/mocks/iap_products_mock.dart index 9742c8a..b5d9aaa 100644 --- a/integration_test/mocks/iap_products_mock.dart +++ b/integration_test/mocks/iap_products_mock.dart @@ -20,16 +20,11 @@ class MockIAPProductsProvider extends StatefulWidget { class MockIAPProductsProviderState extends State { late bool _purchased = widget.initialyPurchased; + @override Widget build(BuildContext context) { return IAPProducts( - products: List.from([ - IAPProduct( - storeId: IAPProductType.paidFeatures.storeId, - status: _purchased ? IAPProductStatus.purchased : IAPProductStatus.purchasable, - price: '0.0\$', - ), - ]), + isPro: _purchased, child: widget.child, ); } diff --git a/integration_test/purchases_test.dart b/integration_test/purchases_test.dart index a0862fb..e54c0d5 100644 --- a/integration_test/purchases_test.dart +++ b/integration_test/purchases_test.dart @@ -15,7 +15,6 @@ import 'package:lightmeter/screens/metering/components/shared/readings_container import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart'; import 'package:lightmeter/screens/settings/screen_settings.dart'; import 'package:lightmeter/utils/platform_utils.dart'; -import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:meta/meta.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -42,7 +41,7 @@ void testPurchases(String description) { UserPreferencesService.seenChangelogVersionKey: await const PlatformUtils().version, }); - await tester.pumpApplication(productStatus: IAPProductStatus.purchasable); + await tester.pumpApplication(isPro: false); await tester.takePhoto(); /// Expect the bare minimum free functionallity diff --git a/integration_test/utils/widget_tester_actions.dart b/integration_test/utils/widget_tester_actions.dart index 72c0e55..7754bb4 100644 --- a/integration_test/utils/widget_tester_actions.dart +++ b/integration_test/utils/widget_tester_actions.dart @@ -7,7 +7,6 @@ import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart'; import 'package:lightmeter/screens/metering/screen_metering.dart'; -import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import '../mocks/iap_products_mock.dart'; @@ -19,7 +18,7 @@ const mockPhotoEv100 = 8.3; extension WidgetTesterCommonActions on WidgetTester { Future pumpApplication({ - IAPProductStatus productStatus = IAPProductStatus.purchased, + bool isPro = true, TogglableMap? equipmentProfiles, String selectedEquipmentProfileId = '', TogglableMap? predefinedFilms, @@ -28,7 +27,7 @@ extension WidgetTesterCommonActions on WidgetTester { }) async { await pumpWidget( MockIAPProductsProvider( - initialyPurchased: productStatus == IAPProductStatus.purchased, + initialyPurchased: isPro, child: ApplicationWrapper( const Environment.dev(), child: MockIAPProviders( diff --git a/lib/application.dart b/lib/application.dart index abc3a1f..a22f6f2 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -59,7 +59,7 @@ class Application extends StatelessWidget { EquipmentProfileEditFlow(args: context.routeArgs()), NavigationRoutes.filmsListScreen.name: (_) => const FilmsScreen(), NavigationRoutes.filmEditScreen.name: (context) => FilmEditFlow(args: context.routeArgs()), - NavigationRoutes.proFeaturesScreen.name: (_) => LightmeterProScreen(), + NavigationRoutes.proFeaturesScreen.name: (_) => const LightmeterProScreen(), NavigationRoutes.timerScreen.name: (context) => TimerFlow(args: context.routeArgs()), NavigationRoutes.logbookPhotosListScreen.name: (_) => const LogbookPhotosScreen(), NavigationRoutes.logbookPhotoEditScreen.name: (context) => diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index c397307..49e4a59 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -176,5 +176,18 @@ "location": "Standort", "noMapsAppFound": "Keine Kartenanwendung gefunden.", "logbook": "Fototagebuch", - "noPhotos": "Keine Fotos" + "noPhotos": "Keine Fotos", + "continuePurchase": "Weiter", + "monthly": "Monatlich", + "yearly": "Jährlich", + "lifetime": "Für immer", + "pricePerMonth": "{price}/Monat", + "pricePerYear": "{price}/Jahr", + "@pricePerMonth": { + "placeholders": { + "price": { + "type": "String" + } + } + } } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index ce1a9e1..3c6542e 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -176,5 +176,18 @@ "location": "Location", "noMapsAppFound": "No maps application found.", "logbook": "Logbook", - "noPhotos": "No photos" + "noPhotos": "No photos", + "continuePurchase": "Continue", + "monthly": "Monthly", + "yearly": "Yearly", + "lifetime": "Lifetime", + "pricePerMonth": "{price}/month", + "pricePerYear": "{price}/year", + "@pricePerMonth": { + "placeholders": { + "price": { + "type": "String" + } + } + } } diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 901d38a..a64a52b 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -167,5 +167,18 @@ "location": "Emplacement", "noMapsAppFound": "Aucune application de cartes trouvée.", "logbook": "Carnet photo", - "noPhotos": "Aucune photo" + "noPhotos": "Aucune photo", + "continuePurchase": "Continuer", + "monthly": "Mensuel", + "yearly": "Annuel", + "lifetime": "Pour toujours", + "pricePerMonth": "{price}/mois", + "pricePerYear": "{price}/an", + "@pricePerMonth": { + "placeholders": { + "price": { + "type": "String" + } + } + } } diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index e276ccc..5d12fda 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -166,5 +166,18 @@ "location": "Местоположение", "noMapsAppFound": "Приложение карт не найдено.", "logbook": "Фотожурнал", - "noPhotos": "Нет фотографий" + "noPhotos": "Нет фотографий", + "continuePurchase": "Продолжить", + "monthly": "Ежемесячно", + "yearly": "Ежегодно", + "lifetime": "Навсегда", + "pricePerMonth": "{price}/месяц", + "pricePerYear": "{price}/год", + "@pricePerMonth": { + "placeholders": { + "price": { + "type": "String" + } + } + } } diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 7ecbe52..5484593 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -164,5 +164,18 @@ "location": "位置", "noMapsAppFound": "未找到地图应用程序。", "logbook": "拍照日志", - "noPhotos": "没有照片" + "noPhotos": "没有照片", + "continuePurchase": "继续", + "monthly": "月付", + "yearly": "年付", + "lifetime": "永久", + "pricePerMonth": "{price}/月", + "pricePerYear": "{price}/年", + "@pricePerMonth": { + "placeholders": { + "price": { + "type": "String" + } + } + } } diff --git a/lib/res/theme.dart b/lib/res/theme.dart index 0362dac..7c8454a 100644 --- a/lib/res/theme.dart +++ b/lib/res/theme.dart @@ -64,6 +64,16 @@ ThemeData themeFrom(Color primaryColor, Brightness brightness) { scaffoldBackgroundColor: scheme.surface, ); return theme.copyWith( + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + minimumSize: const Size.square(Dimens.grid56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(Dimens.borderRadiusM), + ), + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + ), + ), listTileTheme: ListTileThemeData( style: ListTileStyle.list, iconColor: scheme.onSurface, diff --git a/lib/runner.dart b/lib/runner.dart index 765f300..16f7255 100644 --- a/lib/runner.dart +++ b/lib/runner.dart @@ -5,7 +5,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:lightmeter/application.dart'; import 'package:lightmeter/application_wrapper.dart'; -import 'package:lightmeter/constants.dart'; import 'package:lightmeter/data/analytics/analytics.dart'; import 'package:lightmeter/data/analytics/api/analytics_firebase.dart'; import 'package:lightmeter/environment.dart'; @@ -27,18 +26,10 @@ Future runLightmeterApp(Environment env) async { runApp( env.buildType == BuildType.dev ? IAPProducts( - products: [ - IAPProduct( - storeId: IAPProductType.paidFeatures.storeId, - price: '0.0\$', - ), - ], + isPro: true, child: application, ) - : IAPProductsProvider( - apiUrl: iapServerUrl, - child: application, - ), + : IAPProductsProvider(child: application), ); }, _errorsLogger.logCrash, 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 new file mode 100644 index 0000000..b3a8f6d --- /dev/null +++ b/lib/screens/lightmeter_pro/components/offering/widget_offering_lightmeter_pro.dart @@ -0,0 +1,222 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/res/theme.dart'; +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, + required this.isEnabled, + required this.onBuyProduct, + }); + + final bool isEnabled; + final ValueChanged onBuyProduct; + + @override + State createState() => _LightmeterProOfferingState(); +} + +class _LightmeterProOfferingState extends State { + late final Future> productsFuture; + IAPProduct? selected; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + selected = IAPProducts.of(context).monthly ?? IAPProducts.of(context).lifetime; + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(Dimens.borderRadiusL), + topRight: Radius.circular(Dimens.borderRadiusL), + ), + color: Theme.of(context).colorScheme.surfaceElevated1, + ), + width: MediaQuery.sizeOf(context).width, + padding: EdgeInsets.fromLTRB( + Dimens.paddingM, + Dimens.paddingM, + Dimens.paddingM, + Dimens.paddingM + MediaQuery.paddingOf(context).bottom, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + if (!_isLifetimeOnly) + AnimatedOpacity( + duration: Dimens.durationM, + opacity: widget.isEnabled ? Dimens.enabledOpacity : Dimens.disabledOpacity, + child: IgnorePointer( + ignoring: !widget.isEnabled, + child: Padding( + padding: const EdgeInsets.only(bottom: Dimens.paddingS), + 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: widget.isEnabled && selected != null ? () => widget.onBuyProduct(selected!) : null, + ), + ], + ), + ); + } + + bool get _isLifetimeOnly => !IAPProducts.of(context).hasSubscriptions; + + void _selectProduct(IAPProduct product) { + setState(() { + selected = product; + }); + } +} + +class _Products extends StatelessWidget { + const _Products({ + this.monthly, + this.yearly, + this.lifetime, + required this.selected, + required this.onProductSelected, + }); + + final IAPProduct? monthly; + final IAPProduct? yearly; + final IAPProduct? lifetime; + final IAPProduct? selected; + final ValueChanged onProductSelected; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (monthly case final monthly?) + Padding( + padding: const EdgeInsets.only(bottom: Dimens.paddingS), + child: _ProductItem( + title: S.of(context).monthly, + price: S.of(context).pricePerMonth(monthly.price), + isSelected: selected == monthly, + onPressed: () => onProductSelected(monthly), + ), + ), + if (yearly case final yearly?) + Padding( + padding: const EdgeInsets.only(bottom: Dimens.paddingS), + child: _ProductItem( + title: S.of(context).yearly, + price: S.of(context).pricePerYear(yearly.price), + isSelected: selected == yearly, + onPressed: () => onProductSelected(yearly), + ), + ), + if (lifetime case final lifetime?) + _ProductItem( + title: S.of(context).lifetime, + price: lifetime.price, + isSelected: selected == lifetime, + onPressed: () => onProductSelected(lifetime), + ), + ], + ); + } +} + +class _ProductItem extends StatelessWidget { + const _ProductItem({ + required this.title, + required this.price, + required this.isSelected, + required this.onPressed, + }); + + final String title; + final String price; + final bool isSelected; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Material( + color: + isSelected ? Theme.of(context).colorScheme.primaryContainer : Theme.of(context).colorScheme.surfaceElevated2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(Dimens.borderRadiusM), + side: isSelected + ? BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 2, + ) + : BorderSide.none, + ), + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: onPressed, + child: Padding( + /// [Radio] has 12pt paddings around the button. + /// [Dimens.paddingM] - 12pt = 4pt + padding: const EdgeInsets.fromLTRB( + Dimens.grid4, + Dimens.grid4, + Dimens.paddingM, + Dimens.grid4, + ), + child: Row( + children: [ + Radio( + value: isSelected, + groupValue: true, + onChanged: (_) => onPressed(), + ), + _ProductAnimatedText( + title, + isSelected: isSelected, + ), + const Spacer(), + _ProductAnimatedText( + price, + isSelected: isSelected, + ), + ], + ), + ), + ), + ); + } +} + +class _ProductAnimatedText extends StatelessWidget { + const _ProductAnimatedText(this.text, {required this.isSelected}); + + final String text; + final bool isSelected; + + @override + Widget build(BuildContext context) { + return AnimatedDefaultTextStyle( + duration: Dimens.durationM, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w500, + color: + isSelected ? Theme.of(context).colorScheme.onPrimaryContainer : Theme.of(context).colorScheme.onSurface, + ), + child: Text(text), + ); + } +} diff --git a/lib/screens/lightmeter_pro/screen_lightmeter_pro.dart b/lib/screens/lightmeter_pro/screen_lightmeter_pro.dart index c527f76..00ba81d 100644 --- a/lib/screens/lightmeter_pro/screen_lightmeter_pro.dart +++ b/lib/screens/lightmeter_pro/screen_lightmeter_pro.dart @@ -1,92 +1,157 @@ 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/providers/services_provider.dart'; import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/res/theme.dart'; +import 'package:lightmeter/screens/lightmeter_pro/components/offering/widget_offering_lightmeter_pro.dart'; 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 { +typedef PurchasesState = ({bool isPurchasingProduct, bool isRestoringPurchases}); + +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 _purchasesNotifier = ValueNotifier( + ( + isPurchasingProduct: false, + isRestoringPurchases: false, + ), + ); @override Widget build(BuildContext context) { - return Column( - children: [ - Expanded( - child: SliverScreen( - title: Text(S.of(context).proFeaturesTitle), - slivers: [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(Dimens.paddingM), - child: Text( - S.of(context).proFeaturesPromoText, - style: Theme.of(context).textTheme.bodyLarge, - ), - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB( - Dimens.paddingM, - 0, - Dimens.paddingM, - Dimens.paddingS, - ), - child: Text( - S.of(context).proFeaturesWhatsIncluded, - style: Theme.of(context).textTheme.headlineSmall, - ), - ), - ), - const SliverToBoxAdapter(child: _FeaturesHeader()), - SliverList.separated( - itemCount: features.length, - itemBuilder: (_, index) => _FeatureItem(feature: features[index]), - separatorBuilder: (_, __) => const Padding( - padding: EdgeInsets.symmetric(horizontal: Dimens.paddingM), - child: Divider(), - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(Dimens.paddingM), - child: Text(S.of(context).proFeaturesSupportText), - ), - ), - ], + return SliverScreen( + title: Text(S.of(context).proFeaturesTitle), + appBarActions: [ + ValueListenableBuilder( + valueListenable: _purchasesNotifier, + builder: (context, value, _) { + if (value.isRestoringPurchases) { + return const SizedBox.square( + dimension: Dimens.grid24 - Dimens.grid4, + child: CircularProgressIndicator(), + ); + } else { + return IconButton( + onPressed: value.isPurchasingProduct ? null : _restorePurchases, + icon: const Icon(Icons.restore), + tooltip: S.of(context).restorePurchases, + ); + } + }, + ), + ], + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(Dimens.paddingM), + child: Text( + S.of(context).proFeaturesPromoText, + style: Theme.of(context).textTheme.bodyLarge, + ), ), ), - Container( - color: Theme.of(context).colorScheme.surfaceElevated1, - width: MediaQuery.sizeOf(context).width, - padding: EdgeInsets.fromLTRB( - Dimens.paddingM, - Dimens.paddingM, - Dimens.paddingM, - Dimens.paddingM + MediaQuery.paddingOf(context).bottom, + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB( + Dimens.paddingM, + 0, + Dimens.paddingM, + Dimens.paddingS, + ), + child: Text( + S.of(context).proFeaturesWhatsIncluded, + style: Theme.of(context).textTheme.headlineSmall, + ), ), - child: FilledButton( - onPressed: () { - ServicesProvider.maybeOf(context) - ?.analytics - .setCustomKey('iap_product_type', IAPProductType.paidFeatures.storeId); - IAPProductsProvider.maybeOf(context)?.buy(IAPProductType.paidFeatures); - Navigator.of(context).pop(); - }, - child: Text(S.of(context).getNowFor(IAPProducts.productOf(context, IAPProductType.paidFeatures)!.price)), + ), + const SliverToBoxAdapter(child: _FeaturesHeader()), + SliverList.separated( + itemCount: features.length, + itemBuilder: (_, index) => _FeatureItem(feature: features[index]), + separatorBuilder: (_, __) => const Padding( + padding: EdgeInsets.symmetric(horizontal: Dimens.paddingM), + child: Divider(), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(Dimens.paddingM), + child: Text(S.of(context).proFeaturesSupportText), ), ), ], + bottomNavigationBar: ValueListenableBuilder( + valueListenable: _purchasesNotifier, + builder: (context, value, _) { + return LightmeterProOffering( + isEnabled: !value.isRestoringPurchases && !value.isPurchasingProduct, + onBuyProduct: _buyPro, + ); + }, + ), ); } + + @override + void dispose() { + _purchasesNotifier.dispose(); + super.dispose(); + } + + Future _restorePurchases() async { + _purchasesNotifier.isRestoringPurchases = 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 { + _purchasesNotifier.isRestoringPurchases = false; + } + } + + Future _buyPro(IAPProduct product) async { + _purchasesNotifier.isPurchasingProduct = 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 { + _purchasesNotifier.isPurchasingProduct = false; + } + } + + void _showSnackbar(String message) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + behavior: SnackBarBehavior.floating, + ), + ); + } + } } class _FeaturesHeader extends StatelessWidget { @@ -227,3 +292,13 @@ class _CheckBox extends StatelessWidget { ); } } + +extension on ValueNotifier { + set isPurchasingProduct(bool isPurchasingProduct) { + value = (isPurchasingProduct: isPurchasingProduct, isRestoringPurchases: value.isRestoringPurchases); + } + + set isRestoringPurchases(bool isRestoringPurchases) { + value = (isPurchasingProduct: value.isPurchasingProduct, isRestoringPurchases: isRestoringPurchases); + } +} diff --git a/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart b/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart index 2a0f50d..2fb8e30 100644 --- a/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart +++ b/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart @@ -128,7 +128,7 @@ class AnimatedDialogState extends State with SingleTickerProvide @override Widget build(BuildContext context) { - return InkWell( + return GestureDetector( key: _key, onTap: _openDialog, child: Opacity( diff --git a/lib/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart b/lib/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart index 93c39e6..160dcc0 100644 --- a/lib/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart +++ b/lib/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart @@ -1,31 +1,19 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/navigation/routes.dart'; -import 'package:lightmeter/res/dimens.dart'; -import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; class BuyProListTile extends StatelessWidget { const BuyProListTile({super.key}); @override Widget build(BuildContext context) { - final status = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status; - final isPending = status == IAPProductStatus.purchased || status == null; + // TODO: implement pending handling via REvenueCat return ListTile( leading: const Icon(Icons.bolt), title: Text(S.of(context).getPro), - onTap: !isPending - ? () { - Navigator.of(context).pushNamed(NavigationRoutes.proFeaturesScreen.name); - } - : null, - trailing: isPending - ? const SizedBox( - height: Dimens.grid24, - width: Dimens.grid24, - child: CircularProgressIndicator(), - ) - : null, + onTap: () { + Navigator.of(context).pushNamed(NavigationRoutes.proFeaturesScreen.name); + }, ); } } diff --git a/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart b/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart index 8cc522a..d44c872 100644 --- a/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart +++ b/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart @@ -1,19 +1,16 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart'; import 'package:lightmeter/utils/context_utils.dart'; -import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; /// Depends on the product status and replaces [onTap] with purchase callback /// if the product is purchasable. class IAPListTile extends StatelessWidget { - final IAPProductType product; final Icon leading; final Text title; final VoidCallback onTap; final bool showPendingTrailing; const IAPListTile({ - this.product = IAPProductType.paidFeatures, required this.leading, required this.title, required this.onTap, diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart index ca3205c..c6c7788 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -9,6 +9,7 @@ import 'package:lightmeter/screens/settings/components/theme/widget_settings_sec import 'package:lightmeter/screens/settings/flow_settings.dart'; import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; import 'package:lightmeter/utils/context_utils.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -22,6 +23,7 @@ class _SettingsScreenState extends State { void didChangeDependencies() { super.didChangeDependencies(); SettingsInteractorProvider.of(context).disableVolumeHandling(); + IAPProductsProvider.maybeOf(context)?.checkIsPro(); } @override diff --git a/lib/screens/shared/button/widget_button_filled_large.dart b/lib/screens/shared/button/widget_button_filled_large.dart new file mode 100644 index 0000000..8f7af2b --- /dev/null +++ b/lib/screens/shared/button/widget_button_filled_large.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; + +class FilledButtonLarge extends StatelessWidget { + const FilledButtonLarge({ + required this.title, + required this.onPressed, + }); + + final String title; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return FilledButton( + style: Theme.of(context) + .filledButtonTheme + .style! + .copyWith(textStyle: WidgetStatePropertyAll(Theme.of(context).textTheme.titleMedium)), + onPressed: onPressed, + child: Text(S.of(context).continuePurchase), + ); + } +} diff --git a/lib/screens/shared/sliver_screen/screen_sliver.dart b/lib/screens/shared/sliver_screen/screen_sliver.dart index fd96546..22f3029 100644 --- a/lib/screens/shared/sliver_screen/screen_sliver.dart +++ b/lib/screens/shared/sliver_screen/screen_sliver.dart @@ -8,12 +8,14 @@ class SliverScreen extends StatelessWidget { final List appBarActions; final PreferredSizeWidget? bottom; final List slivers; + final Widget? bottomNavigationBar; const SliverScreen({ this.title, this.appBarActions = const [], this.bottom, required this.slivers, + this.bottomNavigationBar, super.key, }); @@ -23,14 +25,21 @@ class SliverScreen extends StatelessWidget { body: SafeArea( top: false, bottom: false, - child: CustomScrollView( - slivers: [ - _AppBar( - title: title, - appBarActions: appBarActions, - bottom: bottom, + child: Column( + children: [ + Expanded( + child: CustomScrollView( + slivers: [ + _AppBar( + title: title, + appBarActions: appBarActions, + bottom: bottom, + ), + ...slivers, + ], + ), ), - ...slivers, + if (bottomNavigationBar != null) bottomNavigationBar!, ], ), ), diff --git a/lib/utils/context_utils.dart b/lib/utils/context_utils.dart index f64ea76..0f25953 100644 --- a/lib/utils/context_utils.dart +++ b/lib/utils/context_utils.dart @@ -8,5 +8,5 @@ extension BuildContextUtils on BuildContext { return UserPreferencesProvider.meteringScreenFeatureOf(this, feature); } - bool get isPro => IAPProducts.isPurchased(this, IAPProductType.paidFeatures); + bool get isPro => IAPProducts.isPro(this); } diff --git a/pubspec.lock b/pubspec.lock index e95eb64..70eaa25 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -623,6 +623,14 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -756,38 +764,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.3.0" - in_app_purchase: - dependency: transitive - description: - name: in_app_purchase - sha256: "960f26a08d9351fb8f89f08901f8a829d41b04d45a694b8f776121d9e41dcad6" - url: "https://pub.dev" - source: hosted - version: "3.2.0" - in_app_purchase_android: - dependency: transitive - description: - name: in_app_purchase_android - sha256: b7cc194f183a97d71e6da1edb4a4095466ca0c22533615ecad021bdf95a5ecdc - url: "https://pub.dev" - source: hosted - version: "0.3.6+13" - in_app_purchase_platform_interface: - dependency: transitive - description: - name: in_app_purchase_platform_interface - sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c" - url: "https://pub.dev" - source: hosted - version: "1.4.0" - in_app_purchase_storekit: - dependency: transitive - description: - name: in_app_purchase_storekit - sha256: "6ce1361278cacc0481508989ba419b2c9f46a2b0dc54b3fe54f5ee63c2718fef" - url: "https://pub.dev" - source: hosted - version: "0.3.22+1" integration_test: dependency: "direct dev" description: flutter @@ -885,11 +861,11 @@ packages: dependency: "direct main" description: path: "." - ref: HEAD - resolved-ref: "32e053b7d14009c6d61daf56f1556de3365297cb" + ref: "v4.0.0" + resolved-ref: e951b5349fb8d1ff8fe476a9f6bd42f20e07f4e9 url: "https://github.com/vodemn/m3_lightmeter_iap" source: git - version: "2.2.0+31" + version: "4.0.0+33" m3_lightmeter_resources: dependency: "direct main" description: @@ -1163,6 +1139,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + purchases_flutter: + dependency: transitive + description: + name: purchases_flutter + sha256: "0d64050d8620740c67cbcd8697bd7447bfb39ec0b04c09dc12977b6ec4a2ad50" + url: "https://pub.dev" + source: hosted + version: "9.0.0-beta.2" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 114557f..0b6362d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: m3_lightmeter_iap: git: url: "https://github.com/vodemn/m3_lightmeter_iap" - branch: v3.0.0 + ref: v4.0.0 m3_lightmeter_resources: git: url: "https://github.com/vodemn/m3_lightmeter_resources" diff --git a/test/application_mock.dart b/test/application_mock.dart index 00c99ec..693397c 100644 --- a/test/application_mock.dart +++ b/test/application_mock.dart @@ -57,11 +57,11 @@ class WidgetTestApplicationMock extends StatelessWidget { } class GoldenTestApplicationMock extends StatefulWidget { - final IAPProductStatus productStatus; + final bool isPro; final Widget child; const GoldenTestApplicationMock({ - this.productStatus = IAPProductStatus.purchased, + this.isPro = true, required this.child, super.key, }); @@ -100,13 +100,22 @@ class _GoldenTestApplicationMockState extends State { @override Widget build(BuildContext context) { return IAPProducts( - products: [ - IAPProduct( - storeId: IAPProductType.paidFeatures.storeId, - status: widget.productStatus, - price: '0.0\$', - ), - ], + isPro: widget.isPro, + lifetime: const IAPProduct( + storeId: '', + type: PurchaseType.lifetime, + price: '0.0\$', + ), + yearly: const IAPProduct( + storeId: '', + type: PurchaseType.yearly, + price: '0.0\$', + ), + monthly: const IAPProduct( + storeId: '', + type: PurchaseType.monthly, + price: '0.0\$', + ), child: _MockApplicationWrapper( child: MockIAPProviders( selectedEquipmentProfileId: mockEquipmentProfiles.first.id, diff --git a/test/providers/equipment_profile_provider_test.dart b/test/providers/equipment_profile_provider_test.dart index 6c8638d..8aacb8b 100644 --- a/test/providers/equipment_profile_provider_test.dart +++ b/test/providers/equipment_profile_provider_test.dart @@ -33,16 +33,10 @@ void main() { reset(storageService); }); - Future pumpTestWidget(WidgetTester tester, IAPProductStatus productStatus) async { + Future pumpTestWidget(WidgetTester tester, bool isPro) async { await tester.pumpWidget( IAPProducts( - products: [ - IAPProduct( - storeId: IAPProductType.paidFeatures.storeId, - status: productStatus, - price: '0.0\$', - ), - ], + isPro: isPro, child: EquipmentProfilesProvider( storageService: storageService, child: const _Application(), @@ -69,31 +63,23 @@ void main() { () { setUp(() { when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles.first.id); - when(() => storageService.getEquipmentProfiles()).thenAnswer((_) => Future.value(_customProfiles.toTogglableMap())); + when(() => storageService.getEquipmentProfiles()) + .thenAnswer((_) => Future.value(_customProfiles.toTogglableMap())); }); testWidgets( - 'IAPProductStatus.purchased - show all saved profiles', + 'Pro - show all saved profiles', (tester) async { - await pumpTestWidget(tester, IAPProductStatus.purchased); + await pumpTestWidget(tester, true); expectEquipmentProfilesCount(_customProfiles.length + 1); expectSelectedEquipmentProfileName(_customProfiles.first.name); }, ); testWidgets( - 'IAPProductStatus.purchasable - show only default', + 'Not Pro - show only default', (tester) async { - await pumpTestWidget(tester, IAPProductStatus.purchasable); - expectEquipmentProfilesCount(1); - expectSelectedEquipmentProfileName(''); - }, - ); - - testWidgets( - 'IAPProductStatus.pending - show only default', - (tester) async { - await pumpTestWidget(tester, IAPProductStatus.pending); + await pumpTestWidget(tester, false); expectEquipmentProfilesCount(1); expectSelectedEquipmentProfileName(''); }, @@ -105,7 +91,7 @@ void main() { 'toggleProfile', (tester) async { when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles.first.id); - await pumpTestWidget(tester, IAPProductStatus.purchased); + await pumpTestWidget(tester, true); expectEquipmentProfilesCount(_customProfiles.length + 1); expectEquipmentProfilesInUseCount(_customProfiles.length + 1); expectSelectedEquipmentProfileName(_customProfiles.first.name); @@ -127,7 +113,7 @@ void main() { when(() => storageService.getEquipmentProfiles()).thenAnswer((_) async => {}); when(() => storageService.selectedEquipmentProfileId).thenReturn(''); - await pumpTestWidget(tester, IAPProductStatus.purchased); + await pumpTestWidget(tester, true); expectEquipmentProfilesCount(1); expectSelectedEquipmentProfileName(''); diff --git a/test/providers/films_provider_test.dart b/test/providers/films_provider_test.dart index d77f053..97b6ec7 100644 --- a/test/providers/films_provider_test.dart +++ b/test/providers/films_provider_test.dart @@ -30,16 +30,10 @@ void main() { reset(storageService); }); - Future pumpTestWidget(WidgetTester tester, IAPProductStatus productStatus) async { + Future pumpTestWidget(WidgetTester tester, bool isPro) async { await tester.pumpWidget( IAPProducts( - products: [ - IAPProduct( - storeId: IAPProductType.paidFeatures.storeId, - status: productStatus, - price: '0.0\$', - ), - ], + isPro: isPro, child: FilmsProvider( storageService: storageService, child: const _Application(), @@ -76,9 +70,9 @@ void main() { }); testWidgets( - 'IAPProductStatus.purchased - show all saved films', + 'Pro - show all saved films', (tester) async { - await pumpTestWidget(tester, IAPProductStatus.purchased); + await pumpTestWidget(tester, true); expectPredefinedFilmsCount(mockPredefinedFilms.length); expectCustomFilmsCount(mockCustomFilms.length); expectFilmsInUseCount(mockPredefinedFilms.length + mockCustomFilms.length + 1); @@ -87,20 +81,9 @@ void main() { ); testWidgets( - 'IAPProductStatus.purchasable - show only default', + 'Not Pro - show only default', (tester) async { - await pumpTestWidget(tester, IAPProductStatus.purchasable); - expectPredefinedFilmsCount(0); - expectCustomFilmsCount(0); - expectFilmsInUseCount(1); - expectSelectedFilmName(''); - }, - ); - - testWidgets( - 'IAPProductStatus.pending - show only default', - (tester) async { - await pumpTestWidget(tester, IAPProductStatus.pending); + await pumpTestWidget(tester, false); expectPredefinedFilmsCount(0); expectCustomFilmsCount(0); expectFilmsInUseCount(1); @@ -117,7 +100,7 @@ void main() { 'toggle predefined film', (tester) async { when(() => storageService.selectedFilmId).thenReturn(mockPredefinedFilms.first.id); - await pumpTestWidget(tester, IAPProductStatus.purchased); + await pumpTestWidget(tester, true); expectPredefinedFilmsCount(mockPredefinedFilms.length); expectCustomFilmsCount(mockCustomFilms.length); expectFilmsInUseCount(mockPredefinedFilms.length + mockCustomFilms.length + 1); @@ -139,7 +122,7 @@ void main() { 'toggle custom film', (tester) async { when(() => storageService.selectedFilmId).thenReturn(mockCustomFilms.first.id); - await pumpTestWidget(tester, IAPProductStatus.purchased); + await pumpTestWidget(tester, true); expectPredefinedFilmsCount(mockPredefinedFilms.length); expectCustomFilmsCount(mockCustomFilms.length); expectFilmsInUseCount(mockPredefinedFilms.length + mockCustomFilms.length + 1); @@ -163,7 +146,7 @@ void main() { 'selectFilm', (tester) async { when(() => storageService.selectedFilmId).thenReturn(''); - await pumpTestWidget(tester, IAPProductStatus.purchased); + await pumpTestWidget(tester, true); expectSelectedFilmName(''); tester.filmsProvider.selectFilm(mockPredefinedFilms.first); @@ -184,7 +167,7 @@ void main() { (tester) async { when(() => storageService.selectedFilmId).thenReturn(''); when(() => storageService.getCustomFilms()).thenAnswer((_) => Future.value({})); - await pumpTestWidget(tester, IAPProductStatus.purchased); + await pumpTestWidget(tester, true); expectPredefinedFilmsCount(mockPredefinedFilms.length); expectCustomFilmsCount(0); expectFilmsInUseCount(mockPredefinedFilms.length + 0 + 1); diff --git a/test/providers/logbook_photos_provider_test.dart b/test/providers/logbook_photos_provider_test.dart index c28e4a4..e0999d1 100644 --- a/test/providers/logbook_photos_provider_test.dart +++ b/test/providers/logbook_photos_provider_test.dart @@ -43,16 +43,10 @@ void main() { reset(storageService); }); - Future pumpTestWidget(WidgetTester tester, IAPProductStatus productStatus) async { + Future pumpTestWidget(WidgetTester tester, bool isPro) async { await tester.pumpWidget( IAPProducts( - products: [ - IAPProduct( - storeId: IAPProductType.paidFeatures.storeId, - status: productStatus, - price: '0.0\$', - ), - ], + isPro: isPro, child: LogbookPhotosProvider( storageService: storageService, geolocationService: geolocationService, @@ -79,27 +73,18 @@ void main() { }); testWidgets( - 'IAPProductStatus.purchased - show all saved photos', + 'Pro - show all saved photos', (tester) async { - await pumpTestWidget(tester, IAPProductStatus.purchased); + await pumpTestWidget(tester, true); expectLogbookPhotosCount(_customPhotos.length); expectLogbookPhotosEnabled(true); }, ); testWidgets( - 'IAPProductStatus.purchasable - show empty list', + 'Not Pro - show empty list', (tester) async { - await pumpTestWidget(tester, IAPProductStatus.purchasable); - expectLogbookPhotosCount(0); - expectLogbookPhotosEnabled(true); - }, - ); - - testWidgets( - 'IAPProductStatus.pending - show empty list', - (tester) async { - await pumpTestWidget(tester, IAPProductStatus.pending); + await pumpTestWidget(tester, false); expectLogbookPhotosCount(0); expectLogbookPhotosEnabled(true); }, @@ -111,7 +96,7 @@ void main() { 'saveLogbookPhotos', (tester) async { when(() => storageService.getPhotos()).thenAnswer((_) => Future.value(_customPhotos)); - await pumpTestWidget(tester, IAPProductStatus.purchased); + await pumpTestWidget(tester, true); expectLogbookPhotosCount(_customPhotos.length); expectLogbookPhotosEnabled(true); @@ -132,7 +117,7 @@ void main() { (tester) async { when(() => storageService.getPhotos()).thenAnswer((_) async => []); - await pumpTestWidget(tester, IAPProductStatus.purchased); + await pumpTestWidget(tester, true); expectLogbookPhotosCount(0); expectLogbookPhotosEnabled(true); @@ -168,7 +153,7 @@ void main() { 'addPhotoIfPossible when disabled', (tester) async { when(() => storageService.getPhotos()).thenAnswer((_) async => []); - await pumpTestWidget(tester, IAPProductStatus.purchased); + await pumpTestWidget(tester, true); // Disable logbook photos tester.logbookPhotosProvider.saveLogbookPhotos(false); diff --git a/test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png b/test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png index 883eed5..d04fdec 100644 Binary files a/test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png and b/test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png differ diff --git a/test/screens/lightmeter_pro/lightmeter_pro_screen_golden_test.dart b/test/screens/lightmeter_pro/lightmeter_pro_screen_golden_test.dart index a70327f..46a44a1 100644 --- a/test/screens/lightmeter_pro/lightmeter_pro_screen_golden_test.dart +++ b/test/screens/lightmeter_pro/lightmeter_pro_screen_golden_test.dart @@ -3,7 +3,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:golden_toolkit/golden_toolkit.dart'; import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/screens/lightmeter_pro/screen_lightmeter_pro.dart'; -import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../application_mock.dart'; @@ -41,8 +40,8 @@ class _MockLightmeterProFlow extends StatelessWidget { @override Widget build(BuildContext context) { - return GoldenTestApplicationMock( - productStatus: IAPProductStatus.purchasable, + return const GoldenTestApplicationMock( + isPro: false, child: LightmeterProScreen(), ); } diff --git a/test/screens/metering/components/shared/readings_container/equipment_profile_picker_test.dart b/test/screens/metering/components/shared/readings_container/equipment_profile_picker_test.dart index b9dcb51..d66b01d 100644 --- a/test/screens/metering/components/shared/readings_container/equipment_profile_picker_test.dart +++ b/test/screens/metering/components/shared/readings_container/equipment_profile_picker_test.dart @@ -26,13 +26,7 @@ void main() { Future pumpApplication(WidgetTester tester) async { await tester.pumpWidget( IAPProducts( - products: [ - IAPProduct( - storeId: IAPProductType.paidFeatures.storeId, - status: IAPProductStatus.purchased, - price: '0.0\$', - ), - ], + isPro: true, child: EquipmentProfilesProvider( storageService: storageService, child: WidgetTestApplicationMock( diff --git a/test/screens/metering/components/shared/readings_container/film_picker_test.dart b/test/screens/metering/components/shared/readings_container/film_picker_test.dart index 7e12256..8fc4f9d 100644 --- a/test/screens/metering/components/shared/readings_container/film_picker_test.dart +++ b/test/screens/metering/components/shared/readings_container/film_picker_test.dart @@ -28,13 +28,7 @@ void main() { Future pumpApplication(WidgetTester tester) async { await tester.pumpWidget( IAPProducts( - products: [ - IAPProduct( - storeId: IAPProductType.paidFeatures.storeId, - status: IAPProductStatus.purchased, - price: '0.0\$', - ), - ], + isPro: true, child: FilmsProvider( storageService: mockFilmsStorageService, child: const WidgetTestApplicationMock( diff --git a/test/screens/metering/screen_metering_golden_test.dart b/test/screens/metering/screen_metering_golden_test.dart index 9ca908f..bcd77c8 100644 --- a/test/screens/metering/screen_metering_golden_test.dart +++ b/test/screens/metering/screen_metering_golden_test.dart @@ -9,7 +9,6 @@ import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/metering/flow_metering.dart'; -import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../integration_test/utils/finder_actions.dart'; @@ -18,27 +17,27 @@ import '../../application_mock.dart'; import '../../utils/golden_test_set_theme.dart'; class _MeteringScreenConfig { - final IAPProductStatus iapProductStatus; + final bool isPro; final EvSourceType evSourceType; _MeteringScreenConfig( - this.iapProductStatus, + this.isPro, this.evSourceType, ); @override String toString() { final buffer = StringBuffer(); - buffer.write(iapProductStatus.toString().split('.')[1]); + buffer.write(isPro ? 'purchased' : 'purchasable'); buffer.write(' - '); buffer.write(evSourceType.toString().split('.')[1]); return buffer.toString(); } } -final _testScenarios = [IAPProductStatus.purchased, IAPProductStatus.purchasable].expand( - (iapProductStatus) => EvSourceType.values.map( - (evSourceType) => _MeteringScreenConfig(iapProductStatus, evSourceType), +final _testScenarios = [true, false].expand( + (isPro) => EvSourceType.values.map( + (evSourceType) => _MeteringScreenConfig(isPro, evSourceType), ), ); @@ -97,7 +96,7 @@ void main() { for (final scenario in _testScenarios) { builder.addScenario( name: scenario.toString(), - widget: _MockMeteringFlow(productStatus: scenario.iapProductStatus), + widget: _MockMeteringFlow(isPro: scenario.isPro), onCreate: (scenarioWidgetKey) async { await setEvSource(tester, scenarioWidgetKey, scenario.evSourceType); if (scenarioWidgetKey.toString().contains('Dark')) { @@ -121,14 +120,14 @@ void main() { } class _MockMeteringFlow extends StatelessWidget { - final IAPProductStatus productStatus; + final bool isPro; - const _MockMeteringFlow({required this.productStatus}); + const _MockMeteringFlow({required this.isPro}); @override Widget build(BuildContext context) { return GoldenTestApplicationMock( - productStatus: productStatus, + isPro: isPro, child: const MeteringFlow(), ); } diff --git a/test/screens/metering/utils/listener_equipment_profiles_test.dart b/test/screens/metering/utils/listener_equipment_profiles_test.dart index d27bd5a..cf04ef4 100644 --- a/test/screens/metering/utils/listener_equipment_profiles_test.dart +++ b/test/screens/metering/utils/listener_equipment_profiles_test.dart @@ -36,13 +36,7 @@ void main() { Future pumpTestWidget(WidgetTester tester) async { await tester.pumpWidget( IAPProducts( - products: [ - IAPProduct( - storeId: IAPProductType.paidFeatures.storeId, - status: IAPProductStatus.purchased, - price: '0.0\$', - ), - ], + isPro: true, child: EquipmentProfilesProvider( storageService: storageService, child: MaterialApp( diff --git a/test/screens/settings/settings_screen_golden_test.dart b/test/screens/settings/settings_screen_golden_test.dart index 6c34522..da19a9c 100644 --- a/test/screens/settings/settings_screen_golden_test.dart +++ b/test/screens/settings/settings_screen_golden_test.dart @@ -8,7 +8,6 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/screens/settings/flow_settings.dart'; -import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -16,21 +15,19 @@ import '../../application_mock.dart'; import '../../utils/golden_test_set_theme.dart'; class _SettingsScreenConfig { - final IAPProductStatus iapProductStatus; + final bool isPro; - _SettingsScreenConfig(this.iapProductStatus); + _SettingsScreenConfig(this.isPro); @override String toString() { final buffer = StringBuffer(); - buffer.write(iapProductStatus.toString().split('.')[1]); + buffer.write(isPro ? 'purchased' : 'purchasable'); return buffer.toString(); } } -final _testScenarios = [IAPProductStatus.purchased, IAPProductStatus.purchasable].map( - (iapProductStatus) => _SettingsScreenConfig(iapProductStatus), -); +final _testScenarios = [true, false].map((isPro) => _SettingsScreenConfig(isPro)); void main() { setUpAll(() { @@ -60,7 +57,7 @@ void main() { for (final scenario in _testScenarios) { builder.addScenario( name: scenario.toString(), - widget: _MockSettingsFlow(productStatus: scenario.iapProductStatus), + widget: _MockSettingsFlow(isPro: scenario.isPro), onCreate: (scenarioWidgetKey) async { if (scenarioWidgetKey.toString().contains('Dark')) { await setTheme(tester, scenarioWidgetKey, ThemeType.dark); @@ -78,14 +75,14 @@ void main() { } class _MockSettingsFlow extends StatelessWidget { - final IAPProductStatus productStatus; + final bool isPro; - const _MockSettingsFlow({required this.productStatus}); + const _MockSettingsFlow({required this.isPro}); @override Widget build(BuildContext context) { return GoldenTestApplicationMock( - productStatus: productStatus, + isPro: isPro, child: const SettingsFlow(), ); }