Showing
9 changed files
with
98 additions
and
4 deletions
| @@ -5,6 +5,7 @@ import 'package:get/get.dart'; | @@ -5,6 +5,7 @@ import 'package:get/get.dart'; | ||
| 5 | import '../../core/config/app_environment_config.dart'; | 5 | import '../../core/config/app_environment_config.dart'; |
| 6 | import '../../core/error/app_error_handler.dart'; | 6 | import '../../core/error/app_error_handler.dart'; |
| 7 | import '../../core/network/api/config_api.dart'; | 7 | import '../../core/network/api/config_api.dart'; |
| 8 | +import '../../core/services/app_config_service.dart'; | ||
| 8 | import '../../core/network/api/health_api.dart'; | 9 | import '../../core/network/api/health_api.dart'; |
| 9 | import '../../core/network/api/interaction_api.dart'; | 10 | import '../../core/network/api/interaction_api.dart'; |
| 10 | import '../../core/network/api/obs_api.dart'; | 11 | import '../../core/network/api/obs_api.dart'; |
| @@ -89,9 +90,10 @@ void registerPayDeps(DioClient dioClient) { | @@ -89,9 +90,10 @@ void registerPayDeps(DioClient dioClient) { | ||
| 89 | Get.lazyPut(() => PayApi(dioClient), fenix: true); | 90 | Get.lazyPut(() => PayApi(dioClient), fenix: true); |
| 90 | } | 91 | } |
| 91 | 92 | ||
| 92 | -/// Remote config API. | 93 | +/// Remote config API + global AppConfig service. |
| 93 | void registerConfigDeps(DioClient dioClient) { | 94 | void registerConfigDeps(DioClient dioClient) { |
| 94 | Get.lazyPut(() => ConfigApi(dioClient), fenix: true); | 95 | Get.lazyPut(() => ConfigApi(dioClient), fenix: true); |
| 96 | + Get.put(AppConfigService(Get.find<ConfigApi>()), permanent: true); | ||
| 95 | } | 97 | } |
| 96 | 98 | ||
| 97 | /// Watch theme API. | 99 | /// Watch theme API. |
| 1 | +import 'dart:async'; | ||
| 2 | + | ||
| 1 | import 'package:flutter/widgets.dart'; | 3 | import 'package:flutter/widgets.dart'; |
| 2 | import 'package:get/get.dart'; | 4 | import 'package:get/get.dart'; |
| 3 | 5 | ||
| @@ -5,6 +7,7 @@ import '../../core/config/app_environment_config.dart'; | @@ -5,6 +7,7 @@ import '../../core/config/app_environment_config.dart'; | ||
| 5 | import '../../core/logging/app_logger.dart'; | 7 | import '../../core/logging/app_logger.dart'; |
| 6 | import '../../core/network/user_agent_provider.dart'; | 8 | import '../../core/network/user_agent_provider.dart'; |
| 7 | import '../../core/services/user_state_service.dart'; | 9 | import '../../core/services/user_state_service.dart'; |
| 10 | +import '../../core/services/app_config_service.dart'; | ||
| 8 | import '../../data/local/local_storage.dart'; | 11 | import '../../data/local/local_storage.dart'; |
| 9 | import '../../data/local/user_account_storage.dart'; | 12 | import '../../data/local/user_account_storage.dart'; |
| 10 | import '../../data/local/user_preferences_storage.dart'; | 13 | import '../../data/local/user_preferences_storage.dart'; |
| @@ -41,6 +44,7 @@ abstract final class AppBootstrap { | @@ -41,6 +44,7 @@ abstract final class AppBootstrap { | ||
| 41 | 44 | ||
| 42 | if (local.termsAgreed) { | 45 | if (local.termsAgreed) { |
| 43 | await Get.find<UserStateService>().initializeSdks(); | 46 | await Get.find<UserStateService>().initializeSdks(); |
| 47 | + unawaited(AppConfigService.to.fetch()); | ||
| 44 | } | 48 | } |
| 45 | } | 49 | } |
| 46 | } | 50 | } |
| @@ -4,6 +4,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/today/hrv_measuremen | @@ -4,6 +4,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/today/hrv_measuremen | ||
| 4 | import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart'; | 4 | import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart'; |
| 5 | import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_bottom_slogan.dart'; | 5 | import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_bottom_slogan.dart'; |
| 6 | import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart'; | 6 | import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart'; |
| 7 | +import 'package:doublefeel_flutter/core/services/app_config_service.dart'; | ||
| 7 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; | 8 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; |
| 8 | 9 | ||
| 9 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 10 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| @@ -214,7 +215,8 @@ class TodayTabBody extends StatelessWidget { | @@ -214,7 +215,8 @@ class TodayTabBody extends StatelessWidget { | ||
| 214 | : const SizedBox.shrink(), | 215 | : const SizedBox.shrink(), |
| 215 | ), | 216 | ), |
| 216 | Obx(() { | 217 | Obx(() { |
| 217 | - return Get.find<UserStateService>().isVip | 218 | + return Get.find<UserStateService>().isVip || |
| 219 | + !AppConfigService.to.config.showYearVipDiscount | ||
| 218 | ? const SizedBox.shrink() | 220 | ? const SizedBox.shrink() |
| 219 | : GestureDetector( | 221 | : GestureDetector( |
| 220 | onTap: () { | 222 | onTap: () { |
| @@ -744,7 +746,8 @@ class _TopDateBar extends StatelessWidget { | @@ -744,7 +746,8 @@ class _TopDateBar extends StatelessWidget { | ||
| 744 | ], | 746 | ], |
| 745 | const Spacer(), | 747 | const Spacer(), |
| 746 | Obx(() { | 748 | Obx(() { |
| 747 | - return Get.find<UserStateService>().isVip | 749 | + return Get.find<UserStateService>().isVip || |
| 750 | + !AppConfigService.to.config.showYearVipDiscount | ||
| 748 | ? const SizedBox.shrink() | 751 | ? const SizedBox.shrink() |
| 749 | : GestureDetector( | 752 | : GestureDetector( |
| 750 | onTap: () { | 753 | onTap: () { |
| @@ -3,6 +3,8 @@ import 'dart:convert'; | @@ -3,6 +3,8 @@ import 'dart:convert'; | ||
| 3 | import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; | 3 | import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; |
| 4 | import 'package:doublefeel_flutter/core/constants/app_const.dart'; | 4 | import 'package:doublefeel_flutter/core/constants/app_const.dart'; |
| 5 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | 5 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; |
| 6 | +import 'package:doublefeel_flutter/core/network/api/config_api.dart'; | ||
| 7 | +import 'package:doublefeel_flutter/core/services/app_config_service.dart'; | ||
| 6 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; | 8 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; |
| 7 | import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; | 9 | import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; |
| 8 | import 'package:doublefeel_flutter/l10n/gen/app_localizations.dart'; | 10 | import 'package:doublefeel_flutter/l10n/gen/app_localizations.dart'; |
| @@ -81,6 +83,7 @@ class LoginController extends GetxController { | @@ -81,6 +83,7 @@ class LoginController extends GetxController { | ||
| 81 | ta.track('enter_doublefeel_welcome_page'); | 83 | ta.track('enter_doublefeel_welcome_page'); |
| 82 | needTrackWelcomepage = false; | 84 | needTrackWelcomepage = false; |
| 83 | } | 85 | } |
| 86 | + unawaited(AppConfigService.to.fetch()); | ||
| 84 | } catch (e) { | 87 | } catch (e) { |
| 85 | AppLogger.e('initializeSdks error: $e'); | 88 | AppLogger.e('initializeSdks error: $e'); |
| 86 | } | 89 | } |
| @@ -6,6 +6,7 @@ import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; | @@ -6,6 +6,7 @@ import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; | ||
| 6 | import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; | 6 | import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; |
| 7 | import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; | 7 | import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; |
| 8 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 8 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 9 | +import 'package:doublefeel_flutter/core/services/app_config_service.dart'; | ||
| 9 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; | 10 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; |
| 10 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; | 11 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; |
| 11 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 12 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| @@ -91,7 +92,8 @@ class PurchaseController extends GetxController { | @@ -91,7 +92,8 @@ class PurchaseController extends GetxController { | ||
| 91 | 92 | ||
| 92 | void onBackPressed() { | 93 | void onBackPressed() { |
| 93 | if (isFromOnboard) { | 94 | if (isFromOnboard) { |
| 94 | - if (Get.arguments?['is_membership_offer'] == true) { | 95 | + if (Get.arguments?['is_membership_offer'] == true || |
| 96 | + !AppConfigService.to.config.showYearVipDiscount) { | ||
| 95 | Get.offAllNamed(AppRoutes.home, arguments: {'isFromOnboard': true}); | 97 | Get.offAllNamed(AppRoutes.home, arguments: {'isFromOnboard': true}); |
| 96 | } else { | 98 | } else { |
| 97 | Get.offNamed( | 99 | Get.offNamed( |
| @@ -22,4 +22,18 @@ class ConfigApi { | @@ -22,4 +22,18 @@ class ConfigApi { | ||
| 22 | }, | 22 | }, |
| 23 | ); | 23 | ); |
| 24 | } | 24 | } |
| 25 | + | ||
| 26 | + Future<AppResult<AppConfig>> getAppConfigList() { | ||
| 27 | + return safeCall( | ||
| 28 | + call: () async { | ||
| 29 | + final response = await _dioClient.dio.get( | ||
| 30 | + ApiPaths.appConfig, | ||
| 31 | + options: _dioClient.noTokenOptions(), | ||
| 32 | + ); | ||
| 33 | + return AppConfig.fromJson( | ||
| 34 | + response.data as Map<String, dynamic>, | ||
| 35 | + ); | ||
| 36 | + }, | ||
| 37 | + ); | ||
| 38 | + } | ||
| 25 | } | 39 | } |
| @@ -58,6 +58,7 @@ abstract final class ApiPaths { | @@ -58,6 +58,7 @@ abstract final class ApiPaths { | ||
| 58 | 58 | ||
| 59 | // Config | 59 | // Config |
| 60 | static const dynamicConfigList = '/client/doublefeel/dynamic_config/list/'; | 60 | static const dynamicConfigList = '/client/doublefeel/dynamic_config/list/'; |
| 61 | + static const appConfig = '/client/doublefeel/app_config/'; | ||
| 61 | static const contentCheckText = '/client/doublefeel/content_check/text/'; | 62 | static const contentCheckText = '/client/doublefeel/content_check/text/'; |
| 62 | 63 | ||
| 63 | // OBS | 64 | // OBS |
lib/core/services/app_config_service.dart
0 → 100644
| 1 | +import 'package:doublefeel_flutter/core/result/app_result.dart'; | ||
| 2 | +import 'package:get/get.dart'; | ||
| 3 | + | ||
| 4 | +import '../logging/app_logger.dart'; | ||
| 5 | +import '../network/api/config_api.dart'; | ||
| 6 | +import '../../data/models/config/config_models.dart'; | ||
| 7 | + | ||
| 8 | +/// Globally accessible service that fetches and caches [AppConfig] from the | ||
| 9 | +/// server. Registered with [permanent: true] so GetX never disposes it. | ||
| 10 | +/// | ||
| 11 | +/// Usage (anywhere in the app): | ||
| 12 | +/// ```dart | ||
| 13 | +/// // One-off read | ||
| 14 | +/// final show = AppConfigService.to.config.showYearVipDiscount; | ||
| 15 | +/// | ||
| 16 | +/// // Reactive (inside Obx / ever) | ||
| 17 | +/// Obx(() => AppConfigService.to.config.showYearVipDiscount ? ... : ...) | ||
| 18 | +/// ``` | ||
| 19 | +class AppConfigService extends GetxService { | ||
| 20 | + AppConfigService(this._configApi); | ||
| 21 | + | ||
| 22 | + static AppConfigService get to => Get.find(); | ||
| 23 | + | ||
| 24 | + final ConfigApi _configApi; | ||
| 25 | + final Rx<AppConfig> _config = const AppConfig().obs; | ||
| 26 | + | ||
| 27 | + /// Latest remote [AppConfig]. Defaults to [AppConfig()] until [fetch] succeeds. | ||
| 28 | + AppConfig get config => _config.value; | ||
| 29 | + | ||
| 30 | + /// Reactive [AppConfig] stream — use inside `Obx` / `ever`. | ||
| 31 | + Rx<AppConfig> get configRx => _config; | ||
| 32 | + | ||
| 33 | + /// Fetches [AppConfig] from the server and updates [config]. | ||
| 34 | + /// Safe to call multiple times; failures are logged and ignored. | ||
| 35 | + Future<void> fetch() async { | ||
| 36 | + final res = await _configApi.getAppConfigList(); | ||
| 37 | + switch (res) { | ||
| 38 | + case AppSuccess(:final data): | ||
| 39 | + { | ||
| 40 | + _config.value = data; | ||
| 41 | + AppLogger.i('AppConfigService: config fetched — $data'); | ||
| 42 | + } | ||
| 43 | + break; | ||
| 44 | + case AppFailure(): | ||
| 45 | + { | ||
| 46 | + AppLogger.w('AppConfigService: fetch failed', res.error); | ||
| 47 | + } | ||
| 48 | + break; | ||
| 49 | + } | ||
| 50 | + } | ||
| 51 | +} |
| @@ -51,3 +51,17 @@ class ConfigData { | @@ -51,3 +51,17 @@ class ConfigData { | ||
| 51 | return val; | 51 | return val; |
| 52 | } | 52 | } |
| 53 | } | 53 | } |
| 54 | + | ||
| 55 | +class AppConfig { | ||
| 56 | + const AppConfig({ | ||
| 57 | + this.showYearVipDiscount = true, | ||
| 58 | + }); | ||
| 59 | + | ||
| 60 | + final bool showYearVipDiscount; | ||
| 61 | + | ||
| 62 | + factory AppConfig.fromJson(Map<String, dynamic> json) { | ||
| 63 | + return AppConfig( | ||
| 64 | + showYearVipDiscount: json['show_year_vip_discount'] as bool, | ||
| 65 | + ); | ||
| 66 | + } | ||
| 67 | +} |
-
Please register or login to post a comment