diff --git a/.gitignore b/.gitignore index bbb74115..9d5ba6c3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ *.mocks.dart android/build/ ios/XCBuildData/ +.env diff --git a/ENV_SETUP.md b/ENV_SETUP.md new file mode 100644 index 00000000..f57b9796 --- /dev/null +++ b/ENV_SETUP.md @@ -0,0 +1,82 @@ +# Настройка переменных окружения + +Этот проект использует переменные окружения для хранения секретных данных и конфигурации. + +## Установка + +1. Скопируйте файл `env.example` в `.env`: +```bash +cp env.example .env +``` + +2. Отредактируйте файл `.env` и замените значения на ваши собственные секреты. + +## Переменные окружения + +### API URLs +- `BASE_URL` - основной URL API (по умолчанию: https://eatvio.ru) +- `DEV_BASE_URL` - URL API для разработки (по умолчанию: https://dev2.eatvio.com) + +### Sentry Configuration +- `SENTRY_DSN` - DSN для Sentry мониторинга + +### Firebase Configuration +- `FIREBASE_PROJECT_ID` - ID проекта Firebase +- `FIREBASE_DATABASE_URL` - URL базы данных Firebase +- `FIREBASE_STORAGE_BUCKET` - bucket для хранения файлов +- `FIREBASE_MESSAGING_SENDER_ID` - ID отправителя сообщений + +### Firebase Android Configuration +- `FIREBASE_ANDROID_API_KEY` - API ключ для Android +- `FIREBASE_ANDROID_APP_ID` - ID приложения для Android + +### Firebase iOS Configuration +- `FIREBASE_IOS_API_KEY` - API ключ для iOS +- `FIREBASE_IOS_APP_ID` - ID приложения для iOS +- `FIREBASE_IOS_BUNDLE_ID` - Bundle ID для iOS + +### App Store URLs +- `APP_STORE_URL` - URL App Store +- `RUSTORE_URL` - URL RuStore + +### Telegram Support +- `TELEGRAM_SUPPORT_URL` - URL поддержки в Telegram + +### Test Credentials (только для разработки) +- `TEST_EMAIL` - тестовый email +- `TEST_PASSWORD` - тестовый пароль + +### Email Validation API +- `EMAIL_VALIDATION_API_URL` - URL API для валидации email + +### Connection Check URLs +- `GOOGLE_CHECK_URL` - URL для проверки соединения (Google) +- `YANDEX_CHECK_URL` - URL для проверки соединения (Yandex) + +## Безопасность + +⚠️ **ВАЖНО**: Файл `.env` добавлен в `.gitignore` и не должен попадать в репозиторий. Все секретные данные должны храниться только в этом файле. + +## Использование в коде + +Для доступа к переменным окружения используйте класс `EnvConfig`: + +```dart +import 'package:eatvio_app/core/env_config.dart'; + +String baseUrl = EnvConfig.baseUrl; +String sentryDsn = EnvConfig.sentryDsn; +``` + +## Миграция + +Все секреты были перенесены из кода в переменные окружения: + +1. **Sentry DSN** - из `main.dart` +2. **Firebase конфигурация** - из `firebase_options.dart` +3. **API URLs** - из `const.dart` +4. **URL для проверки соединения** - из `internet_connection_checker_screen.dart` +5. **URL для обновления приложения** - из `update_screen.dart` +6. **URL поддержки Telegram** - из `support_screen.dart` +7. **API для валидации email** - из `registration_repository.dart` +8. **Тестовые учетные данные** - из `test_cases.dart` \ No newline at end of file diff --git a/lib/core/const.dart b/lib/core/const.dart index ad841863..efed7d49 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,4 +1,6 @@ -String baseUrl = 'https://eatvio.ru'; +import 'package:eatvio_app/core/env_config.dart'; + +String baseUrl = EnvConfig.baseUrl; bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); const List durationValues = [7, 14, 21]; diff --git a/lib/features/dev_tools/presentation/dev_tools_screen.dart b/lib/features/dev_tools/presentation/dev_tools_screen.dart index a4ff7730..4922a87d 100644 --- a/lib/features/dev_tools/presentation/dev_tools_screen.dart +++ b/lib/features/dev_tools/presentation/dev_tools_screen.dart @@ -9,6 +9,7 @@ import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/dev_tools/test_cases.dart'; +import 'package:eatvio_app/core/env_config.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -97,10 +98,15 @@ class _DevToolsScreenContentState extends State { padding: const EdgeInsets.symmetric(horizontal: 10), child: Column( children: [ - Padding(padding: const EdgeInsets.all(8.0), child: Row(children: [Text('BaseUrl: $baseUrlForTest')])), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row(children: [Text('BaseUrl: $baseUrlForTest')]), + ), Row( children: [ - Expanded(child: CustomTextFormField(controller: controller, hintText: 'type new base url')), + Expanded( + child: CustomTextFormField(controller: controller, hintText: 'type new base url'), + ), IconButton( onPressed: () { setState(() { @@ -116,13 +122,13 @@ class _DevToolsScreenContentState extends State { ), const SizedBox(height: 10), CustomOutlinedButton( - text: baseUrl == 'https://dev2.eatvio.com' ? 'Set production base url' : 'Set dev2 base url', + text: baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url', onPressed: () { setState(() { - if (baseUrl == 'https://eatvio.ru') { - baseUrlForTest = 'https://dev2.eatvio.com'; + if (baseUrl == EnvConfig.baseUrl) { + baseUrlForTest = EnvConfig.devBaseUrl; } else { - baseUrlForTest = 'https://eatvio.ru'; + baseUrlForTest = EnvConfig.baseUrl; } baseUrl = baseUrlForTest.toLowerCase(); sl().options.baseUrl = baseUrl; @@ -132,7 +138,7 @@ class _DevToolsScreenContentState extends State { buttonStyle: const ButtonStyle(), ), SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10), - if (baseUrl.toLowerCase() != 'https://eatvio.ru') + if (baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase()) ListView.builder( shrinkWrap: true, itemCount: testCases.length, @@ -175,12 +181,7 @@ class _DevToolsScreenContentState extends State { builder: (context) => AlertDialog( title: const Text('Test Failed'), content: SingleChildScrollView(child: Text(result.error ?? 'No error details.')), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))], ), ); } @@ -197,12 +198,7 @@ class _DevToolsScreenContentState extends State { builder: (context) => AlertDialog( title: const Text('Test Failed'), content: SingleChildScrollView(child: Text('$e\n$stack')), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))], ), ); } @@ -216,10 +212,10 @@ class _DevToolsScreenContentState extends State { TestState.fail => Container(width: 20, height: 20, color: Colors.red), TestState.notRun => Container(width: 20, height: 20, color: Colors.grey), TestState.loading => const SizedBox( - width: 20, - height: 20, - child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())), - ), + width: 20, + height: 20, + child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())), + ), }, ], ), diff --git a/lib/features/dev_tools/test_cases.dart b/lib/features/dev_tools/test_cases.dart index eb72a1e7..3d43bd2a 100644 --- a/lib/features/dev_tools/test_cases.dart +++ b/lib/features/dev_tools/test_cases.dart @@ -35,6 +35,7 @@ import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart'; import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart'; import 'package:eatvio_app/features/registration/services/registration_service.dart'; import 'package:eatvio_app/features/settings/services/settings_service.dart'; +import 'package:eatvio_app/core/env_config.dart'; class TestResult { final bool success; @@ -477,7 +478,9 @@ class AuthServiceTest implements TestCases { final dio = sl(); final authService = AuthService(dio, baseUrl: baseUrl); // Use dummy credentials, expecting failure but no crash - await authService.getTokenByCredentials(credentials: const CredentialsModel(email: 'reshetnick.mike+user1@gmail.com', password: '1234567890')); + await authService.getTokenByCredentials( + credentials: CredentialsModel(email: EnvConfig.testEmail, password: EnvConfig.testPassword), + ); return TestResult(false, 'Should not succeed'); } catch (e) { return TestResult(true, 'Exception expected for dummy credentials'); diff --git a/lib/features/internet_connection_checker/internet_connection_checker_screen.dart b/lib/features/internet_connection_checker/internet_connection_checker_screen.dart index 3aa1bc34..576eb891 100644 --- a/lib/features/internet_connection_checker/internet_connection_checker_screen.dart +++ b/lib/features/internet_connection_checker/internet_connection_checker_screen.dart @@ -10,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_svg/svg.dart'; import 'package:http/http.dart' as http; +import 'package:eatvio_app/core/env_config.dart'; class InternetConnectionCheckerScreen extends StatefulWidget { const InternetConnectionCheckerScreen({super.key, required this.child}); @@ -66,12 +67,7 @@ class _InternetConnectionCheckerScreenState extends State emailIsReachable(String email) async { try { - final response = await sl().getUri(Uri.parse('https://api.validate.email/validate?email=$email')); + final response = await sl().getUri(Uri.parse('${EnvConfig.emailValidationApiUrl}?email=$email')); if (response.data['result']['reachable'] != 'invalid') { return true; } else { diff --git a/lib/features/support/presentation/pages/support_screen.dart b/lib/features/support/presentation/pages/support_screen.dart index b550040f..ce5b843a 100644 --- a/lib/features/support/presentation/pages/support_screen.dart +++ b/lib/features/support/presentation/pages/support_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:eatvio_app/core/env_config.dart'; class SupportScreen extends StatelessWidget { const SupportScreen({super.key}); @@ -15,48 +16,28 @@ class SupportScreen extends StatelessWidget { final s = S.of(context); return Scaffold( - appBar: AppBar( - title: const Text('Поддержка'), - ), + appBar: AppBar(title: const Text('Поддержка')), body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.all(16), - child: Text( - 'Если у вас есть вопросы по работе нашего приложения, вы можете задать их, перейдя по ссылке:', - style: TextStyle( - fontSize: 16, - ), - ), + child: Text('Если у вас есть вопросы по работе нашего приложения, вы можете задать их, перейдя по ссылке:', style: TextStyle(fontSize: 16)), ), InkWell( onTap: () { - launchUrl( - Uri.parse('https://t.me/eatvio'), - mode: LaunchMode.externalApplication, - ); + launchUrl(Uri.parse(EnvConfig.telegramSupportUrl), mode: LaunchMode.externalApplication); }, borderRadius: BorderRadius.circular(16), child: Ink( width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)), padding: const EdgeInsets.all(16), child: Row( children: [ - SvgPicture.asset( - 'assets/icons/telegram.svg', - colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn), - ), + SvgPicture.asset('assets/icons/telegram.svg', colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn)), const SizedBox(width: 12), - const Text( - 'Telegram: t.me/eatvio', - style: TextStyle( - fontSize: 16, - ), - ), + const Text('Telegram: t.me/eatvio', style: TextStyle(fontSize: 16)), ], ), ), @@ -69,21 +50,14 @@ class SupportScreen extends StatelessWidget { }, borderRadius: BorderRadius.circular(16), child: Ink( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)), width: double.infinity, padding: const EdgeInsets.all(16), child: const Row( children: [ Icon(Icons.alternate_email_rounded), SizedBox(width: 12), - Text( - 'E-mail: help@eatv.io', - style: TextStyle( - fontSize: 16, - ), - ), + Text('E-mail: help@eatv.io', style: TextStyle(fontSize: 16)), ], ), ), @@ -92,13 +66,7 @@ class SupportScreen extends StatelessWidget { const SizedBox(height: 16), const Padding( padding: EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'Правовые документы', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), + child: Text('Правовые документы', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), ), const SizedBox(height: 12), InkWell( @@ -106,12 +74,7 @@ class SupportScreen extends StatelessWidget { borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text( - 'Публичная офёрта', - style: TextStyle( - fontSize: 16, - ), - ), + child: Text('Публичная офёрта', style: TextStyle(fontSize: 16)), ), ), const SizedBox(height: 8), @@ -120,12 +83,7 @@ class SupportScreen extends StatelessWidget { borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text( - 'Политика конфиденциальности', - style: TextStyle( - fontSize: 16, - ), - ), + child: Text('Политика конфиденциальности', style: TextStyle(fontSize: 16)), ), ), const SizedBox(height: 8), @@ -134,12 +92,7 @@ class SupportScreen extends StatelessWidget { borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text( - 'О компании', - style: TextStyle( - fontSize: 16, - ), - ), + child: Text('О компании', style: TextStyle(fontSize: 16)), ), ), ], diff --git a/lib/features/update_screen/presentation/update_screen.dart b/lib/features/update_screen/presentation/update_screen.dart index 186987c2..68ec9640 100644 --- a/lib/features/update_screen/presentation/update_screen.dart +++ b/lib/features/update_screen/presentation/update_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../../core/theme/theme_helper.dart'; +import '../../../../core/env_config.dart'; class UpdateScreen extends StatelessWidget { const UpdateScreen({super.key}); @@ -17,16 +18,7 @@ class UpdateScreen extends StatelessWidget { body: Stack( clipBehavior: Clip.none, children: [ - Positioned( - left: -20, - top: -20, - right: -20, - bottom: -20, - child: Image.asset( - 'assets/images/bg_welcome.png', - fit: BoxFit.cover, - ), - ), + Positioned(left: -20, top: -20, right: -20, bottom: -20, child: Image.asset('assets/images/bg_welcome.png', fit: BoxFit.cover)), Positioned.fill( child: SafeArea( child: Padding( @@ -48,47 +40,26 @@ class UpdateScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16), - const Text( - 'Пора обновиться! ⚡', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w800, - height: 1, - ), - ), + const Text('Пора обновиться! ⚡', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w800, height: 1)), const SizedBox(height: 20), const Text( 'Мы выпустили важное обновление\nс новыми функциями и исправлениями.\nЧтобы продолжить пользоваться\nприложением без сбоев, обновите\nего прямо сейчас.', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1.15, - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1.15), ), const SizedBox(height: 32), InkWell( onTap: () async { - final Uri url = Platform.isAndroid - ? Uri.parse('https://www.rustore.ru/catalog/app/com.eatvioapp.app') - : Uri.parse('https://apps.apple.com/app/idYOUR_APP_ID'); + final Uri url = Platform.isAndroid ? Uri.parse(EnvConfig.rustoreUrl) : Uri.parse(EnvConfig.appStoreUrl); await launchUrl(url); }, borderRadius: BorderRadius.circular(8), child: Ink( height: 52, - decoration: BoxDecoration( - color: appTheme.blueA400, - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(8)), child: const Center( child: Text( 'Обновить', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - height: 1, - color: Colors.white, - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, height: 1, color: Colors.white), ), ), ), diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index dcf61a5d..6450332e 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -1,8 +1,8 @@ // File generated by FlutterFire CLI. // ignore_for_file: type=lint import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' - show defaultTargetPlatform, kIsWeb, TargetPlatform; +import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; +import 'package:eatvio_app/core/env_config.dart'; /// Default [FirebaseOptions] for use with your Firebase apps. /// @@ -43,28 +43,26 @@ class DefaultFirebaseOptions { 'you can reconfigure this by running the FlutterFire CLI again.', ); default: - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); + throw UnsupportedError('DefaultFirebaseOptions are not supported for this platform.'); } } - static const FirebaseOptions android = FirebaseOptions( - apiKey: 'AIzaSyBUe1UqAAMfgMcd4hWdRoLF1Os-yn-HE9k', - appId: '1:613980912922:android:4de95e5f2d50ba69ecc0a3', - messagingSenderId: '613980912922', - projectId: 'eatvio-3467b', - databaseURL: 'https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app', - storageBucket: 'eatvio-3467b.firebasestorage.app', + static FirebaseOptions get android => FirebaseOptions( + apiKey: EnvConfig.firebaseAndroidApiKey, + appId: EnvConfig.firebaseAndroidAppId, + messagingSenderId: EnvConfig.firebaseMessagingSenderId, + projectId: EnvConfig.firebaseProjectId, + databaseURL: EnvConfig.firebaseDatabaseUrl, + storageBucket: EnvConfig.firebaseStorageBucket, ); - static const FirebaseOptions ios = FirebaseOptions( - apiKey: 'AIzaSyBnE8tQjL9_1HuXrTnqNy6qbXpoenO36yQ', - appId: '1:613980912922:ios:c557d140a1212e63ecc0a3', - messagingSenderId: '613980912922', - projectId: 'eatvio-3467b', - databaseURL: 'https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app', - storageBucket: 'eatvio-3467b.firebasestorage.app', - iosBundleId: 'com.eatvioapp.app', + static FirebaseOptions get ios => FirebaseOptions( + apiKey: EnvConfig.firebaseIosApiKey, + appId: EnvConfig.firebaseIosAppId, + messagingSenderId: EnvConfig.firebaseMessagingSenderId, + projectId: EnvConfig.firebaseProjectId, + databaseURL: EnvConfig.firebaseDatabaseUrl, + storageBucket: EnvConfig.firebaseStorageBucket, + iosBundleId: EnvConfig.firebaseIosBundleId, ); } diff --git a/lib/main.dart b/lib/main.dart index 3fd39e7f..8e2796b0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,22 +19,24 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:eatvio_app/core/env_config.dart'; Future main() async { WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + + await dotenv.load(fileName: ".env"); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await FirebaseMessaging.instance.requestPermission(); await di.init(); - await SentryFlutter.init( - (options) { - options.dsn = 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2'; - options.tracesSampleRate = 1.0; - options.debug = false; - }, - appRunner: () => runApp(const MyApp()), - ); + await SentryFlutter.init((options) { + options.dsn = EnvConfig.sentryDsn; + options.tracesSampleRate = 1.0; + options.debug = false; + }, appRunner: () => runApp(const MyApp())); } class MyApp extends StatelessWidget { diff --git a/pubspec.yaml b/pubspec.yaml index 0dd1a2da..ef434489 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -72,6 +72,7 @@ dependencies: device_info_plus: ^11.5.0 permission_handler: ^12.0.1 qr_code_scanner_plus: ^2.0.10+1 + flutter_dotenv: ^5.1.0 dev_dependencies: build_runner: ^2.6.0