feat(proj): init

This commit is contained in:
vl.arkhangelskii
2026-09-21 04:06:43 +03:00
commit c956b94983
1076 changed files with 50876 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
import 'package:please_pay_me/core/config/env_file.dart';
/// Runtime configuration.
///
/// Values are resolved in this order:
/// 1. `--dart-define=PPM_*` / `--dart-define-from-file=.env` (CI and `run.ps1`)
/// 2. key/value map parsed from `mobile/.env` (tests and explicit loaders)
/// 3. production cabinet if nothing is set
class AppConfig {
static const productionOrigin = 'https://please-pay-me.ru';
const AppConfig({
required this.apiBaseUrl,
required this.webCabinetUrl,
this.demoMode = false,
});
factory AppConfig.fromEnvironment({Map<String, String> file = const {}}) {
const definedApi = String.fromEnvironment('PPM_API_BASE_URL');
const definedWeb = String.fromEnvironment('PPM_WEB_URL');
const definedDemo = String.fromEnvironment('PPM_DEMO');
return AppConfig.fromMap({
...file,
if (definedApi.isNotEmpty) 'PPM_API_BASE_URL': definedApi,
if (definedWeb.isNotEmpty) 'PPM_WEB_URL': definedWeb,
if (definedDemo.isNotEmpty) 'PPM_DEMO': definedDemo,
});
}
factory AppConfig.fromMap(Map<String, String> values) {
final apiBase = normalizeUrl(values['PPM_API_BASE_URL'] ?? '');
final webUrl = normalizeUrl(values['PPM_WEB_URL'] ?? '');
final demoForced = parseEnvFlag(values['PPM_DEMO']);
final resolvedApi = apiBase.isEmpty ? productionOrigin : apiBase;
final resolvedWeb = webUrl.isEmpty ? resolvedApi : webUrl;
return AppConfig(
apiBaseUrl: resolvedApi,
webCabinetUrl: resolvedWeb,
demoMode: demoForced,
);
}
static const demo = AppConfig(apiBaseUrl: '', webCabinetUrl: '', demoMode: true);
final String apiBaseUrl;
final String webCabinetUrl;
final bool demoMode;
AppConfig copyWith({String? apiBaseUrl, String? webCabinetUrl, bool? demoMode}) {
return AppConfig(
apiBaseUrl: apiBaseUrl ?? this.apiBaseUrl,
webCabinetUrl: webCabinetUrl ?? this.webCabinetUrl,
demoMode: demoMode ?? this.demoMode,
);
}
}
+41
View File
@@ -0,0 +1,41 @@
/// Minimal `.env` parser (KEY=VALUE, `#` comments, optional quotes).
///
/// Kept tiny and dependency-free so config loading is easy to test and does
/// not pull `flutter_dotenv` into the production graph.
Map<String, String> parseEnvFile(String source) {
final values = <String, String>{};
for (final raw in source.split(RegExp(r'\r?\n'))) {
final line = raw.trim();
if (line.isEmpty || line.startsWith('#')) continue;
final separator = line.indexOf('=');
if (separator <= 0) continue;
final key = line.substring(0, separator).trim();
if (key.isEmpty) continue;
var value = line.substring(separator + 1).trim();
if (value.length >= 2) {
final quote = value[0];
if ((quote == '"' || quote == "'") && value.endsWith(quote)) {
value = value.substring(1, value.length - 1);
}
}
values[key] = value;
}
return values;
}
String normalizeUrl(String url) => url.trim().replaceAll(RegExp(r'/$'), '');
bool parseEnvFlag(String? raw, {bool fallback = false}) {
if (raw == null || raw.trim().isEmpty) return fallback;
return switch (raw.trim().toLowerCase()) {
'1' || 'true' || 'yes' || 'on' => true,
'0' || 'false' || 'no' || 'off' => false,
_ => fallback,
};
}
+3
View File
@@ -0,0 +1,3 @@
import 'env_loader_stub.dart' if (dart.library.io) 'env_loader_io.dart' as impl;
Future<Map<String, String>> loadEnvFile() => impl.loadEnvFileImpl();
+15
View File
@@ -0,0 +1,15 @@
import 'dart:io';
import 'package:please_pay_me/core/config/env_file.dart';
/// Reads `mobile/.env` when the process cwd is the package or the repo root.
/// Dart-defines from `run.ps1` still win in [AppConfig.fromEnvironment].
Future<Map<String, String>> loadEnvFileImpl() async {
for (final path in const ['.env', 'mobile/.env']) {
final file = File(path);
if (await file.exists()) {
return parseEnvFile(await file.readAsString());
}
}
return const {};
}
@@ -0,0 +1 @@
Future<Map<String, String>> loadEnvFileImpl() async => const {};