Files
sure/mobile/lib/models/account.dart
Lazy Bone 7866598057 Mobile native client via Flutter (#426)
* feat: mobile support.

Basic functionality development includes adding and deleting transactions,viewing balances,

* Fix mobile support issues in PR #426

This commit addresses the critical issues identified in the mobile-support PR:

1. **GitHub Actions Workflow Path Issues (Critical)**
   - Add mobile/ prefix to all path filters in flutter-build.yml
   - Add working-directory to all Flutter commands
   - Fix Android keystore and iOS CocoaPods paths
   - Fix artifact upload paths

2. **Error Handling Improvements**
   - Add try-catch blocks to all HTTP requests in services
   - Wrap all JSON parsing operations in error handling
   - Add proper error messages for network failures

3. **HTTP Request Timeout Configuration**
   - Add 30-second timeout to all HTTP requests
   - Prevents hanging on network failures

4. **Defensive Null Checks in Providers**
   - Add containsKey() checks before accessing result maps
   - Add proper type casting with null safety
   - Add fallback error messages

These changes ensure the workflow triggers correctly on mobile/ directory
changes and improves overall code robustness.

* Fix transactions exposure and error handling issues

- Add UnmodifiableListView to transactions getter to prevent external mutation
- Call notifyListeners() immediately after setting _isLoading = false
- Move jsonDecode to run only after successful statusCode verification
- Replace string concatenation with Uri.replace() for proper URL encoding
- Add try/catch for jsonDecode on non-2xx responses to handle non-JSON errors

* Fix exception handling and duplicate parsing in auth_service.dart

- Replace broad catch-all exception handlers with targeted exception handling
- Add specific catches for SocketException, TimeoutException, HttpException, FormatException, and TypeError
- Return safe, user-friendly error messages instead of exposing internal details
- Log full exception details and stack traces using debugPrint for debugging
- Fix duplicate User.fromJson calls in login and signup methods by parsing once and reusing the instance
- Improve code efficiency and security by preventing information leakage

* Fix 2FA login crash and improve UX

Fixed the crash that occurred when logging in with 2FA-enabled accounts
and improved the user experience by not showing error messages when MFA
is required (it's a normal flow, not an error).

Changes:
- Added mounted check before setState() in login screen
- Modified AuthProvider to not set error message when MFA is required
- Ensures smooth transition from password entry to OTP entry
- Prevents "setState() called after dispose()" error

The flow now works correctly:
1. User enters email/password → clicks Sign In
2. Backend responds with mfa_required
3. OTP input field appears with friendly blue prompt (no red error)
4. User enters 6-digit code → clicks Sign In again
5. Login succeeds

* Add debug logs to trace 2FA login flow

Added comprehensive debug logging to understand why OTP field
is not showing when MFA is required:
- Log backend response status and body
- Log login result in AuthProvider
- Log MFA required state
- Log when OTP field should be shown

This will help identify if the issue is:
1. Backend not returning mfa_required flag
2. Response parsing issue
3. State management issue
4. UI rendering issue

* Fix 2FA login flow by moving MFA state to AuthProvider

PROBLEM:
The LoginScreen was being recreated when AuthProvider called notifyListeners(),
causing all internal state (_showOtpField) to be lost. This resulted in the
OTP input field never appearing, making 2FA login impossible.

ROOT CAUSE:
The AppWrapper uses a Consumer<AuthProvider> that rebuilds the entire widget
tree when auth state changes. When login() sets isLoading=false and calls
notifyListeners(), a brand new LoginScreen instance is created, resetting
all internal state.

SOLUTION:
- Moved _showMfaInput state from LoginScreen to AuthProvider
- AuthProvider now manages when to show the MFA input field
- LoginScreen uses Consumer to read this state reactively
- State survives widget rebuilds

FLOW:
1. User enters email/password → clicks Sign In
2. Backend responds with mfa_required: true
3. AuthProvider sets _showMfaInput = true
4. Consumer rebuilds, showing OTP field (state preserved)
5. User enters code → clicks Sign In
6. Backend validates → returns tokens → login succeeds

Backend is confirmed working via tests (auth_controller_test.rb).

* Fix mobile 2FA login requiring double password entry

Problem:
When 2FA is required during mobile login, the LoginScreen was being
destroyed and recreated, causing text controllers to reset and forcing
users to re-enter their credentials.

Root cause:
AppWrapper was checking authProvider.isLoading and showing a full-screen
loading indicator during login attempts. This caused LoginScreen to be
unmounted when isLoading=true, destroying the State and text controllers.
When the backend returned mfa_required, isLoading=false triggered
recreation of LoginScreen with empty fields.

Solution:
- Add isInitializing state to AuthProvider to distinguish initial auth
  check from active login attempts
- Update AppWrapper to only show loading spinner during isInitializing,
  not during login flow
- LoginScreen now persists across login attempts, preserving entered
  credentials

Flow after fix:
1. User enters email/password
2. LoginScreen stays mounted (shows loading in button only)
3. Backend returns mfa_required
4. MFA field appears, email/password fields retain values
5. User enters OTP and submits (email/password automatically included)

Files changed:
- mobile/lib/providers/auth_provider.dart: Add isInitializing state
- mobile/lib/main.dart: Use isInitializing instead of isLoading in AppWrapper

* Add OTP error feedback for mobile 2FA login

When users enter an incorrect OTP code during 2FA login, the app now:
- Displays an error message indicating the code was invalid
- Keeps the MFA input field visible for retry
- Automatically clears the OTP field for easy re-entry

Changes:
- mobile/lib/providers/auth_provider.dart:
  * Distinguish between first MFA request vs invalid OTP error
  * Show error message when OTP code was submitted but invalid
  * Keep MFA input visible when in MFA flow with errors

- mobile/lib/screens/login_screen.dart:
  * Clear OTP field after failed login attempt
  * Improve UX by allowing easy retry without re-entering credentials

User flow after fix:
1. User enters email/password
2. MFA required - OTP field appears
3. User enters wrong OTP
4. Error message shows "Two-factor authentication required"
5. OTP field clears, ready for new code
6. User can immediately retry without re-entering email/password

* Improve OTP error message clarity

When user enters an invalid OTP code, show clearer error message
"Invalid authentication code. Please try again." instead of the
confusing "Two-factor authentication required" from backend.

This makes it clear that the OTP was wrong, not that they need to
start the 2FA process.

* chore: delete generation ai create test flow md.

* Update mobile/lib/screens/login_screen.dart

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>

* feat: add pubspec.lock file.

* Linter

* Update mobile/android/app/build.gradle

Co-authored-by: Pedro Piñera Buendía <663605+pepicrft@users.noreply.github.com>
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>

* Update mobile/android/app/build.gradle

com.sure.mobile -> am.sure.mobile

Co-authored-by: Pedro Piñera Buendía <663605+pepicrft@users.noreply.github.com>
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>

* Update mobile/ios/Runner.xcodeproj/project.pbxproj

Co-authored-by: Pedro Piñera Buendía <663605+pepicrft@users.noreply.github.com>
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>

* Update mobile/ios/Runner.xcodeproj/project.pbxproj

Co-authored-by: Pedro Piñera Buendía <663605+pepicrft@users.noreply.github.com>
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>

* Update mobile/ios/Runner.xcodeproj/project.pbxproj

Co-authored-by: Pedro Piñera Buendía <663605+pepicrft@users.noreply.github.com>
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>

* Fix iOS deployment target and update documentation

- Update iOS minimum deployment target from 12.0 to 13.0 in Podfile for Flutter compatibility
- Translate SIGNING_SETUP.md from Chinese to English for better accessibility
- Remove TECHNICAL_GUIDE.md as requested

* Restore TECHNICAL_GUIDE.md with partial content removal

- Restore mobile/docs/TECHNICAL_GUIDE.md (previously deleted)
- Remove only License, Contributing, and Related Links sections (from line 445 onwards)
- Keep all technical documentation content (lines 1-444)

* Fix setState after dispose errors across mobile app

This commit fixes 5 critical setState/dispose errors identified by Cursor:

1. backend_config_screen.dart: Add mounted checks in _testConnection()
   and _saveAndContinue() methods to prevent setState calls after async
   operations (http.get, SharedPreferences) when widget is disposed.

2. transaction_form_screen.dart: Add mounted check in _selectDate()
   after showDatePicker to prevent setState when modal is dismissed
   while date picker is open.

3. main.dart: Add mounted check in _checkBackendConfig() after
   ApiConfig.initialize() to handle disposal during async initialization.

4. transactions_list_screen.dart: Add mounted check in the .then()
   callback of _showAddTransactionForm() to prevent calling
   _loadTransactions() on a disposed widget when modal is closed.

5. transactions_provider.dart: Fix premature notifyListeners() by
   removing intermediate notification after _isLoading = false,
   ensuring listeners only get notified once with complete state updates
   to prevent momentary stale UI state.

All setState calls after async operations now properly check mounted
status to prevent "setState() called after dispose()" errors.

* Fix Android build: Remove package attribute from AndroidManifest.xml

Remove deprecated package attribute from AndroidManifest.xml. The namespace
is now correctly defined only in build.gradle as required by newer versions
of Android Gradle Plugin.

This fixes the build error:
"Incorrect package="com.sure.mobile" found in source AndroidManifest.xml.
Setting the namespace via the package attribute in the source
AndroidManifest.xml is no longer supported."

* Update issue templates

* Change package name from com.sure.mobile to am.sure.mobile

Updated Android package name across all files:
- build.gradle: namespace and applicationId
- MainActivity.kt: package declaration and file path
- Moved MainActivity.kt from com/sure/mobile to am/sure/mobile

This aligns with the package name change made in the mobile-support branch
and fixes app crashes caused by package name mismatch.

* Fix mobile app code quality issues

- Add mounted check in backend_config_screen.dart to prevent setState after dispose
- Translate Chinese comments to English in transactions_list_screen.dart for better maintainability
- Replace brittle string-split date conversion with DateFormat in transaction_form_screen.dart for safer date handling

These changes address code review feedback and improve code robustness.

* Remove feature request template

Delete unused feature request issue template file.

* Fix mobile app code quality issues

- Fix URL construction in backend_config_screen.dart to prevent double slashes by normalizing base URL (removing trailing slashes) before appending paths
- Update pubspec.yaml to require Flutter 3.27.0+ for withValues API compatibility
- Improve amount parsing robustness in transactions_list_screen.dart with proper locale handling, sign detection, and fallback error handling
- Fix dismissible delete handler to prevent UI/backend inconsistency by moving deletion to confirmDismiss and only allowing dismissal on success

* Fix mobile app performance and security issues

- Eliminate duplicate _getAmountDisplayInfo calls in transactions list by computing display info once per transaction item
- Upgrade flutter_secure_storage from 9.0.0 to 10.0.0 for AES-GCM encryption
- Update dev dependencies: flutter_lints to 6.0.0 and flutter_launcher_icons to 0.14.4

* Update Android SDK requirements for flutter_secure_storage v10

- Increase compileSdk from 35 to 36
- Increase minSdkVersion from 21 to 24

This is required by flutter_secure_storage v10+ which uses newer Android APIs for AES-GCM encryption.

* Fix transaction deletion message not displaying properly

The success message was being shown in the onDismissed callback, which
executes after the dismissal animation completes. By that time, the
context may have become invalid due to widget tree rebuilds, causing
the SnackBar to not display.

Moved the success message to the confirmDismiss callback where we
already have a captured scaffoldMessenger reference, ensuring the
message displays reliably before the dismissal animation begins.

* Add mounted check before showing SnackBar after async operation

* Update mobile/android/app/build.gradle

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>

* Fix empty state refresh and auth error feedback in mobile transactions screen

- Wrap empty state in RefreshIndicator with CustomScrollView to enable pull-to-refresh when no transactions exist
- Wrap error state in RefreshIndicator as well for consistency
- Add SnackBar feedback when auth token is null in _loadTransactions instead of silent failure
- Ensure mounted check before showing SnackBar to prevent errors after widget disposal

* Fix flash of 'No accounts yet' page on app startup

Added initialization state tracking to AccountsProvider to prevent
the empty state from briefly showing while accounts are being loaded
for the first time.

Changes:
- Add _isInitializing flag to AccountsProvider (starts as true)
- Set to false after first fetchAccounts() completes
- Reset to true when clearAccounts() is called
- Update DashboardScreen to show loading during initialization

This ensures a smooth user experience without visual flashing on app launch.

* Refactor: Extract transaction deletion logic into dedicated method

Improved code readability by extracting the 67-line confirmDismiss
callback into a separate _confirmAndDeleteTransaction method.

Changes:
- Add Transaction model import
- Create _confirmAndDeleteTransaction method that handles:
  - Confirmation dialog
  - Token retrieval
  - Deletion API call
  - Success/failure feedback
- Simplify confirmDismiss to single line calling new method

This separation of concerns makes the code more maintainable and
the Dismissible widget configuration more concise.

* Enhance Flutter build workflow with keystore checks

Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>

* Implement conditional signing configuration

Added a check for keystore properties before configuring signing.

Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>

---------

Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>
Co-authored-by: dwvwdv <dwvwdv@protonmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Pedro Piñera Buendía <663605+pepicrft@users.noreply.github.com>
2026-01-08 11:27:31 +01:00

67 lines
1.6 KiB
Dart

class Account {
final String id;
final String name;
final String balance;
final String currency;
final String? classification;
final String accountType;
Account({
required this.id,
required this.name,
required this.balance,
required this.currency,
this.classification,
required this.accountType,
});
factory Account.fromJson(Map<String, dynamic> json) {
return Account(
id: json['id'].toString(),
name: json['name'] as String,
balance: json['balance'] as String,
currency: json['currency'] as String,
classification: json['classification'] as String?,
accountType: json['account_type'] as String,
);
}
bool get isAsset => classification == 'asset';
bool get isLiability => classification == 'liability';
double get balanceAsDouble {
try {
// Remove commas and any other non-numeric characters except dots and minus signs
final cleanedBalance = balance.replaceAll(RegExp(r'[^\d.-]'), '');
return double.parse(cleanedBalance);
} catch (e) {
return 0.0;
}
}
String get displayAccountType {
switch (accountType) {
case 'depository':
return 'Bank Account';
case 'credit_card':
return 'Credit Card';
case 'investment':
return 'Investment';
case 'loan':
return 'Loan';
case 'property':
return 'Property';
case 'vehicle':
return 'Vehicle';
case 'crypto':
return 'Crypto';
case 'other_asset':
return 'Other Asset';
case 'other_liability':
return 'Other Liability';
default:
return accountType;
}
}
}