feat: allow users on staging to switch their environment (#2799)

pull/2245/head
ggurdin 6 months ago committed by GitHub
parent 6c3de08019
commit 33425f4406
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,3 +1,4 @@
ENVIRONMENT = 'staging'
CHOREO_API = 'https://api.staging.pangea.chat' CHOREO_API = 'https://api.staging.pangea.chat'
FRONTEND_URL='https://app.staging.pangea.chat' FRONTEND_URL='https://app.staging.pangea.chat'
SYNAPSE_URL = 'matrix.staging.pangea.chat' SYNAPSE_URL = 'matrix.staging.pangea.chat'

@ -47,6 +47,8 @@ jobs:
touch public/.env touch public/.env
echo "$WEB_APP_ENV" >> public/.env echo "$WEB_APP_ENV" >> public/.env
cp public/.env public/assets/.env cp public/.env public/assets/.env
touch public/assets/envs.json
echo "$ENV_OVERRIDES" >> public/assets/envs.json
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4 uses: peaceiris/actions-gh-pages@v4
with: with:

@ -45,6 +45,8 @@ jobs:
rm assets/.env rm assets/.env
echo "$WEB_APP_ENV" >> .env echo "$WEB_APP_ENV" >> .env
cp .env assets/.env cp .env assets/.env
touch assets/envs.json
echo "$ENV_OVERRIDES" >> assets/envs.json
- name: Apply .env patch - name: Apply .env patch
run: git apply ./scripts/enable_mobile_env.patch run: git apply ./scripts/enable_mobile_env.patch
- name: Install Fastlane - name: Install Fastlane

1
.gitignore vendored

@ -18,6 +18,7 @@ keys.json
!/public/.env !/public/.env
*.env.local_choreo *.env.local_choreo
*.env.prod *.env.prod
envs.json
# libolm package # libolm package
/assets/js/package /assets/js/package

@ -1,3 +1,4 @@
ENVIRONMENT = 'staging'
CHOREO_API = 'https://api.staging.pangea.chat' CHOREO_API = 'https://api.staging.pangea.chat'
FRONTEND_URL = 'https://app.staging.pangea.chat' FRONTEND_URL = 'https://app.staging.pangea.chat'

@ -4934,5 +4934,7 @@
"joinSpaceOnboardingDesc": "Do you have an invite code or link to a learning community?", "joinSpaceOnboardingDesc": "Do you have an invite code or link to a learning community?",
"skipForNow": "Skip for now", "skipForNow": "Skip for now",
"permissions": "Permissions", "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"
} }

@ -15,7 +15,7 @@ abstract class AppConfig {
static String? get applicationWelcomeMessage => _applicationWelcomeMessage; static String? get applicationWelcomeMessage => _applicationWelcomeMessage;
// #Pangea // #Pangea
// static String _defaultHomeserver = 'matrix.org'; // static String _defaultHomeserver = 'matrix.org';
static String _defaultHomeserver = Environment.synapseURL; static String get _defaultHomeserver => Environment.synapseURL;
// #Pangea // #Pangea
static String get defaultHomeserver => _defaultHomeserver; static String get defaultHomeserver => _defaultHomeserver;
static double fontSizeFactor = 1; static double fontSizeFactor = 1;
@ -206,9 +206,11 @@ abstract class AppConfig {
if (json['application_welcome_message'] is String) { if (json['application_welcome_message'] is String) {
_applicationWelcomeMessage = json['application_welcome_message']; _applicationWelcomeMessage = json['application_welcome_message'];
} }
if (json['default_homeserver'] is String) { // #Pangea
_defaultHomeserver = json['default_homeserver']; // if (json['default_homeserver'] is String) {
} // _defaultHomeserver = json['default_homeserver'];
// }
// Pangea#
if (json['privacy_url'] is String) { if (json['privacy_url'] is String) {
_privacyUrl = json['privacy_url']; _privacyUrl = json['privacy_url'];
} }

@ -110,7 +110,7 @@ Future<void> startGui(List<Client> clients, SharedPreferences store) async {
// staging or vice versa, logout. // staging or vice versa, logout.
if (firstClient?.userID?.domain != null) { if (firstClient?.userID?.domain != null) {
final isStagingUser = firstClient!.userID!.domain!.contains("staging"); final isStagingUser = firstClient!.userID!.domain!.contains("staging");
final isStagingServer = Environment.isStaging; final isStagingServer = Environment.synapseURL.contains("staging");
if (isStagingServer != isStagingUser) { if (isStagingServer != isStagingUser) {
await firstClient.logout(); await firstClient.logout();
} }

@ -330,7 +330,7 @@ class SettingsView extends StatelessWidget {
}, },
), ),
// Conditional ListTile based on the environment (staging or not) // Conditional ListTile based on the environment (staging or not)
if (Environment.isStaging) if (Environment.isStagingEnvironment)
ListTile( ListTile(
leading: const Icon(Icons.bug_report_outlined), leading: const Icon(Icons.bug_report_outlined),
title: Text(L10n.of(context).connectedToStaging), title: Text(L10n.of(context).connectedToStaging),

@ -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 /// A minimized version of AnalyticsController that get the logged in user's analytics
class GetAnalyticsController extends BaseController { class GetAnalyticsController extends BaseController {
final GetStorage analyticsBox = GetStorage("analytics_storage"); static final GetStorage analyticsBox = GetStorage("analytics_storage");
late PangeaController _pangeaController; late PangeaController _pangeaController;
late PracticeSelectionRepo perMessage; late PracticeSelectionRepo perMessage;
@ -276,6 +276,15 @@ class GetAnalyticsController extends BaseController {
} }
} }
Future<void> clearMessagesCache() async =>
analyticsBox.remove(PLocalKey.messagesSinceUpdate);
Future<void> setMessagesCache(Map<dynamic, dynamic> cacheValue) async =>
analyticsBox.write(
PLocalKey.messagesSinceUpdate,
cacheValue,
);
/// A flat list of all locally cached construct uses /// A flat list of all locally cached construct uses
List<OneConstructUse> get _locallyCachedConstructs => List<OneConstructUse> get _locallyCachedConstructs =>
messagesSinceUpdate.values.expand((e) => e).toList(); messagesSinceUpdate.values.expand((e) => e).toList();

@ -174,7 +174,7 @@ class LevelUpBannerState extends State<LevelUpBanner>
} }
Future<void> _toggleDetails() async { Future<void> _toggleDetails() async {
if (!Environment.isStaging) return; if (!Environment.isStagingEnvironment) return;
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -282,7 +282,7 @@ class LevelUpBannerState extends State<LevelUpBanner>
), ),
Row( Row(
children: [ children: [
if (Environment.isStaging) if (Environment.isStagingEnvironment)
AnimatedSize( AnimatedSize(
duration: FluffyThemes.animationDuration, duration: FluffyThemes.animationDuration,
child: _error == null child: _error == null

@ -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_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_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/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/base_controller.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart';
@ -319,16 +318,14 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
/// Clears the local cache of recently sent constructs. Called before updating analytics /// Clears the local cache of recently sent constructs. Called before updating analytics
void clearMessagesSinceUpdate({clearDrafts = false}) { void clearMessagesSinceUpdate({clearDrafts = false}) {
if (clearDrafts) { if (clearDrafts) {
MatrixState.pangeaController.getAnalytics.analyticsBox MatrixState.pangeaController.getAnalytics.clearMessagesCache();
.remove(PLocalKey.messagesSinceUpdate);
return; return;
} }
final localCache = _pangeaController.getAnalytics.messagesSinceUpdate; final localCache = _pangeaController.getAnalytics.messagesSinceUpdate;
final draftKeys = localCache.keys.where((key) => key.startsWith('draft')); final draftKeys = localCache.keys.where((key) => key.startsWith('draft'));
if (draftKeys.isEmpty) { if (draftKeys.isEmpty) {
MatrixState.pangeaController.getAnalytics.analyticsBox MatrixState.pangeaController.getAnalytics.clearMessagesCache();
.remove(PLocalKey.messagesSinceUpdate);
return; return;
} }
@ -348,10 +345,8 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
final constructJsons = entry.value.map((e) => e.toJson()).toList(); final constructJsons = entry.value.map((e) => e.toJson()).toList();
formattedCache[entry.key] = constructJsons; formattedCache[entry.key] = constructJsons;
} }
await MatrixState.pangeaController.getAnalytics.analyticsBox.write( await MatrixState.pangeaController.getAnalytics
PLocalKey.messagesSinceUpdate, .setMessagesCache(formattedCache);
formattedCache,
);
} }
/// Prevent concurrent updates to analytics /// Prevent concurrent updates to analytics

@ -3,7 +3,7 @@ import 'package:fluffychat/pangea/common/config/environment.dart';
class BotName { class BotName {
static String get byEnvironment => Environment.botName != null static String get byEnvironment => Environment.botName != null
? Environment.botName! ? Environment.botName!
: Environment.isStaging : Environment.isStagingEnvironment
? "@bot:staging.pangea.chat" ? "@bot:staging.pangea.chat"
: "@bot:pangea.chat"; : "@bot:pangea.chat";
static String get localBot => "@matrix-bot-test:staging.pangea.chat"; static String get localBot => "@matrix-bot-test:staging.pangea.chat";

@ -22,7 +22,6 @@ Map<String, dynamic> defaultPowerLevels(String userID) => {
"m.room.tombstone": 100, "m.room.tombstone": 100,
}, },
"users": { "users": {
"@bot:staging.pangea.chat": 50,
userID: 100, userID: 100,
}, },
}; };

@ -1,25 +1,35 @@
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.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 { class Environment {
static bool get itIsTime => static bool get itIsTime =>
DateTime.utc(2023, 1, 25).isBefore(DateTime.now()); DateTime.utc(2023, 1, 25).isBefore(DateTime.now());
static String get fileName { static bool get isStagingEnvironment =>
return ".env"; dotenv.env["ENVIRONMENT"] == "staging";
}
static bool get isStaging => synapseURL.contains("staging");
static String get frontendURL { 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 { 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 { static String get homeServer {
String? homeServerFromSynapseURL = dotenv.env['SYNAPSE_URL']; String? homeServerFromSynapseURL =
appConfigOverride?.synapseURL ?? dotenv.env['SYNAPSE_URL'];
if (homeServerFromSynapseURL != null) { if (homeServerFromSynapseURL != null) {
if (homeServerFromSynapseURL.startsWith("http://")) { if (homeServerFromSynapseURL.startsWith("http://")) {
homeServerFromSynapseURL = homeServerFromSynapseURL =
@ -34,13 +44,14 @@ class Environment {
homeServerFromSynapseURL.replaceFirst("matrix.", ""); homeServerFromSynapseURL.replaceFirst("matrix.", "");
} }
} }
return dotenv.env["HOME_SERVER"] ?? return appConfigOverride?.homeServer ??
dotenv.env["HOME_SERVER"] ??
homeServerFromSynapseURL ?? homeServerFromSynapseURL ??
'Home Server not found'; 'Home Server not found';
} }
static String get choreoApi { static String get choreoApi {
final envEntry = dotenv.env['CHOREO_API']; final envEntry = appConfigOverride?.choreoApi ?? dotenv.env['CHOREO_API'];
if (envEntry == null) { if (envEntry == null) {
return "Not found"; return "Not found";
} }
@ -54,48 +65,214 @@ class Environment {
} }
static String get choreoApiKey { static String get choreoApiKey {
return dotenv.env['CHOREO_API_KEY'] ?? return appConfigOverride?.choreoApiKey ??
dotenv.env['CHOREO_API_KEY'] ??
'e6fa9fa97031ba0c852efe78457922f278a2fbc109752fe18e465337699e9873'; 'e6fa9fa97031ba0c852efe78457922f278a2fbc109752fe18e465337699e9873';
} }
static String get sentryDsn { static String get sentryDsn {
return dotenv.env["SENTRY_DSN"] ?? return appConfigOverride?.sentryDsn ??
dotenv.env["SENTRY_DSN"] ??
'https://c2fd19ab2cdc4ebb939a32d01c0e9fa1@o225078.ingest.sentry.io/1376295'; 'https://c2fd19ab2cdc4ebb939a32d01c0e9fa1@o225078.ingest.sentry.io/1376295';
} }
static String get rcGoogleKey { 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 { static String get rcIosKey {
return dotenv.env["RC_IOS_KEY"] ?? 'appl_DUPqnxuLjkBLzhBPTWeDjqNENuv'; return appConfigOverride?.rcIosKey ??
} dotenv.env["RC_IOS_KEY"] ??
'appl_DUPqnxuLjkBLzhBPTWeDjqNENuv';
// This is a public key
static String get rcStripeKey {
return dotenv.env["RC_STRIPE_KEY"] ?? 'strp_YWZxWUeEfvagiefDNoofinaRCOl';
} }
static String get rcOfferingName { static String get rcOfferingName {
return dotenv.env["RC_OFFERING_NAME"] ?? 'default'; return appConfigOverride?.rcOfferingName ??
dotenv.env["RC_OFFERING_NAME"] ??
'default';
} }
static String get stripeManagementUrl { 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'; 'https://billing.stripe.com/p/login/dR6dSkf5p6rBc4EcMM';
} }
static String get supportSpaceId {
return isStaging
? '!gqSNSkvwTpgumyjLsV:staging.pangea.chat'
: '!MvJoWwKJErvFuTYOdq:pangea.chat';
}
static String get supportUserId { 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 { 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<List<AppConfigOverride>> getAppConfigOverrides() async {
if (!isStagingEnvironment) {
return [];
}
List<dynamic> 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<AppConfigOverride> overrides = [];
for (final entry in data) {
if (entry is! Map<String, dynamic>) {
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<void> 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<String, dynamic> 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<String, dynamic> 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;
} }
} }

@ -10,4 +10,5 @@ class PLocalKey {
static const String justInputtedCode = 'justInputtedCode'; static const String justInputtedCode = 'justInputtedCode';
static const String availableSubscriptionInfo = 'availableSubscriptionInfo'; static const String availableSubscriptionInfo = 'availableSubscriptionInfo';
static const String showedUpdateDialog = 'showedUpdateDialog'; static const String showedUpdateDialog = 'showedUpdateDialog';
static const String appConfigOverride = 'appConfigOverride';
} }

@ -107,17 +107,35 @@ class PangeaController {
_logOutfromPangea() { _logOutfromPangea() {
debugPrint("Pangea logout"); debugPrint("Pangea logout");
GoogleAnalytics.logout(); GoogleAnalytics.logout();
_clearCachedData(); clearCache();
} }
void _clearCachedData() { static final List<String> _storageKeys = [
GetStorage('mode_list_storage').erase(); 'mode_list_storage',
GetStorage('activity_plan_storage').erase(); 'activity_plan_storage',
GetStorage('bookmarked_activities').erase(); 'bookmarked_activities',
GetStorage('objective_list_storage').erase(); 'objective_list_storage',
GetStorage('topic_list_storage').erase(); 'topic_list_storage',
GetStorage('lemma_storage').erase(); 'activity_plan_search_storage',
GetStorage().erase(); "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<void> clearCache() async {
final List<Future<void>> futures = [];
for (final key in _storageKeys) {
futures.add(GetStorage(key).erase());
}
await Future.wait(futures);
} }
Future<void> checkHomeServerAction() async { Future<void> checkHomeServerAction() async {
@ -339,7 +357,7 @@ class PangeaController {
_languageStream ??= userController.stateStream.listen((update) { _languageStream ??= userController.stateStream.listen((update) {
if (update is Map<String, dynamic> && if (update is Map<String, dynamic> &&
update['prev_target_lang'] != null) { update['prev_target_lang'] != null) {
_clearCachedData(); clearCache();
} }
}); });
} }

@ -13,11 +13,11 @@ class PApiUrls {
static String subscriptionPrefix = "/subscription"; static String subscriptionPrefix = "/subscription";
static String accountPrefix = "/account"; static String accountPrefix = "/account";
static String choreoEndpoint = static String get choreoEndpoint =>
"${Environment.choreoApi}${PApiUrls.choreoPrefix}"; "${Environment.choreoApi}${PApiUrls.choreoPrefix}";
static String subscriptionEndpoint = static String get subscriptionEndpoint =>
"${Environment.choreoApi}${PApiUrls.subscriptionPrefix}"; "${Environment.choreoApi}${PApiUrls.subscriptionPrefix}";
static String accountEndpoint = static String get accountEndpoint =>
"${Environment.choreoApi}${PApiUrls.accountPrefix}"; "${Environment.choreoApi}${PApiUrls.accountPrefix}";
/// ---------------------- Util -------------------------------------- /// ---------------------- Util --------------------------------------

@ -29,7 +29,7 @@ class ErrorHandler {
options.debug = kDebugMode; options.debug = kDebugMode;
options.environment = kDebugMode options.environment = kDebugMode
? "debug" ? "debug"
: Environment.isStaging : Environment.isStagingEnvironment
? "staging" ? "staging"
: "productionC"; : "productionC";
}, },

@ -3,15 +3,57 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.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/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'; import 'package:fluffychat/pangea/login/widgets/full_width_button.dart';
class LoginOrSignupView extends StatelessWidget { class LoginOrSignupView extends StatefulWidget {
const LoginOrSignupView({super.key}); const LoginOrSignupView({super.key});
@override
State<LoginOrSignupView> createState() => LoginOrSignupViewState();
}
class LoginOrSignupViewState extends State<LoginOrSignupView> {
List<AppConfigOverride> _overrides = [];
@override
void initState() {
super.initState();
_loadOverrides();
}
Future<void> _loadOverrides() async {
final overrides = await Environment.getAppConfigOverrides();
if (mounted) {
setState(() => _overrides = overrides);
}
}
Future<void> _setEnvironment() async {
if (_overrides.isEmpty) return;
final resp = await showDialog<AppConfigOverride?>(
context: context,
builder: (context) => AppConfigDialog(overrides: _overrides),
);
await Environment.setAppConfigOverride(resp);
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PangeaLoginScaffold( return PangeaLoginScaffold(
actions: Environment.isStagingEnvironment && _overrides.isNotEmpty
? [
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: _setEnvironment,
),
]
: null,
children: [ children: [
FullWidthButton( FullWidthButton(
title: L10n.of(context).createAnAccount, title: L10n.of(context).createAnAccount,

@ -13,6 +13,7 @@ class PangeaLoginScaffold extends StatelessWidget {
final List<Widget> children; final List<Widget> children;
final bool showAppName; final bool showAppName;
final AppBar? customAppBar; final AppBar? customAppBar;
final List<Widget>? actions;
const PangeaLoginScaffold({ const PangeaLoginScaffold({
required this.children, required this.children,
@ -21,6 +22,7 @@ class PangeaLoginScaffold extends StatelessWidget {
this.mainAssetUrl, this.mainAssetUrl,
this.showAppName = true, this.showAppName = true,
this.customAppBar, this.customAppBar,
this.actions,
super.key, super.key,
}); });
@ -32,6 +34,7 @@ class PangeaLoginScaffold extends StatelessWidget {
appBar: customAppBar ?? appBar: customAppBar ??
AppBar( AppBar(
toolbarHeight: isColumnMode ? null : 40.0, toolbarHeight: isColumnMode ? null : 40.0,
actions: actions,
), ),
body: LayoutBuilder( body: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {

@ -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<AppConfigOverride> overrides;
const AppConfigDialog({
super.key,
required this.overrides,
});
@override
State<AppConfigDialog> createState() => AppConfigDialogState();
}
class AppConfigDialogState extends State<AppConfigDialog> {
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<AppConfigOverride?>.adaptive(
title: Text(
override.environment ?? L10n.of(context).unkDisplayName,
),
value: override,
groupValue: selectedOverride,
onChanged: (override) {
setState(() {
selectedOverride = override;
});
},
);
}).toList()
..insert(
0,
RadioListTile<AppConfigOverride?>.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),
),
],
);
}
}

@ -37,6 +37,7 @@ enum SubscriptionStatus {
} }
class SubscriptionController extends BaseController { class SubscriptionController extends BaseController {
static final GetStorage subscriptionBox = GetStorage("subscription_storage");
late PangeaController _pangeaController; late PangeaController _pangeaController;
CurrentSubscriptionInfo? currentSubscriptionInfo; CurrentSubscriptionInfo? currentSubscriptionInfo;
@ -81,7 +82,6 @@ class SubscriptionController extends BaseController {
await initialize(); await initialize();
} }
final GetStorage subscriptionBox = GetStorage("subscription_storage");
Future<void> _initialize() async { Future<void> _initialize() async {
try { try {
if (_userID == null) { if (_userID == null) {
@ -374,6 +374,37 @@ class SubscriptionController extends BaseController {
String? get defaultManagementURL => String? get defaultManagementURL =>
currentSubscriptionInfo?.currentSubscription currentSubscriptionInfo?.currentSubscription
?.defaultManagementURL(availableSubscriptionInfo?.appIds); ?.defaultManagementURL(availableSubscriptionInfo?.appIds);
Future<void> setCachedSubscriptionInfo(
AvailableSubscriptionsInfo info,
) =>
subscriptionBox.write(
PLocalKey.availableSubscriptionInfo,
info.toJson(),
);
Future<AvailableSubscriptionsInfo?> getCachedSubscriptionInfo() async {
final entry = subscriptionBox.read(
PLocalKey.availableSubscriptionInfo,
);
if (entry is! Map<String, dynamic>) {
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 { enum SubscriptionDuration {

@ -1,6 +1,5 @@
import 'package:collection/collection.dart'; 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/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/subscription/repo/subscription_repo.dart'; import 'package:fluffychat/pangea/subscription/repo/subscription_repo.dart';
@ -74,9 +73,6 @@ class AvailableSubscriptionsInfo {
List<SubscriptionDetails>? allProducts; List<SubscriptionDetails>? allProducts;
DateTime? lastUpdated; DateTime? lastUpdated;
final subscriptionBox =
MatrixState.pangeaController.subscriptionController.subscriptionBox;
AvailableSubscriptionsInfo({ AvailableSubscriptionsInfo({
this.appIds, this.appIds,
this.allProducts, this.allProducts,
@ -84,7 +80,8 @@ class AvailableSubscriptionsInfo {
}); });
Future<void> setAvailableSubscriptions() async { Future<void> setAvailableSubscriptions() async {
final cachedInfo = _getCachedSubscriptionInfo(); final cachedInfo = await MatrixState.pangeaController.subscriptionController
.getCachedSubscriptionInfo();
appIds ??= cachedInfo?.appIds ?? await SubscriptionRepo.getAppIds(); appIds ??= cachedInfo?.appIds ?? await SubscriptionRepo.getAppIds();
allProducts ??= allProducts ??=
cachedInfo?.allProducts ?? await SubscriptionRepo.getAllProducts(); cachedInfo?.allProducts ?? await SubscriptionRepo.getAllProducts();
@ -102,11 +99,8 @@ class AvailableSubscriptionsInfo {
Future<void> _cacheSubscriptionInfo() async { Future<void> _cacheSubscriptionInfo() async {
try { try {
final json = toJson(); MatrixState.pangeaController.subscriptionController
await subscriptionBox.write( .setCachedSubscriptionInfo(this);
PLocalKey.availableSubscriptionInfo,
json,
);
} catch (e, s) { } catch (e, s) {
ErrorHandler.logError( ErrorHandler.logError(
e: e, e: e,
@ -119,29 +113,6 @@ class AvailableSubscriptionsInfo {
} }
} }
AvailableSubscriptionsInfo? _getCachedSubscriptionInfo() {
final json = subscriptionBox.read(
PLocalKey.availableSubscriptionInfo,
);
if (json is! Map<String, dynamic>) {
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<String, dynamic> json) { factory AvailableSubscriptionsInfo.fromJson(Map<String, dynamic> json) {
if (!json.containsKey('app_ids') || !json.containsKey('all_products')) { if (!json.containsKey('app_ids') || !json.containsKey('all_products')) {
throw "Cached subscription info is missing required fields"; throw "Cached subscription info is missing required fields";

Loading…
Cancel
Save