mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2025-08-26 23:16:42 +00:00
aligned with iap
This commit is contained in:
parent
0f9992b4c9
commit
c86eeb9987
9 changed files with 199 additions and 73 deletions
|
@ -2,10 +2,9 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:m3_lightmeter_iap/src/data/models/iap_product.dart';
|
import 'package:m3_lightmeter_iap/src/data/models/iap_product.dart';
|
||||||
|
|
||||||
class IAPProductsProvider extends StatefulWidget {
|
class IAPProductsProvider extends StatefulWidget {
|
||||||
final String apiUrl;
|
|
||||||
final Widget child;
|
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)!;
|
static IAPProductsProviderState of(BuildContext context) => IAPProductsProvider.maybeOf(context)!;
|
||||||
|
|
||||||
|
@ -21,13 +20,7 @@ class IAPProductsProviderState extends State<IAPProductsProvider> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IAPProducts(
|
return IAPProducts(
|
||||||
products: [
|
isPro: true,
|
||||||
IAPProduct(
|
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
|
||||||
status: IAPProductStatus.purchased,
|
|
||||||
price: '0.0\$',
|
|
||||||
)
|
|
||||||
],
|
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,16 +20,11 @@ class MockIAPProductsProvider extends StatefulWidget {
|
||||||
|
|
||||||
class MockIAPProductsProviderState extends State<MockIAPProductsProvider> {
|
class MockIAPProductsProviderState extends State<MockIAPProductsProvider> {
|
||||||
late bool _purchased = widget.initialyPurchased;
|
late bool _purchased = widget.initialyPurchased;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IAPProducts(
|
return IAPProducts(
|
||||||
products: List.from([
|
isPro: _purchased,
|
||||||
IAPProduct(
|
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
|
||||||
status: _purchased ? IAPProductStatus.purchased : IAPProductStatus.purchasable,
|
|
||||||
price: '0.0\$',
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/res/dimens.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/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart';
|
||||||
import 'package:lightmeter/screens/metering/screen_metering.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 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
import '../mocks/iap_products_mock.dart';
|
import '../mocks/iap_products_mock.dart';
|
||||||
|
@ -19,7 +18,7 @@ const mockPhotoEv100 = 8.3;
|
||||||
|
|
||||||
extension WidgetTesterCommonActions on WidgetTester {
|
extension WidgetTesterCommonActions on WidgetTester {
|
||||||
Future<void> pumpApplication({
|
Future<void> pumpApplication({
|
||||||
IAPProductStatus productStatus = IAPProductStatus.purchased,
|
bool isPro = true,
|
||||||
TogglableMap<EquipmentProfile>? equipmentProfiles,
|
TogglableMap<EquipmentProfile>? equipmentProfiles,
|
||||||
String selectedEquipmentProfileId = '',
|
String selectedEquipmentProfileId = '',
|
||||||
TogglableMap<Film>? predefinedFilms,
|
TogglableMap<Film>? predefinedFilms,
|
||||||
|
@ -28,7 +27,7 @@ extension WidgetTesterCommonActions on WidgetTester {
|
||||||
}) async {
|
}) async {
|
||||||
await pumpWidget(
|
await pumpWidget(
|
||||||
MockIAPProductsProvider(
|
MockIAPProductsProvider(
|
||||||
initialyPurchased: productStatus == IAPProductStatus.purchased,
|
initialyPurchased: isPro,
|
||||||
child: ApplicationWrapper(
|
child: ApplicationWrapper(
|
||||||
const Environment.dev(),
|
const Environment.dev(),
|
||||||
child: MockIAPProviders(
|
child: MockIAPProviders(
|
||||||
|
|
|
@ -5,7 +5,6 @@ import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'package:lightmeter/application.dart';
|
import 'package:lightmeter/application.dart';
|
||||||
import 'package:lightmeter/application_wrapper.dart';
|
import 'package:lightmeter/application_wrapper.dart';
|
||||||
import 'package:lightmeter/constants.dart';
|
|
||||||
import 'package:lightmeter/data/analytics/analytics.dart';
|
import 'package:lightmeter/data/analytics/analytics.dart';
|
||||||
import 'package:lightmeter/data/analytics/api/analytics_firebase.dart';
|
import 'package:lightmeter/data/analytics/api/analytics_firebase.dart';
|
||||||
import 'package:lightmeter/environment.dart';
|
import 'package:lightmeter/environment.dart';
|
||||||
|
@ -27,18 +26,10 @@ Future<void> runLightmeterApp(Environment env) async {
|
||||||
runApp(
|
runApp(
|
||||||
env.buildType == BuildType.dev
|
env.buildType == BuildType.dev
|
||||||
? IAPProducts(
|
? IAPProducts(
|
||||||
products: [
|
isPro: true,
|
||||||
IAPProduct(
|
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
|
||||||
price: '0.0\$',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: application,
|
child: application,
|
||||||
)
|
)
|
||||||
: IAPProductsProvider(
|
: IAPProductsProvider(child: application),
|
||||||
apiUrl: iapServerUrl,
|
|
||||||
child: application,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_errorsLogger.logCrash,
|
_errorsLogger.logCrash,
|
||||||
|
|
|
@ -0,0 +1,184 @@
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
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:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||||
|
|
||||||
|
class LightmeterProBottomControls extends StatefulWidget {
|
||||||
|
const LightmeterProBottomControls({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LightmeterProBottomControls> createState() => _LightmeterProBottomControlsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LightmeterProBottomControlsState extends State<LightmeterProBottomControls> {
|
||||||
|
late final Future<List<IAPProduct>> productsFuture;
|
||||||
|
bool _isLoading = true;
|
||||||
|
IAPProduct? monthly;
|
||||||
|
IAPProduct? lifetime;
|
||||||
|
IAPProduct? selected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
productsFuture = IAPProductsProvider.of(context).fetchProducts();
|
||||||
|
productsFuture.then((products) {
|
||||||
|
monthly = products.firstWhereOrNull((p) => p.type == PurchaseType.monthly);
|
||||||
|
lifetime = products.firstWhereOrNull((p) => p.type == PurchaseType.lifetime);
|
||||||
|
selected = monthly ?? lifetime;
|
||||||
|
}).onError((_, __) {
|
||||||
|
///
|
||||||
|
}).whenComplete(() {
|
||||||
|
_isLoading = false;
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return 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,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: Dimens.durationM,
|
||||||
|
child: _isLoading
|
||||||
|
? const CircularProgressIndicator()
|
||||||
|
: _Products(
|
||||||
|
monthly: monthly,
|
||||||
|
lifetime: lifetime,
|
||||||
|
selected: selected,
|
||||||
|
onProductSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
selected = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Dimens.grid16),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: selected != null
|
||||||
|
? () {
|
||||||
|
IAPProductsProvider.of(context).buyPro(selected!);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Text("Continue"),
|
||||||
|
),
|
||||||
|
const _RestorePurchasesButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Products extends StatelessWidget {
|
||||||
|
const _Products({
|
||||||
|
this.monthly,
|
||||||
|
this.lifetime,
|
||||||
|
required this.selected,
|
||||||
|
required this.onProductSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IAPProduct? monthly;
|
||||||
|
final IAPProduct? lifetime;
|
||||||
|
final IAPProduct? selected;
|
||||||
|
final ValueChanged<IAPProduct> onProductSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
if (monthly case final monthly?)
|
||||||
|
_OfferingItems(
|
||||||
|
product: monthly,
|
||||||
|
isSelected: selected == monthly,
|
||||||
|
onPressed: () => onProductSelected(monthly),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Dimens.grid8),
|
||||||
|
if (lifetime case final lifetime?)
|
||||||
|
_OfferingItems(
|
||||||
|
product: lifetime,
|
||||||
|
isSelected: selected == lifetime,
|
||||||
|
onPressed: () => onProductSelected(lifetime),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OfferingItems extends StatelessWidget {
|
||||||
|
const _OfferingItems({
|
||||||
|
required this.product,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.onPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IAPProduct product;
|
||||||
|
final bool isSelected;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
shape: isSelected
|
||||||
|
? RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(Dimens.borderRadiusL),
|
||||||
|
side: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onPressed,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingS),
|
||||||
|
child: ListTile(
|
||||||
|
title: AnimatedDefaultTextStyle(
|
||||||
|
duration: Dimens.durationM,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge!.boldIfSelected(isSelected),
|
||||||
|
child: Text(product.type.name),
|
||||||
|
),
|
||||||
|
trailing: AnimatedDefaultTextStyle(
|
||||||
|
duration: Dimens.durationM,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium!.boldIfSelected(isSelected),
|
||||||
|
child: Text(product.price),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RestorePurchasesButton extends StatelessWidget {
|
||||||
|
const _RestorePurchasesButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: () {},
|
||||||
|
child: Text(S.of(context).restorePurchases),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on TextStyle {
|
||||||
|
TextStyle boldIfSelected(bool isSelected) => copyWith(fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal);
|
||||||
|
}
|
|
@ -2,12 +2,10 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/data/models/app_feature.dart';
|
import 'package:lightmeter/data/models/app_feature.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/providers/services_provider.dart';
|
|
||||||
import 'package:lightmeter/res/dimens.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/screens/shared/sliver_screen/screen_sliver.dart';
|
||||||
import 'package:lightmeter/utils/text_height.dart';
|
import 'package:lightmeter/utils/text_height.dart';
|
||||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
|
||||||
|
|
||||||
class LightmeterProScreen extends StatelessWidget {
|
class LightmeterProScreen extends StatelessWidget {
|
||||||
final features =
|
final features =
|
||||||
|
@ -64,26 +62,7 @@ class LightmeterProScreen extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
const LightmeterProBottomControls(),
|
||||||
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: 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)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,19 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/navigation/routes.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 {
|
class BuyProListTile extends StatelessWidget {
|
||||||
const BuyProListTile({super.key});
|
const BuyProListTile({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final status = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status;
|
// TODO: implement pending handling via REvenueCat
|
||||||
final isPending = status == IAPProductStatus.purchased || status == null;
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.bolt),
|
leading: const Icon(Icons.bolt),
|
||||||
title: Text(S.of(context).getPro),
|
title: Text(S.of(context).getPro),
|
||||||
onTap: !isPending
|
onTap: () {
|
||||||
? () {
|
Navigator.of(context).pushNamed(NavigationRoutes.proFeaturesScreen.name);
|
||||||
Navigator.of(context).pushNamed(NavigationRoutes.proFeaturesScreen.name);
|
},
|
||||||
}
|
|
||||||
: null,
|
|
||||||
trailing: isPending
|
|
||||||
? const SizedBox(
|
|
||||||
height: Dimens.grid24,
|
|
||||||
width: Dimens.grid24,
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart';
|
import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart';
|
||||||
import 'package:lightmeter/utils/context_utils.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
|
/// Depends on the product status and replaces [onTap] with purchase callback
|
||||||
/// if the product is purchasable.
|
/// if the product is purchasable.
|
||||||
class IAPListTile extends StatelessWidget {
|
class IAPListTile extends StatelessWidget {
|
||||||
final IAPProductType product;
|
|
||||||
final Icon leading;
|
final Icon leading;
|
||||||
final Text title;
|
final Text title;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final bool showPendingTrailing;
|
final bool showPendingTrailing;
|
||||||
|
|
||||||
const IAPListTile({
|
const IAPListTile({
|
||||||
this.product = IAPProductType.paidFeatures,
|
|
||||||
required this.leading,
|
required this.leading,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
|
|
|
@ -8,5 +8,5 @@ extension BuildContextUtils on BuildContext {
|
||||||
return UserPreferencesProvider.meteringScreenFeatureOf(this, feature);
|
return UserPreferencesProvider.meteringScreenFeatureOf(this, feature);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isPro => IAPProducts.isPurchased(this, IAPProductType.paidFeatures);
|
bool get isPro => IAPProducts.isPro(this);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue