From 33425f440624e3d512ffafe1ad4902db4f9ae221 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Wed, 14 May 2025 15:46:44 -0400 Subject: [PATCH] feat: allow users on staging to switch their environment (#2799) --- .env | 1 + .github/workflows/main_deploy.yaml | 2 + .github/workflows/manual.yml | 2 + .gitignore | 1 + assets/.env | 1 + assets/l10n/intl_en.arb | 4 +- lib/config/app_config.dart | 10 +- lib/main.dart | 2 +- lib/pages/settings/settings_view.dart | 2 +- .../get_analytics_controller.dart | 11 +- lib/pangea/analytics_misc/level_up.dart | 4 +- .../put_analytics_controller.dart | 13 +- lib/pangea/bot/utils/bot_name.dart | 2 +- .../chat/constants/default_power_level.dart | 1 - lib/pangea/common/config/environment.dart | 235 +++++++++++++++--- lib/pangea/common/constants/local.key.dart | 1 + .../common/controllers/pangea_controller.dart | 38 ++- lib/pangea/common/network/urls.dart | 6 +- lib/pangea/common/utils/error_handler.dart | 2 +- .../login/pages/login_or_signup_view.dart | 44 +++- .../login/pages/pangea_login_scaffold.dart | 3 + .../login/widgets/app_config_dialog.dart | 95 +++++++ .../controllers/subscription_controller.dart | 33 ++- .../models/base_subscription_info.dart | 37 +-- 24 files changed, 451 insertions(+), 99 deletions(-) create mode 100644 lib/pangea/login/widgets/app_config_dialog.dart diff --git a/.env b/.env index 1d4a8027e..38131931d 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ +ENVIRONMENT = 'staging' CHOREO_API = 'https://api.staging.pangea.chat' FRONTEND_URL='https://app.staging.pangea.chat' SYNAPSE_URL = 'matrix.staging.pangea.chat' diff --git a/.github/workflows/main_deploy.yaml b/.github/workflows/main_deploy.yaml index 10e2ac0ac..32d04359b 100644 --- a/.github/workflows/main_deploy.yaml +++ b/.github/workflows/main_deploy.yaml @@ -47,6 +47,8 @@ jobs: touch public/.env echo "$WEB_APP_ENV" >> public/.env cp public/.env public/assets/.env + touch public/assets/envs.json + echo "$ENV_OVERRIDES" >> public/assets/envs.json - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 with: diff --git a/.github/workflows/manual.yml b/.github/workflows/manual.yml index 3ced2468a..6388f70ae 100644 --- a/.github/workflows/manual.yml +++ b/.github/workflows/manual.yml @@ -45,6 +45,8 @@ jobs: rm assets/.env echo "$WEB_APP_ENV" >> .env cp .env assets/.env + touch assets/envs.json + echo "$ENV_OVERRIDES" >> assets/envs.json - name: Apply .env patch run: git apply ./scripts/enable_mobile_env.patch - name: Install Fastlane diff --git a/.gitignore b/.gitignore index fb99f07cd..d7d15a4f5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ keys.json !/public/.env *.env.local_choreo *.env.prod +envs.json # libolm package /assets/js/package diff --git a/assets/.env b/assets/.env index 10f5f5f94..16a64f6e4 100644 --- a/assets/.env +++ b/assets/.env @@ -1,3 +1,4 @@ +ENVIRONMENT = 'staging' CHOREO_API = 'https://api.staging.pangea.chat' FRONTEND_URL = 'https://app.staging.pangea.chat' diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index f1ef95961..9eaee21fc 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4934,5 +4934,7 @@ "joinSpaceOnboardingDesc": "Do you have an invite code or link to a learning community?", "skipForNow": "Skip for now", "permissions": "Permissions", - "spaceChildPermission": "Who can add new chats and subspaces to this space" + "spaceChildPermission": "Who can add new chats and subspaces to this space", + "addEnvironmentOverride": "Add environment override", + "defaultOption": "Default" } diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 96724b524..0fbd529c2 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -15,7 +15,7 @@ abstract class AppConfig { static String? get applicationWelcomeMessage => _applicationWelcomeMessage; // #Pangea // static String _defaultHomeserver = 'matrix.org'; - static String _defaultHomeserver = Environment.synapseURL; + static String get _defaultHomeserver => Environment.synapseURL; // #Pangea static String get defaultHomeserver => _defaultHomeserver; static double fontSizeFactor = 1; @@ -206,9 +206,11 @@ abstract class AppConfig { if (json['application_welcome_message'] is String) { _applicationWelcomeMessage = json['application_welcome_message']; } - if (json['default_homeserver'] is String) { - _defaultHomeserver = json['default_homeserver']; - } + // #Pangea + // if (json['default_homeserver'] is String) { + // _defaultHomeserver = json['default_homeserver']; + // } + // Pangea# if (json['privacy_url'] is String) { _privacyUrl = json['privacy_url']; } diff --git a/lib/main.dart b/lib/main.dart index 41ba41998..785ea5edf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -110,7 +110,7 @@ Future startGui(List clients, SharedPreferences store) async { // staging or vice versa, logout. if (firstClient?.userID?.domain != null) { final isStagingUser = firstClient!.userID!.domain!.contains("staging"); - final isStagingServer = Environment.isStaging; + final isStagingServer = Environment.synapseURL.contains("staging"); if (isStagingServer != isStagingUser) { await firstClient.logout(); } diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index 96e846b9e..a912fd401 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -330,7 +330,7 @@ class SettingsView extends StatelessWidget { }, ), // Conditional ListTile based on the environment (staging or not) - if (Environment.isStaging) + if (Environment.isStagingEnvironment) ListTile( leading: const Icon(Icons.bug_report_outlined), title: Text(L10n.of(context).connectedToStaging), diff --git a/lib/pangea/analytics_misc/get_analytics_controller.dart b/lib/pangea/analytics_misc/get_analytics_controller.dart index 7ad571690..dcdcafd65 100644 --- a/lib/pangea/analytics_misc/get_analytics_controller.dart +++ b/lib/pangea/analytics_misc/get_analytics_controller.dart @@ -25,7 +25,7 @@ import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.da /// A minimized version of AnalyticsController that get the logged in user's analytics class GetAnalyticsController extends BaseController { - final GetStorage analyticsBox = GetStorage("analytics_storage"); + static final GetStorage analyticsBox = GetStorage("analytics_storage"); late PangeaController _pangeaController; late PracticeSelectionRepo perMessage; @@ -276,6 +276,15 @@ class GetAnalyticsController extends BaseController { } } + Future clearMessagesCache() async => + analyticsBox.remove(PLocalKey.messagesSinceUpdate); + + Future setMessagesCache(Map cacheValue) async => + analyticsBox.write( + PLocalKey.messagesSinceUpdate, + cacheValue, + ); + /// A flat list of all locally cached construct uses List get _locallyCachedConstructs => messagesSinceUpdate.values.expand((e) => e).toList(); diff --git a/lib/pangea/analytics_misc/level_up.dart b/lib/pangea/analytics_misc/level_up.dart index 4d6c10cb4..3325089e4 100644 --- a/lib/pangea/analytics_misc/level_up.dart +++ b/lib/pangea/analytics_misc/level_up.dart @@ -174,7 +174,7 @@ class LevelUpBannerState extends State } Future _toggleDetails() async { - if (!Environment.isStaging) return; + if (!Environment.isStagingEnvironment) return; if (mounted) { setState(() { @@ -282,7 +282,7 @@ class LevelUpBannerState extends State ), Row( children: [ - if (Environment.isStaging) + if (Environment.isStagingEnvironment) AnimatedSize( duration: FluffyThemes.animationDuration, child: _error == null diff --git a/lib/pangea/analytics_misc/put_analytics_controller.dart b/lib/pangea/analytics_misc/put_analytics_controller.dart index ae17b567c..4197ff7d7 100644 --- a/lib/pangea/analytics_misc/put_analytics_controller.dart +++ b/lib/pangea/analytics_misc/put_analytics_controller.dart @@ -8,7 +8,6 @@ import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; -import 'package:fluffychat/pangea/common/constants/local.key.dart'; import 'package:fluffychat/pangea/common/controllers/base_controller.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; @@ -319,16 +318,14 @@ class PutAnalyticsController extends BaseController { /// Clears the local cache of recently sent constructs. Called before updating analytics void clearMessagesSinceUpdate({clearDrafts = false}) { if (clearDrafts) { - MatrixState.pangeaController.getAnalytics.analyticsBox - .remove(PLocalKey.messagesSinceUpdate); + MatrixState.pangeaController.getAnalytics.clearMessagesCache(); return; } final localCache = _pangeaController.getAnalytics.messagesSinceUpdate; final draftKeys = localCache.keys.where((key) => key.startsWith('draft')); if (draftKeys.isEmpty) { - MatrixState.pangeaController.getAnalytics.analyticsBox - .remove(PLocalKey.messagesSinceUpdate); + MatrixState.pangeaController.getAnalytics.clearMessagesCache(); return; } @@ -348,10 +345,8 @@ class PutAnalyticsController extends BaseController { final constructJsons = entry.value.map((e) => e.toJson()).toList(); formattedCache[entry.key] = constructJsons; } - await MatrixState.pangeaController.getAnalytics.analyticsBox.write( - PLocalKey.messagesSinceUpdate, - formattedCache, - ); + await MatrixState.pangeaController.getAnalytics + .setMessagesCache(formattedCache); } /// Prevent concurrent updates to analytics diff --git a/lib/pangea/bot/utils/bot_name.dart b/lib/pangea/bot/utils/bot_name.dart index c183be359..2596766cc 100644 --- a/lib/pangea/bot/utils/bot_name.dart +++ b/lib/pangea/bot/utils/bot_name.dart @@ -3,7 +3,7 @@ import 'package:fluffychat/pangea/common/config/environment.dart'; class BotName { static String get byEnvironment => Environment.botName != null ? Environment.botName! - : Environment.isStaging + : Environment.isStagingEnvironment ? "@bot:staging.pangea.chat" : "@bot:pangea.chat"; static String get localBot => "@matrix-bot-test:staging.pangea.chat"; diff --git a/lib/pangea/chat/constants/default_power_level.dart b/lib/pangea/chat/constants/default_power_level.dart index 7d1ae2607..c200720f8 100644 --- a/lib/pangea/chat/constants/default_power_level.dart +++ b/lib/pangea/chat/constants/default_power_level.dart @@ -22,7 +22,6 @@ Map defaultPowerLevels(String userID) => { "m.room.tombstone": 100, }, "users": { - "@bot:staging.pangea.chat": 50, userID: 100, }, }; diff --git a/lib/pangea/common/config/environment.dart b/lib/pangea/common/config/environment.dart index 440eb7fe4..c0fbe8a44 100644 --- a/lib/pangea/common/config/environment.dart +++ b/lib/pangea/common/config/environment.dart @@ -1,25 +1,35 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; + import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:get_storage/get_storage.dart'; + +import 'package:fluffychat/pangea/common/constants/local.key.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; class Environment { static bool get itIsTime => DateTime.utc(2023, 1, 25).isBefore(DateTime.now()); - static String get fileName { - return ".env"; - } - - static bool get isStaging => synapseURL.contains("staging"); + static bool get isStagingEnvironment => + dotenv.env["ENVIRONMENT"] == "staging"; static String get frontendURL { - return dotenv.env["FRONTEND_URL"] ?? "Frontend URL NOT FOUND"; + return appConfigOverride?.frontendURL ?? + dotenv.env["FRONTEND_URL"] ?? + "Frontend URL NOT FOUND"; } static String get synapseURL { - return dotenv.env['SYNAPSE_URL'] ?? 'Synapse Url not found'; + return appConfigOverride?.synapseURL ?? + dotenv.env['SYNAPSE_URL'] ?? + 'Synapse Url not found'; } static String get homeServer { - String? homeServerFromSynapseURL = dotenv.env['SYNAPSE_URL']; + String? homeServerFromSynapseURL = + appConfigOverride?.synapseURL ?? dotenv.env['SYNAPSE_URL']; if (homeServerFromSynapseURL != null) { if (homeServerFromSynapseURL.startsWith("http://")) { homeServerFromSynapseURL = @@ -34,13 +44,14 @@ class Environment { homeServerFromSynapseURL.replaceFirst("matrix.", ""); } } - return dotenv.env["HOME_SERVER"] ?? + return appConfigOverride?.homeServer ?? + dotenv.env["HOME_SERVER"] ?? homeServerFromSynapseURL ?? 'Home Server not found'; } static String get choreoApi { - final envEntry = dotenv.env['CHOREO_API']; + final envEntry = appConfigOverride?.choreoApi ?? dotenv.env['CHOREO_API']; if (envEntry == null) { return "Not found"; } @@ -54,48 +65,214 @@ class Environment { } static String get choreoApiKey { - return dotenv.env['CHOREO_API_KEY'] ?? + return appConfigOverride?.choreoApiKey ?? + dotenv.env['CHOREO_API_KEY'] ?? 'e6fa9fa97031ba0c852efe78457922f278a2fbc109752fe18e465337699e9873'; } static String get sentryDsn { - return dotenv.env["SENTRY_DSN"] ?? + return appConfigOverride?.sentryDsn ?? + dotenv.env["SENTRY_DSN"] ?? 'https://c2fd19ab2cdc4ebb939a32d01c0e9fa1@o225078.ingest.sentry.io/1376295'; } static String get rcGoogleKey { - return dotenv.env["RC_GOOGLE_KEY"] ?? 'goog_paQMrzFKGzuWZvcMTPkkvIsifJe'; + return appConfigOverride?.rcGoogleKey ?? + dotenv.env["RC_GOOGLE_KEY"] ?? + 'goog_paQMrzFKGzuWZvcMTPkkvIsifJe'; } static String get rcIosKey { - return dotenv.env["RC_IOS_KEY"] ?? 'appl_DUPqnxuLjkBLzhBPTWeDjqNENuv'; - } - - // This is a public key - static String get rcStripeKey { - return dotenv.env["RC_STRIPE_KEY"] ?? 'strp_YWZxWUeEfvagiefDNoofinaRCOl'; + return appConfigOverride?.rcIosKey ?? + dotenv.env["RC_IOS_KEY"] ?? + 'appl_DUPqnxuLjkBLzhBPTWeDjqNENuv'; } static String get rcOfferingName { - return dotenv.env["RC_OFFERING_NAME"] ?? 'default'; + return appConfigOverride?.rcOfferingName ?? + dotenv.env["RC_OFFERING_NAME"] ?? + 'default'; } static String get stripeManagementUrl { - return dotenv.env["STRIPE_MANAGEMENT_LINK"] ?? + return appConfigOverride?.stripeManagementUrl ?? + dotenv.env["STRIPE_MANAGEMENT_LINK"] ?? 'https://billing.stripe.com/p/login/dR6dSkf5p6rBc4EcMM'; } - static String get supportSpaceId { - return isStaging - ? '!gqSNSkvwTpgumyjLsV:staging.pangea.chat' - : '!MvJoWwKJErvFuTYOdq:pangea.chat'; - } - static String get supportUserId { - return isStaging ? '@support:staging.pangea.chat' : '@support:pangea.chat'; + return synapseURL.contains('staging') + ? '@support:staging.pangea.chat' + : '@support:pangea.chat'; } static String? get botName { - return dotenv.env["BOT_NAME"]; + return appConfigOverride?.botName ?? dotenv.env["BOT_NAME"]; + } + + static final GetStorage appConfigurationStorage = GetStorage('env_override'); + + static Future> getAppConfigOverrides() async { + if (!isStagingEnvironment) { + return []; + } + + List data = []; + try { + final String jsonString = await rootBundle.loadString('assets/envs.json'); + data = jsonDecode(jsonString); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: {}, + ); + return []; + } + + final List overrides = []; + for (final entry in data) { + if (entry is! Map) { + ErrorHandler.logError( + e: Exception("Invalid entry in envs.json"), + s: StackTrace.current, + data: entry, + ); + continue; + } + + try { + final override = AppConfigOverride.fromJson(entry); + overrides.add(override); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: entry, + ); + continue; + } + } + return overrides; + } + + static AppConfigOverride? get appConfigOverride { + final entry = appConfigurationStorage.read(PLocalKey.appConfigOverride); + if (entry == null) return null; + try { + return AppConfigOverride.fromJson(entry); + } catch (e) { + ErrorHandler.logError( + e: e, + s: StackTrace.current, + data: entry, + ); + return null; + } + } + + static Future setAppConfigOverride(AppConfigOverride? override) async { + appConfigurationStorage.write( + PLocalKey.appConfigOverride, + override?.toJson(), + ); + } +} + +class AppConfigOverride { + final String? environment; + final String? frontendURL; + final String? synapseURL; + final String? homeServer; + final String? choreoApi; + final String? choreoApiKey; + final String? sentryDsn; + final String? rcGoogleKey; + final String? rcIosKey; + final String? rcOfferingName; + final String? stripeManagementUrl; + final String? botName; + + const AppConfigOverride({ + this.environment, + this.frontendURL, + this.synapseURL, + this.homeServer, + this.choreoApi, + this.choreoApiKey, + this.sentryDsn, + this.rcGoogleKey, + this.rcIosKey, + this.rcOfferingName, + this.stripeManagementUrl, + this.botName, + }); + + static AppConfigOverride fromJson(Map json) { + return AppConfigOverride( + environment: json['environment'] as String?, + frontendURL: json['frontendURL'] as String?, + synapseURL: json['synapseURL'] as String?, + homeServer: json['homeServer'] as String?, + choreoApi: json['choreoApi'] as String?, + choreoApiKey: json['choreoApiKey'] as String?, + sentryDsn: json['sentryDsn'] as String?, + rcGoogleKey: json['rcGoogleKey'] as String?, + rcIosKey: json['rcIosKey'] as String?, + rcOfferingName: json['rcOfferingName'] as String?, + stripeManagementUrl: json['stripeManagementUrl'] as String?, + botName: json['botName'] as String?, + ); + } + + Map toJson() { + return { + 'environment': environment, + 'frontendURL': frontendURL, + 'synapseURL': synapseURL, + 'homeServer': homeServer, + 'choreoApi': choreoApi, + 'choreoApiKey': choreoApiKey, + 'sentryDsn': sentryDsn, + 'rcGoogleKey': rcGoogleKey, + 'rcIosKey': rcIosKey, + 'rcOfferingName': rcOfferingName, + 'stripeManagementUrl': stripeManagementUrl, + 'botName': botName, + }; + } + + @override + int get hashCode { + return environment.hashCode ^ + frontendURL.hashCode ^ + synapseURL.hashCode ^ + homeServer.hashCode ^ + choreoApi.hashCode ^ + choreoApiKey.hashCode ^ + sentryDsn.hashCode ^ + rcGoogleKey.hashCode ^ + rcIosKey.hashCode ^ + rcOfferingName.hashCode ^ + stripeManagementUrl.hashCode ^ + botName.hashCode; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! AppConfigOverride) return false; + return environment == other.environment && + frontendURL == other.frontendURL && + synapseURL == other.synapseURL && + homeServer == other.homeServer && + choreoApi == other.choreoApi && + choreoApiKey == other.choreoApiKey && + sentryDsn == other.sentryDsn && + rcGoogleKey == other.rcGoogleKey && + rcIosKey == other.rcIosKey && + rcOfferingName == other.rcOfferingName && + stripeManagementUrl == other.stripeManagementUrl && + botName == other.botName; } } diff --git a/lib/pangea/common/constants/local.key.dart b/lib/pangea/common/constants/local.key.dart index 532004687..d905790b0 100644 --- a/lib/pangea/common/constants/local.key.dart +++ b/lib/pangea/common/constants/local.key.dart @@ -10,4 +10,5 @@ class PLocalKey { static const String justInputtedCode = 'justInputtedCode'; static const String availableSubscriptionInfo = 'availableSubscriptionInfo'; static const String showedUpdateDialog = 'showedUpdateDialog'; + static const String appConfigOverride = 'appConfigOverride'; } diff --git a/lib/pangea/common/controllers/pangea_controller.dart b/lib/pangea/common/controllers/pangea_controller.dart index cdff484cb..f150d49b5 100644 --- a/lib/pangea/common/controllers/pangea_controller.dart +++ b/lib/pangea/common/controllers/pangea_controller.dart @@ -107,17 +107,35 @@ class PangeaController { _logOutfromPangea() { debugPrint("Pangea logout"); GoogleAnalytics.logout(); - _clearCachedData(); + clearCache(); } - void _clearCachedData() { - GetStorage('mode_list_storage').erase(); - GetStorage('activity_plan_storage').erase(); - GetStorage('bookmarked_activities').erase(); - GetStorage('objective_list_storage').erase(); - GetStorage('topic_list_storage').erase(); - GetStorage('lemma_storage').erase(); - GetStorage().erase(); + static final List _storageKeys = [ + 'mode_list_storage', + 'activity_plan_storage', + 'bookmarked_activities', + 'objective_list_storage', + 'topic_list_storage', + 'activity_plan_search_storage', + "analytics_storage", + "version_storage", + 'lemma_storage', + 'svg_cache', + 'morphs_storage', + 'morph_meaning_storage', + 'practice_record_cache', + 'practice_selection_cache', + 'class_storage', + 'subscription_storage', + 'vocab_storage', + ]; + + Future clearCache() async { + final List> futures = []; + for (final key in _storageKeys) { + futures.add(GetStorage(key).erase()); + } + await Future.wait(futures); } Future checkHomeServerAction() async { @@ -339,7 +357,7 @@ class PangeaController { _languageStream ??= userController.stateStream.listen((update) { if (update is Map && update['prev_target_lang'] != null) { - _clearCachedData(); + clearCache(); } }); } diff --git a/lib/pangea/common/network/urls.dart b/lib/pangea/common/network/urls.dart index 54a052d52..a667066c8 100644 --- a/lib/pangea/common/network/urls.dart +++ b/lib/pangea/common/network/urls.dart @@ -13,11 +13,11 @@ class PApiUrls { static String subscriptionPrefix = "/subscription"; static String accountPrefix = "/account"; - static String choreoEndpoint = + static String get choreoEndpoint => "${Environment.choreoApi}${PApiUrls.choreoPrefix}"; - static String subscriptionEndpoint = + static String get subscriptionEndpoint => "${Environment.choreoApi}${PApiUrls.subscriptionPrefix}"; - static String accountEndpoint = + static String get accountEndpoint => "${Environment.choreoApi}${PApiUrls.accountPrefix}"; /// ---------------------- Util -------------------------------------- diff --git a/lib/pangea/common/utils/error_handler.dart b/lib/pangea/common/utils/error_handler.dart index 325f0d0af..5900e81af 100644 --- a/lib/pangea/common/utils/error_handler.dart +++ b/lib/pangea/common/utils/error_handler.dart @@ -29,7 +29,7 @@ class ErrorHandler { options.debug = kDebugMode; options.environment = kDebugMode ? "debug" - : Environment.isStaging + : Environment.isStagingEnvironment ? "staging" : "productionC"; }, diff --git a/lib/pangea/login/pages/login_or_signup_view.dart b/lib/pangea/login/pages/login_or_signup_view.dart index 62274d3c1..77ea897ad 100644 --- a/lib/pangea/login/pages/login_or_signup_view.dart +++ b/lib/pangea/login/pages/login_or_signup_view.dart @@ -3,15 +3,57 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; +import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/login/pages/pangea_login_scaffold.dart'; +import 'package:fluffychat/pangea/login/widgets/app_config_dialog.dart'; import 'package:fluffychat/pangea/login/widgets/full_width_button.dart'; -class LoginOrSignupView extends StatelessWidget { +class LoginOrSignupView extends StatefulWidget { const LoginOrSignupView({super.key}); + @override + State createState() => LoginOrSignupViewState(); +} + +class LoginOrSignupViewState extends State { + List _overrides = []; + + @override + void initState() { + super.initState(); + _loadOverrides(); + } + + Future _loadOverrides() async { + final overrides = await Environment.getAppConfigOverrides(); + if (mounted) { + setState(() => _overrides = overrides); + } + } + + Future _setEnvironment() async { + if (_overrides.isEmpty) return; + + final resp = await showDialog( + context: context, + builder: (context) => AppConfigDialog(overrides: _overrides), + ); + + await Environment.setAppConfigOverride(resp); + setState(() {}); + } + @override Widget build(BuildContext context) { return PangeaLoginScaffold( + actions: Environment.isStagingEnvironment && _overrides.isNotEmpty + ? [ + IconButton( + icon: const Icon(Icons.settings_outlined), + onPressed: _setEnvironment, + ), + ] + : null, children: [ FullWidthButton( title: L10n.of(context).createAnAccount, diff --git a/lib/pangea/login/pages/pangea_login_scaffold.dart b/lib/pangea/login/pages/pangea_login_scaffold.dart index ee43ae212..af1fc603a 100644 --- a/lib/pangea/login/pages/pangea_login_scaffold.dart +++ b/lib/pangea/login/pages/pangea_login_scaffold.dart @@ -13,6 +13,7 @@ class PangeaLoginScaffold extends StatelessWidget { final List children; final bool showAppName; final AppBar? customAppBar; + final List? actions; const PangeaLoginScaffold({ required this.children, @@ -21,6 +22,7 @@ class PangeaLoginScaffold extends StatelessWidget { this.mainAssetUrl, this.showAppName = true, this.customAppBar, + this.actions, super.key, }); @@ -32,6 +34,7 @@ class PangeaLoginScaffold extends StatelessWidget { appBar: customAppBar ?? AppBar( toolbarHeight: isColumnMode ? null : 40.0, + actions: actions, ), body: LayoutBuilder( builder: (context, constraints) { diff --git a/lib/pangea/login/widgets/app_config_dialog.dart b/lib/pangea/login/widgets/app_config_dialog.dart new file mode 100644 index 000000000..1ca05b2d2 --- /dev/null +++ b/lib/pangea/login/widgets/app_config_dialog.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; + +class AppConfigDialog extends StatefulWidget { + final List overrides; + const AppConfigDialog({ + super.key, + required this.overrides, + }); + + @override + State createState() => AppConfigDialogState(); +} + +class AppConfigDialogState extends State { + AppConfigOverride? selectedOverride; + + @override + void initState() { + super.initState(); + selectedOverride = Environment.appConfigOverride; + } + + @override + Widget build(BuildContext context) { + return AlertDialog.adaptive( + title: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 256), + child: Text( + L10n.of(context).addEnvironmentOverride, + textAlign: TextAlign.center, + ), + ), + content: Material( + type: MaterialType.transparency, + child: Container( + padding: const EdgeInsets.all(16.0), + constraints: const BoxConstraints( + maxWidth: 256, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...widget.overrides.map((override) { + return RadioListTile.adaptive( + title: Text( + override.environment ?? L10n.of(context).unkDisplayName, + ), + value: override, + groupValue: selectedOverride, + onChanged: (override) { + setState(() { + selectedOverride = override; + }); + }, + ); + }).toList() + ..insert( + 0, + RadioListTile.adaptive( + title: Text(L10n.of(context).defaultOption), + value: null, + groupValue: selectedOverride, + onChanged: (override) { + setState(() { + selectedOverride = null; + }); + }, + ), + ), + ], + ), + ), + ), + ), + actions: [ + AdaptiveDialogAction( + bigButtons: true, + onPressed: () => Navigator.of(context).pop(selectedOverride), + child: Text(L10n.of(context).submit), + ), + AdaptiveDialogAction( + bigButtons: true, + onPressed: Navigator.of(context).pop, + child: Text(L10n.of(context).close), + ), + ], + ); + } +} diff --git a/lib/pangea/subscription/controllers/subscription_controller.dart b/lib/pangea/subscription/controllers/subscription_controller.dart index fc9aedf75..a0f4f058a 100644 --- a/lib/pangea/subscription/controllers/subscription_controller.dart +++ b/lib/pangea/subscription/controllers/subscription_controller.dart @@ -37,6 +37,7 @@ enum SubscriptionStatus { } class SubscriptionController extends BaseController { + static final GetStorage subscriptionBox = GetStorage("subscription_storage"); late PangeaController _pangeaController; CurrentSubscriptionInfo? currentSubscriptionInfo; @@ -81,7 +82,6 @@ class SubscriptionController extends BaseController { await initialize(); } - final GetStorage subscriptionBox = GetStorage("subscription_storage"); Future _initialize() async { try { if (_userID == null) { @@ -374,6 +374,37 @@ class SubscriptionController extends BaseController { String? get defaultManagementURL => currentSubscriptionInfo?.currentSubscription ?.defaultManagementURL(availableSubscriptionInfo?.appIds); + + Future setCachedSubscriptionInfo( + AvailableSubscriptionsInfo info, + ) => + subscriptionBox.write( + PLocalKey.availableSubscriptionInfo, + info.toJson(), + ); + + Future getCachedSubscriptionInfo() async { + final entry = subscriptionBox.read( + PLocalKey.availableSubscriptionInfo, + ); + if (entry is! Map) { + return null; + } + + try { + final resp = AvailableSubscriptionsInfo.fromJson(entry); + return resp.lastUpdated == null ? null : resp; + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + "entry": entry, + }, + ); + return null; + } + } } enum SubscriptionDuration { diff --git a/lib/pangea/subscription/models/base_subscription_info.dart b/lib/pangea/subscription/models/base_subscription_info.dart index cf7ecc54c..0f4e144d4 100644 --- a/lib/pangea/subscription/models/base_subscription_info.dart +++ b/lib/pangea/subscription/models/base_subscription_info.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; -import 'package:fluffychat/pangea/common/constants/local.key.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/subscription/repo/subscription_repo.dart'; @@ -74,9 +73,6 @@ class AvailableSubscriptionsInfo { List? allProducts; DateTime? lastUpdated; - final subscriptionBox = - MatrixState.pangeaController.subscriptionController.subscriptionBox; - AvailableSubscriptionsInfo({ this.appIds, this.allProducts, @@ -84,7 +80,8 @@ class AvailableSubscriptionsInfo { }); Future setAvailableSubscriptions() async { - final cachedInfo = _getCachedSubscriptionInfo(); + final cachedInfo = await MatrixState.pangeaController.subscriptionController + .getCachedSubscriptionInfo(); appIds ??= cachedInfo?.appIds ?? await SubscriptionRepo.getAppIds(); allProducts ??= cachedInfo?.allProducts ?? await SubscriptionRepo.getAllProducts(); @@ -102,11 +99,8 @@ class AvailableSubscriptionsInfo { Future _cacheSubscriptionInfo() async { try { - final json = toJson(); - await subscriptionBox.write( - PLocalKey.availableSubscriptionInfo, - json, - ); + MatrixState.pangeaController.subscriptionController + .setCachedSubscriptionInfo(this); } catch (e, s) { ErrorHandler.logError( e: e, @@ -119,29 +113,6 @@ class AvailableSubscriptionsInfo { } } - AvailableSubscriptionsInfo? _getCachedSubscriptionInfo() { - final json = subscriptionBox.read( - PLocalKey.availableSubscriptionInfo, - ); - if (json is! Map) { - return null; - } - - try { - final resp = AvailableSubscriptionsInfo.fromJson(json); - return resp.lastUpdated == null ? null : resp; - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - data: { - "json": json, - }, - ); - return null; - } - } - factory AvailableSubscriptionsInfo.fromJson(Map json) { if (!json.containsKey('app_ids') || !json.containsKey('all_products')) { throw "Cached subscription info is missing required fields";