sign in with pocketbase working

This commit is contained in:
davmar
2026-09-09 19:59:38 +02:00
parent ae464ff304
commit c6d1fdb19e
27 changed files with 1267 additions and 312 deletions
+66
View File
@@ -0,0 +1,66 @@
/// Runtime config from `.env`, with optional `--dart-define` overrides.
library;
import 'package:flutter_dotenv/flutter_dotenv.dart';
/// Loads once at startup from the `.env` asset (see `.env.example`).
/// Pass `--dart-define=KEY=value` to override for CI / one-off builds.
abstract final class AppConfig {
static var _loaded = false;
static late String apiBaseUrl;
static late String pocketBaseUrl;
static late String pocketBaseUsersCollection;
static Future<void> load() async {
if (_loaded) return;
try {
await dotenv.load(fileName: '.env', isOptional: true);
if (dotenv.env.isEmpty) {
await dotenv.load(fileName: '.env.example', isOptional: true);
}
} catch (_) {
// Missing asset in tests is fine — use fallbacks / loadForTest.
}
apiBaseUrl = _resolve(
dartDefine: const String.fromEnvironment('API_BASE_URL'),
envKey: 'API_BASE_URL',
fallback: 'https://ingtrak.satoshinakamoto.win',
);
pocketBaseUrl = _resolve(
dartDefine: const String.fromEnvironment('POCKETBASE_URL'),
envKey: 'POCKETBASE_URL',
fallback: 'http://127.0.0.1:8090',
);
pocketBaseUsersCollection = _resolve(
dartDefine: const String.fromEnvironment('POCKETBASE_USERS_COLLECTION'),
envKey: 'POCKETBASE_USERS_COLLECTION',
fallback: 'usersapp',
);
_loaded = true;
}
/// For widget tests that don't go through [main].
static void loadForTest({
String apiBaseUrl = 'https://ingtrak.satoshinakamoto.win',
String pocketBaseUrl = 'http://127.0.0.1:8090',
String pocketBaseUsersCollection = 'usersapp',
}) {
AppConfig.apiBaseUrl = apiBaseUrl;
AppConfig.pocketBaseUrl = pocketBaseUrl;
AppConfig.pocketBaseUsersCollection = pocketBaseUsersCollection;
_loaded = true;
}
static String _resolve({
required String dartDefine,
required String envKey,
required String fallback,
}) {
if (dartDefine.isNotEmpty) return dartDefine;
final fromEnv = dotenv.maybeGet(envKey)?.trim();
if (fromEnv != null && fromEnv.isNotEmpty) return fromEnv;
return fallback;
}
}
-3
View File
@@ -240,9 +240,6 @@ abstract final class AppTheme {
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
),
),
side: BorderSide(
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
+14 -12
View File
@@ -1,6 +1,8 @@
/// Shared string keys, timings, and layout breakpoints used across the app.
library;
import 'app_config.dart';
/// Human-readable name shown in the splash screen, app bars, and About tile.
const String kAppName = 'IngTrak';
@@ -10,18 +12,14 @@ const String kPackageName = 'com.example.myapp';
/// How long the splash screen stays visible before navigating to the shell.
const Duration kSplashDuration = Duration(seconds: 2);
/// Base URL for the personal ingtrak FastAPI backend.
///
/// Override at build/run time:
/// `flutter run --dart-define=API_BASE_URL=https://ingtrak.satoshinakamoto.win`
///
/// Local defaults (when override omitted):
/// - Android emulator → `http://10.0.2.2:8000`
/// - Physical device on LAN → `http://192.168.x.x:8000`
const String kApiBaseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'https://ingtrak.satoshinakamoto.win',
);
/// FastAPI bank backend. From `.env` / `--dart-define=API_BASE_URL=…`.
String get kApiBaseUrl => AppConfig.apiBaseUrl;
/// PocketBase server. From `.env` / `--dart-define=POCKETBASE_URL=…`.
String get kPocketBaseUrl => AppConfig.pocketBaseUrl;
/// PocketBase auth collection name (default `usersapp`).
String get kPocketBaseUsersCollection => AppConfig.pocketBaseUsersCollection;
/// Enable Banking redirect derived from [kApiBaseUrl].
/// Must match backend `REDIRECT_URI` and the Control Panel whitelist.
@@ -32,6 +30,7 @@ String get kEnableBankingRedirectUri {
: kApiBaseUrl;
return '$base/api/connect/callback';
}
/// Optional app-scheme bounce used after the http callback (not registered
/// with Enable Banking).
const String kBankRedirectScheme = 'ingtrak';
@@ -46,6 +45,9 @@ abstract final class StorageKeys {
static const txMetaLegacy = 'tx_meta_v1';
static const bankCache = 'bank_cache_v1';
static const importedTransactions = 'imported_txs_v1';
/// Serialized PocketBase auth token + user record (separate from tx data).
static const pocketBaseAuth = 'pb_auth_v1';
}
/// Named routes registered on [MaterialApp].
+80
View File
@@ -2,9 +2,88 @@
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import '../screens/account_screen.dart';
import '../screens/login_screen.dart';
import '../screens/settings_screen.dart';
Widget authAppBarAction(BuildContext context) {
final auth = context.watch<AuthController>();
if (auth.isLoggedIn) {
return PopupMenuButton<_AccountMenuAction>(
tooltip: auth.displayName,
offset: const Offset(0, 48),
onSelected: (action) async {
switch (action) {
case _AccountMenuAction.account:
await Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const AccountScreen()),
);
case _AccountMenuAction.logout:
await auth.logout();
}
},
itemBuilder: (context) {
final email = auth.email;
return [
if (email != null)
PopupMenuItem<_AccountMenuAction>(
enabled: false,
child: Text(
email,
style: Theme.of(context).textTheme.bodySmall,
),
),
const PopupMenuItem<_AccountMenuAction>(
value: _AccountMenuAction.account,
child: ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(Icons.person_outline),
title: Text('Account'),
),
),
const PopupMenuItem<_AccountMenuAction>(
value: _AccountMenuAction.logout,
child: ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(Icons.logout),
title: Text('Sign out'),
),
),
];
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: CircleAvatar(
radius: 16,
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Text(
auth.initials,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
),
);
}
return IconButton(
tooltip: 'Sign in',
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<bool>(builder: (_) => const LoginScreen()),
);
},
icon: const Icon(Icons.person_outline),
);
}
enum _AccountMenuAction { account, logout }
List<Widget> bankAppBarActions({
required BuildContext context,
VoidCallback? onRefresh,
@@ -17,6 +96,7 @@ List<Widget> bankAppBarActions({
onPressed: refreshing ? null : onRefresh,
icon: const Icon(Icons.refresh),
),
authAppBarAction(context),
IconButton(
tooltip: 'Settings',
onPressed: () {