mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-24 16:30:40 +00:00
ML-130 Integrate Firebase Remote Config (#132)
* implemented `RemoteConfigService` * added alternative translations * typo * added `firebase_analytics` * dim paid features list tiles * log list tile tap instead of dialog * implemented `RemoteConfigProvider` * typo
This commit is contained in:
parent
f3b08868be
commit
a52efcd341
19 changed files with 425 additions and 40 deletions
|
@ -1,13 +1,17 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/analytics/analytics.dart';
|
||||
import 'package:lightmeter/data/analytics/api/analytics_firebase.dart';
|
||||
import 'package:lightmeter/data/caffeine_service.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||
import 'package:lightmeter/data/permissions_service.dart';
|
||||
import 'package:lightmeter/data/remote_config_service.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/data/volume_events_service.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
||||
import 'package:lightmeter/providers/films_provider.dart';
|
||||
import 'package:lightmeter/providers/remote_config_provider.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
|
@ -23,9 +27,10 @@ class ApplicationWrapper extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: Future.wait([
|
||||
future: Future.wait<dynamic>([
|
||||
SharedPreferences.getInstance(),
|
||||
const LightSensorService(LocalPlatform()).hasSensor(),
|
||||
const RemoteConfigService().activeAndFetchFeatures(),
|
||||
]),
|
||||
builder: (_, snapshot) {
|
||||
if (snapshot.data != null) {
|
||||
|
@ -33,6 +38,7 @@ class ApplicationWrapper extends StatelessWidget {
|
|||
final userPreferencesService = UserPreferencesService(snapshot.data![0] as SharedPreferences);
|
||||
final hasLightSensor = snapshot.data![1] as bool;
|
||||
return ServicesProvider(
|
||||
analytics: const LightmeterAnalytics(api: LightmeterAnalyticsFirebase()),
|
||||
caffeineService: const CaffeineService(),
|
||||
environment: env.copyWith(hasLightSensor: hasLightSensor),
|
||||
hapticsService: const HapticsService(),
|
||||
|
@ -40,6 +46,8 @@ class ApplicationWrapper extends StatelessWidget {
|
|||
permissionsService: const PermissionsService(),
|
||||
userPreferencesService: userPreferencesService,
|
||||
volumeEventsService: const VolumeEventsService(LocalPlatform()),
|
||||
child: RemoteConfigProvider(
|
||||
remoteConfigService: const RemoteConfigService(),
|
||||
child: EquipmentProfileProvider(
|
||||
storageService: iapService,
|
||||
child: FilmsProvider(
|
||||
|
@ -51,6 +59,7 @@ class ApplicationWrapper extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.error != null) {
|
||||
return Center(child: Text(snapshot.error!.toString()));
|
||||
|
|
34
lib/data/analytics/analytics.dart
Normal file
34
lib/data/analytics/analytics.dart
Normal file
|
@ -0,0 +1,34 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:lightmeter/data/analytics/api/analytics_api_interface.dart';
|
||||
import 'package:lightmeter/data/analytics/entity/analytics_event.dart';
|
||||
|
||||
class LightmeterAnalytics {
|
||||
final ILightmeterAnalyticsApi _api;
|
||||
|
||||
const LightmeterAnalytics({required ILightmeterAnalyticsApi api}) : _api = api;
|
||||
|
||||
Future<void> logEvent(
|
||||
LightmeterAnalyticsEvent event, {
|
||||
Map<String, dynamic>? parameters,
|
||||
}) async {
|
||||
if (kDebugMode) {
|
||||
log('<LightmeterAnalytics> logEvent: ${event.name} / $parameters');
|
||||
return;
|
||||
}
|
||||
|
||||
return _api.logEvent(
|
||||
event: event,
|
||||
parameters: parameters,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> logUnlockProFeatures(String listTileTitle) async {
|
||||
return logEvent(
|
||||
LightmeterAnalyticsEvent.unlockProFeatures,
|
||||
parameters: {"listTileTitle": listTileTitle},
|
||||
);
|
||||
}
|
||||
}
|
8
lib/data/analytics/api/analytics_api_interface.dart
Normal file
8
lib/data/analytics/api/analytics_api_interface.dart
Normal file
|
@ -0,0 +1,8 @@
|
|||
import 'package:lightmeter/data/analytics/entity/analytics_event.dart';
|
||||
|
||||
abstract class ILightmeterAnalyticsApi {
|
||||
Future<void> logEvent({
|
||||
required LightmeterAnalyticsEvent event,
|
||||
Map<String, dynamic>? parameters,
|
||||
});
|
||||
}
|
26
lib/data/analytics/api/analytics_firebase.dart
Normal file
26
lib/data/analytics/api/analytics_firebase.dart
Normal file
|
@ -0,0 +1,26 @@
|
|||
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:lightmeter/data/analytics/api/analytics_api_interface.dart';
|
||||
import 'package:lightmeter/data/analytics/entity/analytics_event.dart';
|
||||
|
||||
class LightmeterAnalyticsFirebase implements ILightmeterAnalyticsApi {
|
||||
const LightmeterAnalyticsFirebase();
|
||||
|
||||
@override
|
||||
Future<void> logEvent({
|
||||
required LightmeterAnalyticsEvent event,
|
||||
Map<String, dynamic>? parameters,
|
||||
}) async {
|
||||
try {
|
||||
await FirebaseAnalytics.instance.logEvent(
|
||||
name: event.name,
|
||||
parameters: parameters,
|
||||
);
|
||||
} on FirebaseException catch (e) {
|
||||
debugPrint('Firebase Analytics Exception: $e');
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
3
lib/data/analytics/entity/analytics_event.dart
Normal file
3
lib/data/analytics/entity/analytics_event.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
enum LightmeterAnalyticsEvent {
|
||||
unlockProFeatures,
|
||||
}
|
5
lib/data/models/feature.dart
Normal file
5
lib/data/models/feature.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
enum Feature { unlockProFeaturesText }
|
||||
|
||||
const featuresDefaultValues = {
|
||||
Feature.unlockProFeaturesText: false,
|
||||
};
|
81
lib/data/remote_config_service.dart
Normal file
81
lib/data/remote_config_service.dart
Normal file
|
@ -0,0 +1,81 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:firebase_remote_config/firebase_remote_config.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:lightmeter/data/models/feature.dart';
|
||||
|
||||
class RemoteConfigService {
|
||||
const RemoteConfigService();
|
||||
|
||||
Future<void> activeAndFetchFeatures() async {
|
||||
final FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.instance;
|
||||
const cacheStaleDuration = kDebugMode ? Duration(minutes: 1) : Duration(hours: 12);
|
||||
|
||||
try {
|
||||
await remoteConfig.setConfigSettings(
|
||||
RemoteConfigSettings(
|
||||
fetchTimeout: const Duration(seconds: 15),
|
||||
minimumFetchInterval: cacheStaleDuration,
|
||||
),
|
||||
);
|
||||
await remoteConfig.setDefaults(featuresDefaultValues.map((key, value) => MapEntry(key.name, value)));
|
||||
await remoteConfig.activate();
|
||||
await remoteConfig.ensureInitialized();
|
||||
unawaited(remoteConfig.fetch());
|
||||
|
||||
log('Firebase remote config initialized successfully');
|
||||
} on FirebaseException catch (e) {
|
||||
_logError('Firebase exception during Firebase Remote Config initialization: $e');
|
||||
} on Exception catch (e) {
|
||||
_logError('Error during Firebase Remote Config initialization: $e');
|
||||
}
|
||||
}
|
||||
|
||||
dynamic getValue(Feature feature) => FirebaseRemoteConfig.instance.getValue(feature.name).toValue(feature);
|
||||
|
||||
Map<Feature, dynamic> getAll() {
|
||||
final Map<Feature, dynamic> result = {};
|
||||
for (final value in FirebaseRemoteConfig.instance.getAll().entries) {
|
||||
try {
|
||||
final feature = Feature.values.firstWhere((f) => f.name == value.key);
|
||||
result[feature] = value.value.toValue(feature);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Stream<Set<Feature>> onConfigUpdated() => FirebaseRemoteConfig.instance.onConfigUpdated.asyncMap(
|
||||
(event) async {
|
||||
await FirebaseRemoteConfig.instance.activate();
|
||||
final Set<Feature> updatedFeatures = {};
|
||||
for (final key in event.updatedKeys) {
|
||||
try {
|
||||
updatedFeatures.add(Feature.values.firstWhere((element) => element.name == key));
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
return updatedFeatures;
|
||||
},
|
||||
);
|
||||
|
||||
bool isEnabled(Feature feature) => FirebaseRemoteConfig.instance.getBool(feature.name);
|
||||
|
||||
void _logError(dynamic throwable, {StackTrace? stackTrace}) {
|
||||
FirebaseCrashlytics.instance.recordError(throwable, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
extension on RemoteConfigValue {
|
||||
dynamic toValue(Feature feature) {
|
||||
switch (feature) {
|
||||
case Feature.unlockProFeaturesText:
|
||||
return asBool();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -92,10 +92,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"buyLightmeterPro": "Buy Lightmeter Pro",
|
||||
"lightmeterPro": "Lightmeter Pro",
|
||||
"buyLightmeterPro": "Buy Lightmeter Pro",
|
||||
"lightmeterProDescription": "Unlocks extra features, such as equipment profiles containing filters for aperture, shutter speed, and more; and a list of films with compensation for what's known as reciprocity failure.\n\nThe source code of Lightmeter is available on GitHub. You are welcome to compile it yourself. However, if you want to support the development and receive new features and updates, consider purchasing Lightmeter Pro.",
|
||||
"buy": "Buy",
|
||||
"proFeatures": "Pro features",
|
||||
"unlockProFeatures": "Unlock Pro features",
|
||||
"unlockProFeaturesDescription": "Unlock professional features, such as equipment profiles containing filters for aperture, shutter speed, and more; and a list of films with compensation for what's known as reciprocity failure.\n\nBy unlocking Pro features you support the development and make it possible to add new features to the app.",
|
||||
"unlock": "Unlock",
|
||||
"tooltipAdd": "Add",
|
||||
"tooltipClose": "Close",
|
||||
"tooltipExpand": "Expand",
|
||||
|
|
|
@ -96,6 +96,10 @@
|
|||
"lightmeterPro": "Lightmeter Pro",
|
||||
"lightmeterProDescription": "Déverrouille des fonctionnalités supplémentaires, telles que des profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec une compensation pour ce que l'on appelle l'échec de réciprocité.\n\nLe code source du Lightmeter est disponible sur GitHub. Vous pouvez le compiler vous-même. Cependant, si vous souhaitez soutenir le développement et recevoir de nouvelles fonctionnalités et mises à jour, envisagez d'acheter Lightmeter Pro.",
|
||||
"buy": "Acheter",
|
||||
"proFeatures": "Fonctionnalités professionnelles",
|
||||
"unlockProFeatures": "Déverrouiller les fonctionnalités professionnelles",
|
||||
"unlockProFeaturesDescription": "Déverrouillez des fonctions professionnelles, telles que des profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec compensation pour ce que l'on appelle l'échec de réciprocité.\n\nEn débloquant les fonctionnalités Pro, vous soutenez le développement et permettez d'ajouter de nouvelles fonctionnalités à l'application.",
|
||||
"unlock": "Déverrouiller",
|
||||
"tooltipAdd": "Ajouter",
|
||||
"tooltipClose": "Fermer",
|
||||
"tooltipExpand": "Élargir",
|
||||
|
|
|
@ -96,6 +96,10 @@
|
|||
"lightmeterPro": "Lightmeter Pro",
|
||||
"lightmeterProDescription": "Даёт доступ к таким функциям как профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений, а также набору пленок с компенсацией эффекта Шварцшильда.\n\nИсходный код Lightmeter доступен на GitHub. Вы можете собрать его самостоятельно. Однако если вы хотите поддержать разработку и получать новые функции и обновления, то приобретите Lightmeter Pro.",
|
||||
"buy": "Купить",
|
||||
"proFeatures": "Профессиональные настройки",
|
||||
"unlockProFeatures": "Разблокировать профессиональные настройки",
|
||||
"unlockProFeaturesDescription": "Вы можете разблокировать профессиональные настройки, такие как профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений, а также набору пленок с компенсацией эффекта Шварцшильда.\n\nПолучая доступ к профессиональным настройкам, вы поддерживаете разработку и делаете возможным появление новых функций в приложении.",
|
||||
"unlock": "Разблокировать",
|
||||
"tooltipAdd": "Добавить",
|
||||
"tooltipClose": "Закрыть",
|
||||
"tooltipExpand": "Развернуть",
|
||||
|
|
|
@ -96,6 +96,10 @@
|
|||
"lightmeterPro": "Lightmeter Pro",
|
||||
"lightmeterProDescription": "购买以解锁额外功能。例如包含光圈、快门速度等参数的配置文件;以及一个胶卷预设列表来提供倒易率失效时的曝光补偿。\n\n您可以在 GitHub 上获取 Lightmeter 的源代码,欢迎自行编译。不过,如果您想支持开发并获得新功能和更新,请考虑购买 Lightmeter Pro。",
|
||||
"buy": "购买",
|
||||
"proFeatures": "专业功能",
|
||||
"unlockProFeatures": "解锁专业功能",
|
||||
"unlockProFeaturesDescription": "解锁专业功能。例如包含光圈、快门速度等参数的配置文件;以及一个胶卷预设列表来提供倒易率失效时的曝光补偿。\n\n通过解锁专业版功能,您可以支持开发工作,帮助为应用程序添加新功能。",
|
||||
"unlock": "解锁",
|
||||
"tooltipAdd": "添加",
|
||||
"tooltipClose": "关闭",
|
||||
"tooltipExpand": "展开",
|
||||
|
|
78
lib/providers/remote_config_provider.dart
Normal file
78
lib/providers/remote_config_provider.dart
Normal file
|
@ -0,0 +1,78 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/feature.dart';
|
||||
import 'package:lightmeter/data/remote_config_service.dart';
|
||||
|
||||
class RemoteConfigProvider extends StatefulWidget {
|
||||
final RemoteConfigService remoteConfigService;
|
||||
final Widget child;
|
||||
|
||||
const RemoteConfigProvider({
|
||||
required this.remoteConfigService,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RemoteConfigProvider> createState() => RemoteConfigProviderState();
|
||||
}
|
||||
|
||||
class RemoteConfigProviderState extends State<RemoteConfigProvider> {
|
||||
late final Map<Feature, dynamic> _config = widget.remoteConfigService.getAll();
|
||||
late final StreamSubscription<Set<Feature>> _updatesSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updatesSubscription = widget.remoteConfigService.onConfigUpdated().listen(_updateFeatures);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_updatesSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RemoteConfig(
|
||||
config: _config,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
void _updateFeatures(Set<Feature> updatedFeatures) {
|
||||
for (final feature in updatedFeatures) {
|
||||
_config[feature] = widget.remoteConfigService.getValue(feature);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteConfig extends InheritedModel<Feature> {
|
||||
final Map<Feature, dynamic> _config;
|
||||
|
||||
const RemoteConfig({
|
||||
super.key,
|
||||
required Map<Feature, dynamic> config,
|
||||
required super.child,
|
||||
}) : _config = config;
|
||||
|
||||
static bool isEnabled(BuildContext context, Feature feature) {
|
||||
return InheritedModel.inheritFrom<RemoteConfig>(context)!._config[feature] as bool;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(RemoteConfig oldWidget) => true;
|
||||
|
||||
@override
|
||||
bool updateShouldNotifyDependent(RemoteConfig oldWidget, Set<Feature> features) {
|
||||
for (final feature in features) {
|
||||
if (oldWidget._config[feature] != _config[feature]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/analytics/analytics.dart';
|
||||
import 'package:lightmeter/data/caffeine_service.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||
|
@ -9,6 +10,7 @@ import 'package:lightmeter/environment.dart';
|
|||
|
||||
// coverage:ignore-start
|
||||
class ServicesProvider extends InheritedWidget {
|
||||
final LightmeterAnalytics analytics;
|
||||
final CaffeineService caffeineService;
|
||||
final Environment environment;
|
||||
final HapticsService hapticsService;
|
||||
|
@ -18,6 +20,7 @@ class ServicesProvider extends InheritedWidget {
|
|||
final VolumeEventsService volumeEventsService;
|
||||
|
||||
const ServicesProvider({
|
||||
required this.analytics,
|
||||
required this.caffeineService,
|
||||
required this.environment,
|
||||
required this.hapticsService,
|
||||
|
|
|
@ -1,20 +1,36 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/feature.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart';
|
||||
import 'package:lightmeter/providers/remote_config_provider.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/settings/components/utils/show_buy_pro_dialog.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
|
||||
class BuyProListTile extends StatelessWidget {
|
||||
const BuyProListTile({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IAPListTile(
|
||||
final unlockFeaturesEnabled = RemoteConfig.isEnabled(context, Feature.unlockProFeaturesText);
|
||||
final status = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status;
|
||||
final isPending = status == IAPProductStatus.purchased || status == null;
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.star),
|
||||
title: Text(S.of(context).buyLightmeterPro),
|
||||
title: Text(unlockFeaturesEnabled ? S.of(context).unlockProFeatures : S.of(context).buyLightmeterPro),
|
||||
onTap: () {
|
||||
showBuyProDialog(context);
|
||||
ServicesProvider.of(context)
|
||||
.analytics
|
||||
.logUnlockProFeatures(unlockFeaturesEnabled ? 'Unlock Pro features' : 'Buy Lightmeter Pro');
|
||||
},
|
||||
showPendingTrailing: true,
|
||||
trailing: isPending
|
||||
? const SizedBox(
|
||||
height: Dimens.grid24,
|
||||
width: Dimens.grid24,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/feature.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/remote_config_provider.dart';
|
||||
import 'package:lightmeter/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart';
|
||||
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
|
||||
|
||||
|
@ -9,7 +11,9 @@ class LightmeterProSettingsSection extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsSection(
|
||||
title: S.of(context).lightmeterPro,
|
||||
title: RemoteConfig.isEnabled(context, Feature.unlockProFeaturesText)
|
||||
? S.of(context).proFeatures
|
||||
: S.of(context).lightmeterPro,
|
||||
children: const [BuyProListTile()],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/settings/components/utils/show_buy_pro_dialog.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
|
||||
/// Depends on the product status and replaces [onTap] with purchase callback
|
||||
|
@ -23,24 +22,14 @@ class IAPListTile extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final status = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status;
|
||||
final isPending = status == IAPProductStatus.purchased || status == null;
|
||||
return ListTile(
|
||||
final isPurchased = IAPProducts.isPurchased(context, IAPProductType.paidFeatures);
|
||||
return Opacity(
|
||||
opacity: isPurchased ? Dimens.enabledOpacity : Dimens.disabledOpacity,
|
||||
child: ListTile(
|
||||
leading: leading,
|
||||
title: title,
|
||||
onTap: switch (status) {
|
||||
IAPProductStatus.purchasable => () => showBuyProDialog(context),
|
||||
IAPProductStatus.pending => null,
|
||||
IAPProductStatus.purchased => onTap,
|
||||
null => null,
|
||||
},
|
||||
trailing: showPendingTrailing && isPending
|
||||
? const SizedBox(
|
||||
height: Dimens.grid24,
|
||||
width: Dimens.grid24,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: null,
|
||||
onTap: isPurchased ? onTap : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/feature.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/remote_config_provider.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
|
||||
Future<void> showBuyProDialog(BuildContext context) {
|
||||
final unlockFeaturesEnabled = RemoteConfig.isEnabled(context, Feature.unlockProFeaturesText);
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
icon: const Icon(Icons.star),
|
||||
titlePadding: Dimens.dialogIconTitlePadding,
|
||||
title: Text(S.of(context).lightmeterPro),
|
||||
title: Text(unlockFeaturesEnabled ? S.of(context).proFeatures : S.of(context).lightmeterPro),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL),
|
||||
content: SingleChildScrollView(child: Text(S.of(context).lightmeterProDescription)),
|
||||
content: SingleChildScrollView(
|
||||
child: Text(
|
||||
unlockFeaturesEnabled ? S.of(context).unlockProFeaturesDescription : S.of(context).lightmeterProDescription,
|
||||
),
|
||||
),
|
||||
actionsPadding: Dimens.dialogActionsPadding,
|
||||
actions: [
|
||||
TextButton(
|
||||
|
@ -23,7 +30,7 @@ Future<void> showBuyProDialog(BuildContext context) {
|
|||
Navigator.of(context).pop();
|
||||
IAPProductsProvider.of(context).buy(IAPProductType.paidFeatures);
|
||||
},
|
||||
child: Text(S.of(context).buy),
|
||||
child: Text(unlockFeaturesEnabled ? S.of(context).unlock : S.of(context).buy),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -13,8 +13,10 @@ dependencies:
|
|||
clipboard: 0.1.3
|
||||
dynamic_color: 1.6.6
|
||||
exif: 3.1.4
|
||||
firebase_core: 2.14.0
|
||||
firebase_crashlytics: 3.3.3
|
||||
firebase_analytics: 10.6.2
|
||||
firebase_core: 2.20.0
|
||||
firebase_crashlytics: 3.4.2
|
||||
firebase_remote_config: 4.3.2
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: 8.1.3
|
||||
|
@ -26,7 +28,7 @@ dependencies:
|
|||
m3_lightmeter_iap:
|
||||
git:
|
||||
url: "https://github.com/vodemn/m3_lightmeter_iap"
|
||||
ref: v0.5.0
|
||||
ref: v0.6.2
|
||||
m3_lightmeter_resources:
|
||||
git:
|
||||
url: "https://github.com/vodemn/m3_lightmeter_resources"
|
||||
|
|
104
test/providers/remote_config_provider_test.dart
Normal file
104
test/providers/remote_config_provider_test.dart
Normal file
|
@ -0,0 +1,104 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:lightmeter/data/models/feature.dart';
|
||||
import 'package:lightmeter/data/remote_config_service.dart';
|
||||
import 'package:lightmeter/providers/remote_config_provider.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class _MockRemoteConfigService extends Mock implements RemoteConfigService {}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
late _MockRemoteConfigService mockRemoteConfigService;
|
||||
|
||||
setUpAll(() {
|
||||
mockRemoteConfigService = _MockRemoteConfigService();
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
when(() => mockRemoteConfigService.getValue(Feature.unlockProFeaturesText)).thenReturn(false);
|
||||
when(() => mockRemoteConfigService.getAll()).thenReturn({Feature.unlockProFeaturesText: false});
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
reset(mockRemoteConfigService);
|
||||
});
|
||||
|
||||
Future<void> pumpTestWidget(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
RemoteConfigProvider(
|
||||
remoteConfigService: mockRemoteConfigService,
|
||||
child: const _Application(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'RemoteConfigProvider init',
|
||||
(tester) async {
|
||||
when(() => mockRemoteConfigService.onConfigUpdated()).thenAnswer((_) => const Stream.empty());
|
||||
|
||||
await pumpTestWidget(tester);
|
||||
expect(find.text('unlockProFeaturesText: false'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'RemoteConfigProvider updates stream',
|
||||
(tester) async {
|
||||
final StreamController<Set<Feature>> remoteConfigUpdateController = StreamController<Set<Feature>>();
|
||||
when(() => mockRemoteConfigService.onConfigUpdated()).thenAnswer((_) => remoteConfigUpdateController.stream);
|
||||
|
||||
await pumpTestWidget(tester);
|
||||
expect(find.text('unlockProFeaturesText: false'), findsOneWidget);
|
||||
|
||||
when(() => mockRemoteConfigService.getValue(Feature.unlockProFeaturesText)).thenReturn(true);
|
||||
remoteConfigUpdateController.add({Feature.unlockProFeaturesText});
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('unlockProFeaturesText: true'), findsOneWidget);
|
||||
|
||||
await remoteConfigUpdateController.close();
|
||||
},
|
||||
);
|
||||
|
||||
test('RemoteConfig.updateShouldNotifyDependent', () {
|
||||
const config = RemoteConfig(config: {Feature.unlockProFeaturesText: false}, child: SizedBox());
|
||||
expect(
|
||||
config.updateShouldNotifyDependent(config, {}),
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
config.updateShouldNotifyDependent(
|
||||
const RemoteConfig(config: {Feature.unlockProFeaturesText: false}, child: SizedBox()),
|
||||
{Feature.unlockProFeaturesText},
|
||||
),
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
config.updateShouldNotifyDependent(
|
||||
const RemoteConfig(config: {Feature.unlockProFeaturesText: true}, child: SizedBox()),
|
||||
{Feature.unlockProFeaturesText},
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class _Application extends StatelessWidget {
|
||||
const _Application();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: Text(
|
||||
"${Feature.unlockProFeaturesText.name}: ${RemoteConfig.isEnabled(context, Feature.unlockProFeaturesText)}",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue