Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,6 @@ app.*.symbols
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock
/app/env/


/modules/common/.flutter-plugins
/modules/common/.flutter-plugins-dependencies
Expand Down
7 changes: 3 additions & 4 deletions app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,8 @@ app.*.symbols
/env*.json

# Environment and configuration files (contain sensitive data)
env/.env
env/.settings
env/env_*.json
../env/.env
../env/env_*.json
.env
.env.*
*.env
Expand All @@ -141,4 +140,4 @@ firebase-debug.log

# Local development files
local.properties
*.properties
*.properties
13 changes: 13 additions & 0 deletions app/env/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# The .env file MUST be in .gitignore
# Delete this file after creating .env
#
# Each flavor (dev/qa/prod) uses the same .env file but reads
# different variables based on the suffix:
# - Development (main_dev.dart) → reads *_DEV variables
# - QA/Staging (main_qa.dart) → reads *_QA variables
# - Production (main.dart) → reads *_PROD variables

API_URL_DEV=https://your-api-url-dev.com
API_URL_QA=https://your-api-url-qa.com
API_URL_PROD=https://your-api-url-prod.com

38 changes: 36 additions & 2 deletions app/lib/main/env/env_config.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import 'package:domain/env/env_config.dart';

enum Flavor { dev, qa, prod }

class FlavorValues {
final String baseUrl;

FlavorValues({required this.baseUrl});
FlavorValues();
}

class FlavorConfig {
Expand All @@ -22,6 +23,17 @@ class FlavorConfig {
flavor.toString(),
values,
);
switch(flavor) {
case Flavor.dev:
EnvConfig.env = EnvConfig.kDevEnv;
break;
case Flavor.qa:
EnvConfig.env = EnvConfig.kQaEnv;
break;
case Flavor.prod:
EnvConfig.env = EnvConfig.kProdEnv;
break;
}
return _instance!;
}

Expand All @@ -34,4 +46,26 @@ class FlavorConfig {
static bool isDevelopment() => instance.flavor == Flavor.dev;

static bool isQA() => instance.flavor == Flavor.qa;

/// Returns the environment file path (single .env file for all flavors)
///
/// SETUP INSTRUCTIONS:
/// 1. Navigate to app/env/ directory
/// 2. Create a .env file (must be in .gitignore):
/// cp .env.example .env
/// 3. Add your environment variables with flavor suffixes:
/// API_URL_DEV=https://dev-api.example.com
/// API_URL_QA=https://qa-api.example.com
/// API_URL_PROD=https://api.example.com
/// 4. The active flavor determines which suffix is used (_DEV, _QA, _PROD)
/// 5. Access variables in domain/lib/env/env_config.dart using:
/// static String get apiUrl => dotenv.env['API_URL_$env']?.toString() ?? '';
/// (The $env automatically appends DEV, QA, or PROD based on active flavor)
/// 6. Go to pubspec.yaml and add the following:
/// assets:
/// - env/.env

static String getEnvFilePath() {
return 'env/.env.example';
}
}
3 changes: 1 addition & 2 deletions app/lib/main/env/main.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import 'package:flutter/material.dart';
import 'package:app/main/env/env_config.dart';
import 'package:app/main/init.dart';
import 'package:domain/env.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
FlavorConfig(
flavor: Flavor.prod,
values: FlavorValues(baseUrl: EnvConfig.apiUrl),
values: FlavorValues(),
);
//Add your firebase configuration here
/*await Firebase.initializeApp(
Expand Down
2 changes: 1 addition & 1 deletion app/lib/main/env/main_dev.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized();
FlavorConfig(
flavor: Flavor.dev,
values: FlavorValues(baseUrl: "https://demo_dev/web_api.json"),
values: FlavorValues(),
);
//Add your firebase configuration here
/*await Firebase.initializeApp(
Expand Down
2 changes: 1 addition & 1 deletion app/lib/main/env/main_qa.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized();
FlavorConfig(
flavor: Flavor.qa,
values: FlavorValues(baseUrl: "https://demo_qa/web_api.json"),
values: FlavorValues(),
);
//Add your firebase configuration here
/*await Firebase.initializeApp(
Expand Down
6 changes: 5 additions & 1 deletion app/lib/main/init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import 'package:domain/init.dart';
import 'package:example_domain/init.dart';
import 'package:example_data/init.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_it/get_it.dart';
import 'package:url_strategy/url_strategy.dart';

import 'env/env_config.dart';

void init() async {
WidgetsFlutterBinding.ensureInitialized();
await initialize();
Expand All @@ -18,11 +21,12 @@ void init() async {
final getIt = GetIt.instance;

Future<void> initialize() async {
await dotenv.load(fileName: FlavorConfig.getEnvFilePath());
await CommonInit.initialize(getIt);
await DataInit.initialize(getIt);
await DomainInit.initialize(getIt);

// Example Module init
await ExampleDomainInit.initialize(getIt);
await ExampleDataInit.initialize(getIt);
await ExampleDomainInit.initialize(getIt);
}
50 changes: 50 additions & 0 deletions app/lib/presentation/ui/custom/environment_selector.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:app/presentation/resources/resources.dart';
import 'package:app/presentation/themes/local_theme.dart';
import 'package:domain/env/env_config.dart';
import 'package:domain/services/environment_service.dart';
import 'package:flutter/material.dart';

import '../../../main/init.dart';

class EnvironmentSelector extends StatelessWidget {
EnvironmentSelector({
super.key,
});

final EnvironmentService environmentService = getIt<EnvironmentService>();

DropdownMenuItem<String> _item(
String value, String label, TextStyle textStyle) =>
DropdownMenuItem<String>(
value: value,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: spacing.xs),
child: Text(label, style: textStyle),
),
);

@override
Widget build(BuildContext context) {
final textStyle =
Theme.of(context).textTheme.bodyLarge?.copyWith(color: Theme.of(context).colorScheme.primary.v0);

final items = <DropdownMenuItem<String>>[
_item(EnvConfig.kDevEnv, 'Development', textStyle!),
_item(EnvConfig.kQaEnv, 'QA', textStyle),
_item(EnvConfig.kProdEnv, 'Production', textStyle),
];

return DropdownButtonFormField<String>(
initialValue: EnvConfig.env,
style: textStyle,
decoration: InputDecoration(
labelText: 'Environment',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.settings),
labelStyle: textStyle,
),
items: items,
onChanged: (value) => environmentService.setEnvironment(value!),
);
}
}
16 changes: 12 additions & 4 deletions app/lib/presentation/ui/pages/login/login_page.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import 'package:app/main/init.dart';
import 'package:app/presentation/resources/resources.dart';
import 'package:app/presentation/themes/app_themes.dart';
import 'package:app/presentation/ui/custom/app_theme_switch.dart';
import 'package:app/presentation/ui/custom/loading_screen.dart';
import 'package:common/core/resource.dart';
import 'package:domain/bloc/auth/auth_cubit.dart';
import 'package:domain/services/auth_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:app/presentation/ui/custom/app_theme_switch.dart';
import 'package:app/presentation/ui/custom/loading_screen.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import '../../custom/environment_selector.dart';

class LoginPage extends StatelessWidget {
AuthService get _authService => getIt();

Expand All @@ -21,12 +25,12 @@ class LoginPage extends StatelessWidget {
appBar: AppBar(),
backgroundColor: context.theme.colorScheme.surface,
body: Padding(
padding: const EdgeInsets.all(16),
padding: EdgeInsets.all(spacing.m),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const AppThemeSwitch(),
const SizedBox(height: 16),
SizedBox(height: spacing.m),
SizedBox(
width: double.maxFinite,
child: ElevatedButton(
Expand All @@ -39,6 +43,10 @@ class LoginPage extends StatelessWidget {
},
),
),
SizedBox(height: spacing.xxxl),
if (kDebugMode) ...[
EnvironmentSelector(),
],
],
),
),
Expand Down
3 changes: 3 additions & 0 deletions app/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies:
equatable: ^2.0.5
firebase_core: ^3.13.0
url_strategy: ^0.2.0
flutter_dotenv: ^5.2.1
flutter_localizations:
sdk: flutter
domain:
Expand Down Expand Up @@ -82,6 +83,8 @@ flutter:
# Remove example assets.
assets:
- assets/images/
# remove example suffix
- env/.env.example
fonts:
- family: Roboto Black
fonts:
Expand Down
6 changes: 0 additions & 6 deletions modules/common/.flutter-plugins

This file was deleted.

14 changes: 0 additions & 14 deletions modules/data/.flutter-plugins

This file was deleted.

16 changes: 11 additions & 5 deletions modules/data/lib/init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,30 @@ import 'package:data/repositories/common_repository_impl.dart';
import 'package:dio/dio.dart';
import 'package:domain/repositories/auth_repository.dart';
import 'package:domain/repositories/common_repository.dart';
import 'package:domain/services/environment_service.dart';
import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'network/config/environment_service_impl.dart';
import 'network/interceptors/auth_token_interceptor.dart';

class DataInit {
static Future<void> initialize(GetIt getIt) async {
final pref = await SharedPreferences.getInstance();

getIt.registerSingleton<SharedPreferences>(pref);
getIt.registerSingleton<Preferences>(PreferencesImpl(getIt()));

//Network
getIt.registerLazySingleton<Dio>(() => NetworkConfig.provideDio());
// Network
getIt.registerLazySingleton<AuthTokenInterceptor>(() => AuthTokenInterceptor(getIt()));
getIt.registerLazySingleton<Dio>(() => NetworkConfig.provideDio(getIt()));
getIt.registerLazySingleton<EnvironmentService>(() => EnvironmentServiceImpl(getIt()));

//Data Sources
// Data Sources

//Repositories
// Repositories
getIt.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImpl(getIt()),
() => AuthRepositoryImpl(getIt(),),
);
getIt.registerLazySingleton<CommonRepository>(
() => CommonRepositoryImpl(getIt()),
Expand Down
16 changes: 16 additions & 0 deletions modules/data/lib/network/config/environment_service_impl.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:data/network/config/network_config.dart';
import 'package:dio/dio.dart';
import 'package:domain/env/env_config.dart';
import 'package:domain/services/environment_service.dart';

class EnvironmentServiceImpl extends EnvironmentService {
final Dio _dio;

EnvironmentServiceImpl(this._dio);

@override
void setEnvironment(String env) {
EnvConfig.env = env;
NetworkConfig.updateBaseUrl(_dio);
}
}
32 changes: 29 additions & 3 deletions modules/data/lib/network/config/network_config.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
import 'package:dio/dio.dart';
import 'package:domain/env/env_config.dart';
import 'package:flutter/foundation.dart';

import '../interceptors/auth_token_interceptor.dart';
import 'network_constants.dart';

class NetworkConfig {
static Dio provideDio() {
static Dio provideDio(AuthTokenInterceptor? authTokenInterceptor) {
final options = BaseOptions(
baseUrl: NetworkConstants.baseUrl,
baseUrl: EnvConfig.apiUrl,
connectTimeout: const Duration(
seconds: NetworkConstants.connectTimeout,
),
receiveTimeout: const Duration(
seconds: NetworkConstants.receiveTimeout,
),
);
return Dio(options);

final dio = Dio(options);

// Add debug logging only in debug mode
if (kDebugMode) {
dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
requestHeader: true,
responseHeader: true,
error: true,
logPrint: (object) => debugPrint(object.toString()),
));
}

if (authTokenInterceptor != null) {
dio.interceptors.add(authTokenInterceptor);
}

return dio;
}

static void updateBaseUrl(Dio dio) {
dio.options.baseUrl = EnvConfig.apiUrl;
}
}
Loading