sign in with pocketbase working
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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].
|
||||
|
||||
@@ -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: () {
|
||||
|
||||
Reference in New Issue
Block a user