diff --git a/mobile/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/mobile/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
index 5c9d77fc1..ba9a79390 100644
--- a/mobile/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
+++ b/mobile/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
@@ -15,6 +15,11 @@ import io.flutter.embedding.engine.FlutterEngine;
public final class GeneratedPluginRegistrant {
private static final String TAG = "GeneratedPluginRegistrant";
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
+ try {
+ flutterEngine.getPlugins().add(new com.llfbandit.app_links.AppLinksPlugin());
+ } catch (Exception e) {
+ Log.e(TAG, "Error registering plugin app_links, com.llfbandit.app_links.AppLinksPlugin", e);
+ }
try {
flutterEngine.getPlugins().add(new dev.fluttercommunity.plus.connectivity.ConnectivityPlugin());
} catch (Exception e) {
@@ -40,5 +45,10 @@ public final class GeneratedPluginRegistrant {
} catch (Exception e) {
Log.e(TAG, "Error registering plugin sqflite_android, com.tekartik.sqflite.SqflitePlugin", e);
}
+ try {
+ flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
+ } catch (Exception e) {
+ Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
+ }
}
}
diff --git a/mobile/assets/images/google_g_logo.svg b/mobile/assets/images/google_g_logo.svg
new file mode 100644
index 000000000..d733a534d
--- /dev/null
+++ b/mobile/assets/images/google_g_logo.svg
@@ -0,0 +1,7 @@
+
diff --git a/mobile/assets/images/logomark.svg b/mobile/assets/images/logomark.svg
new file mode 100644
index 000000000..80e043546
--- /dev/null
+++ b/mobile/assets/images/logomark.svg
@@ -0,0 +1,6 @@
+
diff --git a/mobile/ios/Runner/GeneratedPluginRegistrant.m b/mobile/ios/Runner/GeneratedPluginRegistrant.m
index f59edcc75..5705b470d 100644
--- a/mobile/ios/Runner/GeneratedPluginRegistrant.m
+++ b/mobile/ios/Runner/GeneratedPluginRegistrant.m
@@ -6,6 +6,12 @@
#import "GeneratedPluginRegistrant.h"
+#if __has_include()
+#import
+#else
+@import app_links;
+#endif
+
#if __has_include()
#import
#else
@@ -36,14 +42,22 @@
@import sqflite_darwin;
#endif
+#if __has_include()
+#import
+#else
+@import url_launcher_ios;
+#endif
+
@implementation GeneratedPluginRegistrant
+ (void)registerWithRegistry:(NSObject*)registry {
+ [AppLinksIosPlugin registerWithRegistrar:[registry registrarForPlugin:@"AppLinksIosPlugin"]];
[ConnectivityPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"ConnectivityPlusPlugin"]];
[FlutterSecureStorageDarwinPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterSecureStorageDarwinPlugin"]];
[PathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"PathProviderPlugin"]];
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
+ [URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
}
@end
diff --git a/mobile/lib/screens/backend_config_screen.dart b/mobile/lib/screens/backend_config_screen.dart
index 48bf3dc66..edc50d774 100644
--- a/mobile/lib/screens/backend_config_screen.dart
+++ b/mobile/lib/screens/backend_config_screen.dart
@@ -35,9 +35,13 @@ class _BackendConfigScreenState extends State {
Future _loadSavedUrl() async {
final prefs = await SharedPreferences.getInstance();
final savedUrl = prefs.getString('backend_url');
- if (mounted && savedUrl != null && savedUrl.isNotEmpty) {
+ final urlToShow = (savedUrl != null && savedUrl.isNotEmpty)
+ ? savedUrl
+ : ApiConfig.baseUrl;
+
+ if (mounted) {
setState(() {
- _urlController.text = savedUrl;
+ _urlController.text = urlToShow;
});
}
}
@@ -53,30 +57,37 @@ class _BackendConfigScreenState extends State {
try {
// Normalize base URL by removing trailing slashes
- final normalizedUrl = _urlController.text.trim().replaceAll(RegExp(r'/+$'), '');
+ final normalizedUrl = _urlController.text.trim().replaceAll(
+ RegExp(r'/+$'),
+ '',
+ );
// Check /sessions/new page to verify it's a Sure backend
final sessionsUrl = Uri.parse('$normalizedUrl/sessions/new');
- final sessionsResponse = await http.get(
- sessionsUrl,
- headers: {'Accept': 'text/html'},
- ).timeout(
- const Duration(seconds: 10),
- onTimeout: () {
- throw Exception('Connection timeout. Please check the URL and try again.');
- },
- );
+ final sessionsResponse = await http
+ .get(sessionsUrl, headers: {'Accept': 'text/html'})
+ .timeout(
+ const Duration(seconds: 10),
+ onTimeout: () {
+ throw Exception(
+ 'Connection timeout. Please check the URL and try again.',
+ );
+ },
+ );
- if (sessionsResponse.statusCode >= 200 && sessionsResponse.statusCode < 400) {
+ if (sessionsResponse.statusCode >= 200 &&
+ sessionsResponse.statusCode < 400) {
if (mounted) {
setState(() {
- _successMessage = 'Connection successful! Sure backend is reachable.';
+ _successMessage =
+ 'Connection successful!';
});
}
} else {
if (mounted) {
setState(() {
- _errorMessage = 'Server responded with status ${sessionsResponse.statusCode}. Please check if this is a Sure backend server.';
+ _errorMessage =
+ 'Server responded with status ${sessionsResponse.statusCode}. Please check if this is a Sure backend server.';
});
}
}
@@ -105,7 +116,10 @@ class _BackendConfigScreenState extends State {
try {
// Normalize base URL by removing trailing slashes
- final normalizedUrl = _urlController.text.trim().replaceAll(RegExp(r'/+$'), '');
+ final normalizedUrl = _urlController.text.trim().replaceAll(
+ RegExp(r'/+$'),
+ '',
+ );
// Save URL to SharedPreferences
final prefs = await SharedPreferences.getInstance();
@@ -141,7 +155,8 @@ class _BackendConfigScreenState extends State {
final trimmedValue = value.trim();
// Check if it starts with http:// or https://
- if (!trimmedValue.startsWith('http://') && !trimmedValue.startsWith('https://')) {
+ if (!trimmedValue.startsWith('http://') &&
+ !trimmedValue.startsWith('https://')) {
return 'URL must start with http:// or https://';
}
@@ -180,7 +195,7 @@ class _BackendConfigScreenState extends State {
),
const SizedBox(height: 16),
Text(
- 'Backend Configuration',
+ 'Configuration',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.primary,
@@ -189,7 +204,7 @@ class _BackendConfigScreenState extends State {
),
const SizedBox(height: 8),
Text(
- 'Enter your Sure Finance backend URL',
+ 'Update your Sure server URL',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
),
@@ -209,10 +224,7 @@ class _BackendConfigScreenState extends State {
children: [
Row(
children: [
- Icon(
- Icons.info_outline,
- color: colorScheme.primary,
- ),
+ Icon(Icons.info_outline, color: colorScheme.primary),
const SizedBox(width: 12),
Text(
'Example URLs',
@@ -225,7 +237,7 @@ class _BackendConfigScreenState extends State {
),
const SizedBox(height: 12),
Text(
- '• https://sure.lazyrhythm.com\n'
+ '• https://demo.sure.am\n'
'• https://your-domain.com\n'
'• http://localhost:3000',
style: TextStyle(
@@ -249,15 +261,14 @@ class _BackendConfigScreenState extends State {
),
child: Row(
children: [
- Icon(
- Icons.error_outline,
- color: colorScheme.error,
- ),
+ Icon(Icons.error_outline, color: colorScheme.error),
const SizedBox(width: 12),
Expanded(
child: Text(
_errorMessage!,
- style: TextStyle(color: colorScheme.onErrorContainer),
+ style: TextStyle(
+ color: colorScheme.onErrorContainer,
+ ),
),
),
IconButton(
@@ -316,9 +327,9 @@ class _BackendConfigScreenState extends State {
autocorrect: false,
textInputAction: TextInputAction.done,
decoration: const InputDecoration(
- labelText: 'Backend URL',
+ labelText: 'Sure server URL',
prefixIcon: Icon(Icons.cloud_outlined),
- hintText: 'https://sure.lazyrhythm.com',
+ hintText: 'https://app.sure.am',
),
validator: _validateUrl,
onFieldSubmitted: (_) => _saveAndContinue(),
diff --git a/mobile/lib/screens/login_screen.dart b/mobile/lib/screens/login_screen.dart
index f3089293a..4e2879048 100644
--- a/mobile/lib/screens/login_screen.dart
+++ b/mobile/lib/screens/login_screen.dart
@@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
+import 'package:flutter/gestures.dart';
import 'package:provider/provider.dart';
+import 'package:url_launcher/url_launcher.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import '../providers/auth_provider.dart';
import '../services/api_config.dart';
@@ -18,15 +21,37 @@ class _LoginScreenState extends State {
final _passwordController = TextEditingController();
final _otpController = TextEditingController();
bool _obscurePassword = true;
+ late final TapGestureRecognizer _signUpTapRecognizer;
+
+ @override
+ void initState() {
+ super.initState();
+ _signUpTapRecognizer = TapGestureRecognizer()..onTap = _openSignUpPage;
+ }
@override
void dispose() {
+ _signUpTapRecognizer.dispose();
_emailController.dispose();
_passwordController.dispose();
_otpController.dispose();
super.dispose();
}
+ Future _openSignUpPage() async {
+ final signUpUrl = Uri.parse('${ApiConfig.defaultBaseUrl}/registration/new');
+ final launched = await launchUrl(
+ signUpUrl,
+ mode: LaunchMode.externalApplication,
+ );
+
+ if (!launched && mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Unable to open sign up page')),
+ );
+ }
+ }
+
void _showApiKeyDialog() {
final apiKeyController = TextEditingController();
final outerContext = context;
@@ -44,9 +69,12 @@ class _LoginScreenState extends State {
children: [
Text(
'Enter your API key to sign in.',
- style: Theme.of(outerContext).textTheme.bodyMedium?.copyWith(
- color: Theme.of(outerContext).colorScheme.onSurfaceVariant,
- ),
+ style:
+ Theme.of(outerContext).textTheme.bodyMedium?.copyWith(
+ color: Theme.of(outerContext)
+ .colorScheme
+ .onSurfaceVariant,
+ ),
),
const SizedBox(height: 16),
TextField(
@@ -128,7 +156,8 @@ class _LoginScreenState extends State {
if (!_formKey.currentState!.validate()) return;
final authProvider = Provider.of(context, listen: false);
- final hadOtpCode = authProvider.showMfaInput && _otpController.text.isNotEmpty;
+ final hadOtpCode =
+ authProvider.showMfaInput && _otpController.text.isNotEmpty;
final success = await authProvider.login(
email: _emailController.text.trim(),
@@ -151,303 +180,321 @@ class _LoginScreenState extends State {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
- appBar: AppBar(
- title: const Text(''),
- actions: [
- IconButton(
- icon: const Icon(Icons.settings_outlined),
- tooltip: 'Backend Settings',
- onPressed: widget.onGoToSettings,
- ),
- ],
- ),
body: SafeArea(
- child: SingleChildScrollView(
- padding: const EdgeInsets.all(24),
- child: Form(
- key: _formKey,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- const SizedBox(height: 48),
- // Logo/Title
- Icon(
- Icons.account_balance_wallet,
- size: 80,
- color: colorScheme.primary,
- ),
- const SizedBox(height: 16),
- Text(
- 'Sure Finance',
- style: Theme.of(context).textTheme.headlineMedium?.copyWith(
- fontWeight: FontWeight.bold,
- color: colorScheme.primary,
- ),
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 8),
- Text(
- 'Sign in to manage your finances',
- style: Theme.of(context).textTheme.bodyLarge?.copyWith(
- color: colorScheme.onSurfaceVariant,
- ),
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 48),
-
- // Error Message
- Consumer(
- builder: (context, authProvider, _) {
- if (authProvider.errorMessage != null) {
- return Container(
- padding: const EdgeInsets.all(12),
- margin: const EdgeInsets.only(bottom: 16),
- decoration: BoxDecoration(
- color: colorScheme.errorContainer,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Row(
- children: [
- Icon(
- Icons.error_outline,
- color: colorScheme.error,
+ child: Stack(
+ children: [
+ SingleChildScrollView(
+ padding: const EdgeInsets.all(24),
+ child: Form(
+ key: _formKey,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ const SizedBox(height: 48),
+ // Logo/Title
+ SvgPicture.asset(
+ 'assets/images/logomark.svg',
+ width: 80,
+ height: 80,
+ ),
+ const SizedBox(height: 8),
+ Text.rich(
+ TextSpan(
+ style: Theme.of(context).textTheme.bodyLarge?.copyWith(
+ color: colorScheme.onSurfaceVariant,
),
- const SizedBox(width: 12),
- Expanded(
- child: Text(
- authProvider.errorMessage!,
- style: TextStyle(color: colorScheme.onErrorContainer),
- ),
- ),
- IconButton(
- icon: const Icon(Icons.close),
- onPressed: () => authProvider.clearError(),
- iconSize: 20,
- ),
- ],
- ),
- );
- }
- return const SizedBox.shrink();
- },
- ),
-
- // Email Field
- TextFormField(
- controller: _emailController,
- keyboardType: TextInputType.emailAddress,
- autocorrect: false,
- textInputAction: TextInputAction.next,
- decoration: const InputDecoration(
- labelText: 'Email',
- prefixIcon: Icon(Icons.email_outlined),
- ),
- validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter your email';
- }
- if (!value.contains('@')) {
- return 'Please enter a valid email';
- }
- return null;
- },
- ),
- const SizedBox(height: 16),
-
- // Password and OTP Fields with Consumer
- Consumer(
- builder: (context, authProvider, _) {
- final showOtp = authProvider.showMfaInput;
-
- return Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- // Password Field
- TextFormField(
- controller: _passwordController,
- obscureText: _obscurePassword,
- textInputAction: showOtp
- ? TextInputAction.next
- : TextInputAction.done,
- decoration: InputDecoration(
- labelText: 'Password',
- prefixIcon: const Icon(Icons.lock_outlined),
- suffixIcon: IconButton(
- icon: Icon(
- _obscurePassword
- ? Icons.visibility_outlined
- : Icons.visibility_off_outlined,
- ),
- onPressed: () {
- setState(() {
- _obscurePassword = !_obscurePassword;
- });
- },
+ children: [
+ const TextSpan(text: 'Please '),
+ TextSpan(
+ text: 'Sign Up',
+ style: TextStyle(
+ color: colorScheme.primary,
+ decoration: TextDecoration.underline,
+ fontWeight: FontWeight.w600,
),
+ recognizer: _signUpTapRecognizer,
),
- validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter your password';
- }
- return null;
- },
- onFieldSubmitted: showOtp ? null : (_) => _handleLogin(),
- ),
+ const TextSpan(text: ' first!'),
+ ],
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 48),
- // OTP Field (shown when MFA is required)
- if (showOtp) ...[
- const SizedBox(height: 16),
- Container(
+ // Error Message
+ Consumer(
+ builder: (context, authProvider, _) {
+ if (authProvider.errorMessage != null) {
+ return Container(
padding: const EdgeInsets.all(12),
+ margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
- color: colorScheme.primaryContainer.withValues(alpha: 0.3),
+ color: colorScheme.errorContainer,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(
- Icons.security,
- color: colorScheme.primary,
+ Icons.error_outline,
+ color: colorScheme.error,
),
const SizedBox(width: 12),
Expanded(
child: Text(
- 'Two-factor authentication is enabled. Enter your code.',
- style: TextStyle(color: colorScheme.onSurface),
+ authProvider.errorMessage!,
+ style: TextStyle(
+ color: colorScheme.onErrorContainer),
),
),
+ IconButton(
+ icon: const Icon(Icons.close),
+ onPressed: () => authProvider.clearError(),
+ iconSize: 20,
+ ),
],
),
- ),
- const SizedBox(height: 16),
- TextFormField(
- controller: _otpController,
- keyboardType: TextInputType.number,
- textInputAction: TextInputAction.done,
- decoration: const InputDecoration(
- labelText: 'Authentication Code',
- prefixIcon: Icon(Icons.pin_outlined),
+ );
+ }
+ return const SizedBox.shrink();
+ },
+ ),
+
+ // Email Field
+ TextFormField(
+ controller: _emailController,
+ keyboardType: TextInputType.emailAddress,
+ autocorrect: false,
+ textInputAction: TextInputAction.next,
+ decoration: const InputDecoration(
+ labelText: 'Email',
+ prefixIcon: Icon(Icons.email_outlined),
+ ),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return 'Please enter your email';
+ }
+ if (!value.contains('@')) {
+ return 'Please enter a valid email';
+ }
+ return null;
+ },
+ ),
+ const SizedBox(height: 16),
+
+ // Password and OTP Fields with Consumer
+ Consumer(
+ builder: (context, authProvider, _) {
+ final showOtp = authProvider.showMfaInput;
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ // Password Field
+ TextFormField(
+ controller: _passwordController,
+ obscureText: _obscurePassword,
+ textInputAction: showOtp
+ ? TextInputAction.next
+ : TextInputAction.done,
+ decoration: InputDecoration(
+ labelText: 'Password',
+ prefixIcon: const Icon(Icons.lock_outlined),
+ suffixIcon: IconButton(
+ icon: Icon(
+ _obscurePassword
+ ? Icons.visibility_outlined
+ : Icons.visibility_off_outlined,
+ ),
+ onPressed: () {
+ setState(() {
+ _obscurePassword = !_obscurePassword;
+ });
+ },
+ ),
+ ),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return 'Please enter your password';
+ }
+ return null;
+ },
+ onFieldSubmitted:
+ showOtp ? null : (_) => _handleLogin(),
),
- validator: (value) {
- if (showOtp && (value == null || value.isEmpty)) {
- return 'Please enter your authentication code';
- }
- return null;
- },
- onFieldSubmitted: (_) => _handleLogin(),
+
+ // OTP Field (shown when MFA is required)
+ if (showOtp) ...[
+ const SizedBox(height: 16),
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: colorScheme.primaryContainer
+ .withValues(alpha: 0.3),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ Icons.security,
+ color: colorScheme.primary,
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ 'Two-factor authentication is enabled. Enter your code.',
+ style: TextStyle(
+ color: colorScheme.onSurface),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+ TextFormField(
+ controller: _otpController,
+ keyboardType: TextInputType.number,
+ textInputAction: TextInputAction.done,
+ decoration: const InputDecoration(
+ labelText: 'Authentication Code',
+ prefixIcon: Icon(Icons.pin_outlined),
+ ),
+ validator: (value) {
+ if (showOtp &&
+ (value == null || value.isEmpty)) {
+ return 'Please enter your authentication code';
+ }
+ return null;
+ },
+ onFieldSubmitted: (_) => _handleLogin(),
+ ),
+ ],
+ ],
+ );
+ },
+ ),
+
+ const SizedBox(height: 24),
+
+ // Login Button
+ Consumer(
+ builder: (context, authProvider, _) {
+ return ElevatedButton(
+ onPressed:
+ authProvider.isLoading ? null : _handleLogin,
+ child: authProvider.isLoading
+ ? const SizedBox(
+ height: 20,
+ width: 20,
+ child:
+ CircularProgressIndicator(strokeWidth: 2),
+ )
+ : const Text('Sign In'),
+ );
+ },
+ ),
+
+ const SizedBox(height: 16),
+
+ // Divider with "or"
+ Row(
+ children: [
+ Expanded(
+ child: Divider(color: colorScheme.outlineVariant)),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Text(
+ 'or',
+ style:
+ TextStyle(color: colorScheme.onSurfaceVariant),
+ ),
+ ),
+ Expanded(
+ child: Divider(color: colorScheme.outlineVariant)),
+ ],
+ ),
+
+ const SizedBox(height: 16),
+
+ // Google Sign-In button
+ Consumer(
+ builder: (context, authProvider, _) {
+ return OutlinedButton.icon(
+ onPressed: authProvider.isLoading
+ ? null
+ : () =>
+ authProvider.startSsoLogin('google_oauth2'),
+ icon: SvgPicture.asset(
+ 'assets/images/google_g_logo.svg',
+ width: 18,
+ height: 18,
+ ),
+ label: const Text('Sign in with Google'),
+ style: OutlinedButton.styleFrom(
+ minimumSize: const Size(double.infinity, 50),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ );
+ },
+ ),
+
+ const SizedBox(height: 24),
+
+ // Backend URL info
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: colorScheme.surfaceContainerHighest
+ .withValues(alpha: 0.3),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ children: [
+ Text(
+ 'Sure server URL:',
+ style:
+ Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: colorScheme.onSurfaceVariant,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ ApiConfig.baseUrl,
+ style:
+ Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: colorScheme.primary,
+ fontFamily: 'monospace',
+ ),
+ textAlign: TextAlign.center,
),
],
- ],
- );
- },
- ),
-
- const SizedBox(height: 24),
-
- // Login Button
- Consumer(
- builder: (context, authProvider, _) {
- return ElevatedButton(
- onPressed: authProvider.isLoading ? null : _handleLogin,
- child: authProvider.isLoading
- ? const SizedBox(
- height: 20,
- width: 20,
- child: CircularProgressIndicator(strokeWidth: 2),
- )
- : const Text('Sign In'),
- );
- },
- ),
-
- const SizedBox(height: 12),
-
- // API Key Login Button
- TextButton.icon(
- onPressed: _showApiKeyDialog,
- icon: const Icon(Icons.vpn_key_outlined, size: 18),
- label: const Text('Via API Key Login'),
- ),
-
- const SizedBox(height: 16),
-
- // Divider with "or"
- Row(
- children: [
- Expanded(child: Divider(color: colorScheme.outlineVariant)),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Text(
- 'or',
- style: TextStyle(color: colorScheme.onSurfaceVariant),
),
),
- Expanded(child: Divider(color: colorScheme.outlineVariant)),
+
+ const SizedBox(height: 12),
+
+ // API Key Login Button
+ TextButton.icon(
+ onPressed: _showApiKeyDialog,
+ icon: const Icon(Icons.vpn_key_outlined, size: 18),
+ label: const Text('API-Key Login'),
+ ),
],
),
-
- const SizedBox(height: 16),
-
- // Google Sign-In button
- Consumer(
- builder: (context, authProvider, _) {
- return OutlinedButton.icon(
- onPressed: authProvider.isLoading
- ? null
- : () => authProvider.startSsoLogin('google_oauth2'),
- icon: const Icon(Icons.g_mobiledata, size: 24),
- label: const Text('Sign in with Google'),
- style: OutlinedButton.styleFrom(
- minimumSize: const Size(double.infinity, 50),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- ),
- );
- },
- ),
-
- const SizedBox(height: 24),
-
- // Backend URL info
- Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Column(
- children: [
- Text(
- 'Backend URL:',
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
- color: colorScheme.onSurfaceVariant,
- fontWeight: FontWeight.bold,
- ),
- ),
- const SizedBox(height: 4),
- Text(
- ApiConfig.baseUrl,
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
- color: colorScheme.primary,
- fontFamily: 'monospace',
- ),
- textAlign: TextAlign.center,
- ),
- ],
- ),
- ),
- const SizedBox(height: 8),
- Text(
- 'Connect to your Sure Finance server to manage your accounts.',
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
- color: colorScheme.onSurfaceVariant,
- ),
- textAlign: TextAlign.center,
- ),
- ],
+ ),
),
- ),
+ Positioned(
+ right: 8,
+ top: 8,
+ child: IconButton(
+ icon: const Icon(Icons.settings_outlined),
+ tooltip: 'Backend Settings',
+ onPressed: widget.onGoToSettings,
+ ),
+ ),
+ ],
),
),
);
diff --git a/mobile/lib/services/api_config.dart b/mobile/lib/services/api_config.dart
index bb5f353b0..441618a7c 100644
--- a/mobile/lib/services/api_config.dart
+++ b/mobile/lib/services/api_config.dart
@@ -5,9 +5,12 @@ class ApiConfig {
// For local development, use: http://10.0.2.2:3000 (Android emulator)
// For iOS simulator, use: http://localhost:3000
// For production, use your actual server URL
- static String _baseUrl = 'https://app.sure.am';
+ static const String _defaultBaseUrl = 'https://demo.sure.am';
+ static const String _backendUrlKey = 'backend_url';
+ static String _baseUrl = _defaultBaseUrl;
static String get baseUrl => _baseUrl;
+ static String get defaultBaseUrl => _defaultBaseUrl;
static void setBaseUrl(String url) {
_baseUrl = url;
@@ -34,32 +37,32 @@ class ApiConfig {
/// In token mode, uses Authorization: Bearer header.
static Map getAuthHeaders(String token) {
if (_isApiKeyAuth && _apiKeyValue != null) {
- return {
- 'X-Api-Key': _apiKeyValue!,
- 'Accept': 'application/json',
- };
+ return {'X-Api-Key': _apiKeyValue!, 'Accept': 'application/json'};
}
- return {
- 'Authorization': 'Bearer $token',
- 'Accept': 'application/json',
- };
+ return {'Authorization': 'Bearer $token', 'Accept': 'application/json'};
}
/// Initialize the API configuration by loading the backend URL from storage
- /// Returns true if a saved URL was loaded, false otherwise
+ /// Returns true when a backend URL is configured (stored or default)
static Future initialize() async {
try {
final prefs = await SharedPreferences.getInstance();
- final savedUrl = prefs.getString('backend_url');
+ final savedUrl = prefs.getString(_backendUrlKey);
if (savedUrl != null && savedUrl.isNotEmpty) {
_baseUrl = savedUrl;
return true;
}
- return false;
+
+ // Seed first launch with the active development backend so the app can
+ // go straight to login while still letting users override it later.
+ _baseUrl = _defaultBaseUrl;
+ await prefs.setString(_backendUrlKey, _defaultBaseUrl);
+ return true;
} catch (e) {
// If initialization fails, keep the default URL
- return false;
+ _baseUrl = _defaultBaseUrl;
+ return true;
}
}
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index f1820a9de..fd516dab1 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -1,6 +1,38 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
+ app_links:
+ dependency: "direct main"
+ description:
+ name: app_links
+ sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.4.1"
+ app_links_linux:
+ dependency: transitive
+ description:
+ name: app_links_linux
+ sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.3"
+ app_links_platform_interface:
+ dependency: transitive
+ description:
+ name: app_links_platform_interface
+ sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.2"
+ app_links_web:
+ dependency: transitive
+ description:
+ name: app_links_web
+ sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
archive:
dependency: transitive
description:
@@ -214,6 +246,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
+ flutter_svg:
+ dependency: "direct main"
+ description:
+ name: flutter_svg
+ sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.3"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -224,6 +264,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ gtk:
+ dependency: transitive
+ description:
+ name: gtk
+ sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
http:
dependency: "direct main"
description:
@@ -344,6 +392,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
+ path_parsing:
+ dependency: transitive
+ description:
+ name: path_parsing
+ sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
path_provider:
dependency: transitive
description:
@@ -597,6 +653,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
+ url_launcher:
+ dependency: "direct main"
+ description:
+ name: url_launcher
+ sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.2"
+ url_launcher_android:
+ dependency: transitive
+ description:
+ name: url_launcher_android
+ sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.28"
+ url_launcher_ios:
+ dependency: transitive
+ description:
+ name: url_launcher_ios
+ sha256: b1aca26728b7cc7a3af971bb6f601554a8ae9df2e0a006de8450ba06a17ad36a
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.4.0"
+ url_launcher_linux:
+ dependency: transitive
+ description:
+ name: url_launcher_linux
+ sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.2"
+ url_launcher_macos:
+ dependency: transitive
+ description:
+ name: url_launcher_macos
+ sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.5"
+ url_launcher_platform_interface:
+ dependency: transitive
+ description:
+ name: url_launcher_platform_interface
+ sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.2"
+ url_launcher_web:
+ dependency: transitive
+ description:
+ name: url_launcher_web
+ sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.2"
+ url_launcher_windows:
+ dependency: transitive
+ description:
+ name: url_launcher_windows
+ sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.5"
uuid:
dependency: "direct main"
description:
@@ -605,6 +725,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.2"
+ vector_graphics:
+ dependency: transitive
+ description:
+ name: vector_graphics
+ sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.19"
+ vector_graphics_codec:
+ dependency: transitive
+ description:
+ name: vector_graphics_codec
+ sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.13"
+ vector_graphics_compiler:
+ dependency: transitive
+ description:
+ name: vector_graphics_compiler
+ sha256: "201e876b5d52753626af64b6359cd13ac6011b80728731428fd34bc840f71c9b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.20"
vector_math:
dependency: transitive
description:
@@ -662,5 +806,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
- dart: ">=3.9.0 <4.0.0"
- flutter: ">=3.29.0"
+ dart: ">=3.10.0 <4.0.0"
+ flutter: ">=3.38.0"
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index 1720033ce..24c23e10e 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -22,6 +22,7 @@ dependencies:
uuid: ^4.5.2
app_links: ^6.4.0
url_launcher: ^6.2.5
+ flutter_svg: ^2.2.0
dev_dependencies:
flutter_test:
@@ -33,6 +34,7 @@ flutter:
uses-material-design: true
assets:
- assets/icon/
+ - assets/images/
flutter_launcher_icons:
android: true