mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2025-08-26 15:06:43 +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';
|
||||
|
||||
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,13 +20,7 @@ class IAPProductsProviderState extends State<IAPProductsProvider> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IAPProducts(
|
||||
products: [
|
||||
IAPProduct(
|
||||
storeId: IAPProductType.paidFeatures.storeId,
|
||||
status: IAPProductStatus.purchased,
|
||||
price: '0.0\$',
|
||||
)
|
||||
],
|
||||
isPro: true,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,16 +20,11 @@ class MockIAPProductsProvider extends StatefulWidget {
|
|||
|
||||
class MockIAPProductsProviderState extends State<MockIAPProductsProvider> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<void> pumpApplication({
|
||||
IAPProductStatus productStatus = IAPProductStatus.purchased,
|
||||
bool isPro = true,
|
||||
TogglableMap<EquipmentProfile>? equipmentProfiles,
|
||||
String selectedEquipmentProfileId = '',
|
||||
TogglableMap<Film>? 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(
|
||||
|
|
|
@ -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<void> 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,
|
||||
|
|
|
@ -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: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 {
|
||||
final features =
|
||||
|
@ -64,26 +62,7 @@ class LightmeterProScreen extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
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: 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 LightmeterProBottomControls(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue