Skip to content

Conversation

@dwvwdv
Copy link

@dwvwdv dwvwdv commented Dec 8, 2025

#411 After the discussion, I did some conflict resolution and merging work, moving the project to the /mobile folder.
@jjmata Currently, it needs to be configured with the environment variables required for Android signing.
#241 lightweight alternative to the current PWA implementation.

I'm not sure if any information is missing.
If there is anything missing, please point it out to me. Thank you.

Summary by CodeRabbit

  • New Features

    • Flutter mobile app: onboarding/backend config, login with MFA, account dashboard, transaction creation/listing, and device-aware session handling.
  • CI/CD

    • Automated build workflow for Android (APK/AAB) and unsigned iOS artifacts.
  • Documentation

    • Guides for project setup, iOS build, and Android signing/CI configuration.

✏️ Tip: You can customize this high-level summary in your review settings.

Basic functionality development includes adding and deleting transactions,viewing balances,
@coderabbitai
Copy link

coderabbitai bot commented Dec 8, 2025

Walkthrough

Adds a complete Flutter mobile client: Android and iOS platform projects, Flutter app code (models, services, providers, screens, widgets), CI workflow for builds/tests, documentation, and project configuration including signing setup and assets.

Changes

Cohort / File(s) Summary
CI/CD Workflow
.github/workflows/flutter-build.yml
New GitHub Actions workflow to build Android (APK/AAB) and unsigned iOS artifacts; sets up Java/Flutter, runs pub get, analysis, tests, builds artifacts, and uploads them.
Android Gradle & App
mobile/android/...
mobile/android/build.gradle, mobile/android/settings.gradle, mobile/android/gradle.properties, mobile/android/gradle/wrapper/gradle-wrapper.properties, mobile/android/app/build.gradle, mobile/android/app/src/main/AndroidManifest.xml, mobile/android/app/src/main/kotlin/.../MainActivity.kt, mobile/android/app/src/main/res/...
Full Android project added: Gradle scripts and wrapper (Gradle 8.7), plugin versions, signing config, manifest (permissions, deep link), MainActivity, styles, and launch drawable.
iOS Project & Config
mobile/ios/...
mobile/ios/Flutter/AppFrameworkInfo.plist, mobile/ios/Flutter/Debug.xcconfig, mobile/ios/Flutter/Release.xcconfig, mobile/ios/Podfile, mobile/ios/Runner.xcodeproj/..., mobile/ios/Runner/AppDelegate.swift, mobile/ios/Runner/Runner-Bridging-Header.h, mobile/ios/Runner/GeneratedPluginRegistrant.{h,m}, mobile/ios/Runner/Info.plist, mobile/ios/Runner/Base.lproj/..., mobile/ios/Runner/Assets.xcassets/...
Complete iOS Runner project and Flutter integration files, XCConfigs, Podfile, project.pbxproj, app delegate, bridging header, plugin registrant, storyboards, assets, and Info.plist.
Flutter App Entry & Config
mobile/lib/main.dart, mobile/pubspec.yaml, mobile/analysis_options.yaml
New app entry, routing, MultiProvider setup; pubspec with dependencies (http, provider, shared_preferences, flutter_secure_storage, intl), assets, and lint rules.
Models
mobile/lib/models/*
Adds Account, AuthTokens (with expiry logic), Transaction, and User models with JSON (de)serialization and computed properties.
Providers (State)
mobile/lib/providers/*
Adds AuthProvider (login/signup/logout, token refresh, MFA handling), AccountsProvider (fetch, sort, pagination, summaries), and TransactionsProvider (fetch, delete single/multiple).
Services (API & Device)
mobile/lib/services/*
Adds ApiConfig (base URL persistence), AuthService (login/signup/refresh, secure storage), AccountsService, TransactionsService (create/get/delete/bulk), and DeviceService (device metadata & ID persistence).
UI Screens
mobile/lib/screens/*
Adds BackendConfigScreen (backend URL test/save), LoginScreen (MFA support), DashboardScreen (assets/liabilities summary), TransactionsListScreen (list, multi-delete), TransactionFormScreen (create transaction).
Widgets
mobile/lib/widgets/account_card.dart
AccountCard widget for account list items with optional swipe action.
Documentation & Guides
mobile/README.md, mobile/docs/TECHNICAL_GUIDE.md, mobile/docs/SIGNING_SETUP.md, mobile/docs/iOS_BUILD.md
Project README and detailed guides for technical architecture, Android signing, and iOS build/troubleshooting.
Project Files & Assets
mobile/.gitignore, mobile/assets/images/.gitkeep
Gitignore for Flutter/Android/iOS artifacts and placeholder for image assets.
Tests
mobile/test/widget_test.dart
Basic widget test scaffold.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App as Flutter App
    participant AuthProv as AuthProvider
    participant AuthSvc as AuthService
    participant Backend as Backend API
    participant Storage as SecureStorage

    User->>App: Submit credentials
    App->>AuthProv: login(email,password)
    AuthProv->>AuthSvc: login(...)
    AuthSvc->>Backend: POST /login
    Backend-->>AuthSvc: { access_token, refresh_token, user } or { mfa_required }
    alt MFA required
        AuthSvc-->>AuthProv: mfa_required
        AuthProv-->>App: show OTP input
        User->>App: Submit OTP
        App->>AuthProv: login(..., otp)
        AuthProv->>AuthSvc: login(..., otp)
        AuthSvc->>Backend: POST /login (with otp)
        Backend-->>AuthSvc: tokens + user
    end
    AuthSvc->>Storage: save tokens & user
    AuthSvc-->>AuthProv: success + user
    AuthProv->>App: notifyListeners (authenticated)
    App->>App: Navigate to Dashboard
Loading
sequenceDiagram
    participant Dashboard as DashboardScreen
    participant AuthProv as AuthProvider
    participant Storage as SecureStorage
    participant AccountsProv as AccountsProvider
    participant AccountsSvc as AccountsService
    participant Backend as Backend API

    Dashboard->>AuthProv: request valid access token
    AuthProv->>Storage: read tokens
    Storage-->>AuthProv: tokens (maybe expired)
    alt token expired
        AuthProv->>AuthSvc: refreshToken
        AuthSvc->>Backend: POST /refresh
        Backend-->>AuthSvc: new access_token
        AuthSvc->>Storage: save tokens
    end
    AuthProv-->>Dashboard: accessToken
    Dashboard->>AccountsProv: fetchAccounts(accessToken)
    AccountsProv->>AccountsSvc: getAccounts(accessToken, page)
    AccountsSvc->>Backend: GET /accounts...
    Backend-->>AccountsSvc: accounts + pagination
    AccountsSvc-->>AccountsProv: accounts
    AccountsProv->>Dashboard: notifyListeners (accounts ready)
    Dashboard->>Dashboard: render UI summaries
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

  • Files/areas needing focused review:
    • mobile/ios/Runner.xcodeproj/project.pbxproj — large autogenerated Xcode project; verify paths, build settings, and signing placeholders.
    • mobile/lib/providers/auth_provider.dart & mobile/lib/services/auth_service.dart — token lifecycle, refresh logic, MFA and secure storage interactions.
    • mobile/lib/services/transactions_service.dart — varied response shapes and bulk-delete concurrency/error aggregation.
    • mobile/lib/screens/dashboard_screen.dart, transaction_form_screen.dart, transactions_list_screen.dart — complex UI state, navigation and session-expiry handling.
    • CI workflow .github/workflows/flutter-build.yml and Android signing configs — secret handling and artifact upload steps.

Poem

🐰
I dug a branch of code so neat,
With fluttered paws and tiny feet,
Auths and ledgers, screens that gleam,
CI hums like a midnight stream,
Hop, build, test — the mobile dream!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Mobile native client via Flutter' clearly and accurately describes the main change: introducing a complete Flutter-based mobile application. It is concise, specific, and reflects the primary objective of the PR.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4261e7a and 6c64511.

⛔ Files ignored due to path filters (1)
  • mobile/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • mobile/.gitignore (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • mobile/.gitignore

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (13)
mobile/.gitignore-11-11 (1)

11-11: Remove pubspec.lock from .gitignore.

For Flutter applications, pubspec.lock should be committed to version control to ensure reproducible builds and consistent dependency resolution across all environments and team members.

Apply this diff:

-.metadata
-pubspec.lock
-
+.metadata

Committable suggestion skipped: line range outside the PR's diff.

mobile/lib/services/accounts_service.dart-16-22 (1)

16-22: Add a timeout to prevent indefinite hangs.

The HTTP request lacks a timeout configuration, which could cause the app to hang indefinitely if the server is unresponsive.

Apply this diff:

 final response = await http.get(
   url,
   headers: {
     'Authorization': 'Bearer $accessToken',
     'Accept': 'application/json',
   },
-);
+).timeout(
+  const Duration(seconds: 30),
+  onTimeout: () => http.Response('{"error": "Request timeout"}', 408),
+);
mobile/lib/services/accounts_service.dart-42-48 (1)

42-48: Wrap jsonDecode in try-catch to handle invalid JSON responses.

The server might return HTML error pages or plain text for certain error conditions, which would cause jsonDecode to throw an exception.

Apply this diff:

 } else {
-  final responseData = jsonDecode(response.body);
-  return {
-    'success': false,
-    'error': responseData['error'] ?? 'Failed to fetch accounts',
-  };
+  try {
+    final responseData = jsonDecode(response.body);
+    return {
+      'success': false,
+      'error': responseData['error'] ?? 'Failed to fetch accounts',
+    };
+  } catch (e) {
+    return {
+      'success': false,
+      'error': 'Failed to fetch accounts: ${response.statusCode}',
+    };
+  }
 }
mobile/lib/services/accounts_service.dart-24-35 (1)

24-35: Add defensive null checks and type validation.

The code assumes responseData['accounts'] exists and is a List, and that responseData['pagination'] exists. If the API response structure changes or is malformed, this will throw an exception.

Apply this diff:

 if (response.statusCode == 200) {
-  final responseData = jsonDecode(response.body);
-  
-  final accountsList = (responseData['accounts'] as List)
-      .map((json) => Account.fromJson(json))
-      .toList();
-  
-  return {
-    'success': true,
-    'accounts': accountsList,
-    'pagination': responseData['pagination'],
-  };
+  try {
+    final responseData = jsonDecode(response.body);
+    
+    if (responseData['accounts'] is! List) {
+      return {
+        'success': false,
+        'error': 'Invalid response format',
+      };
+    }
+    
+    final accountsList = (responseData['accounts'] as List)
+        .map((json) => Account.fromJson(json))
+        .toList();
+    
+    return {
+      'success': true,
+      'accounts': accountsList,
+      'pagination': responseData['pagination'],
+    };
+  } catch (e) {
+    return {
+      'success': false,
+      'error': 'Failed to parse response: $e',
+    };
+  }
mobile/lib/models/user.dart-14-21 (1)

14-21: Handle missing id more safely in User.fromJson

id is non‑nullable, but fromJson currently does:

id: json['id'].toString(),

If json['id'] is null or absent, this becomes the literal string "null", which is almost certainly not a valid user id and can hide backend/data issues.

Consider tightening this by either:

  • enforcing presence and failing fast:
final rawId = json['id'];
if (rawId == null) {
  throw ArgumentError('User JSON is missing "id"');
}
return User(
  id: rawId.toString(),
  // ...
);

or

  • making id nullable (final String? id;) if missing ids are genuinely expected.

This keeps your user identifiers consistent and easier to reason about.

mobile/lib/models/account.dart-18-27 (1)

18-27: Add null-safety handling in fromJson to prevent runtime crashes.

Direct casts like json['name'] as String will throw TypeError if the server returns null for any of these fields. Consider defensive parsing:

 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,
+    name: json['name'] as String? ?? '',
+    balance: json['balance'] as String? ?? '0',
+    currency: json['currency'] as String? ?? 'USD',
     classification: json['classification'] as String?,
-    accountType: json['account_type'] as String,
+    accountType: json['account_type'] as String? ?? 'other_asset',
   );
 }
mobile/lib/models/auth_tokens.dart-16-24 (1)

16-24: Add null checks in fromJson to prevent crashes from malformed API responses.

Direct casts like json['access_token'] as String will throw if the key is missing or null. The _parseToInt helper also throws FormatException which propagates uncaught.

 factory AuthTokens.fromJson(Map<String, dynamic> json) {
+  final accessToken = json['access_token'];
+  final refreshToken = json['refresh_token'];
+  if (accessToken == null || refreshToken == null) {
+    throw FormatException('Missing required token fields');
+  }
   return AuthTokens(
-    accessToken: json['access_token'] as String,
-    refreshToken: json['refresh_token'] as String,
-    tokenType: json['token_type'] as String,
+    accessToken: accessToken as String,
+    refreshToken: refreshToken as String,
+    tokenType: json['token_type'] as String? ?? 'Bearer',
     expiresIn: _parseToInt(json['expires_in']),
     createdAt: _parseToInt(json['created_at']),
   );
 }
mobile/lib/providers/transactions_provider.dart-61-79 (1)

61-79: Add try-catch for network exceptions.

Same issue applies to bulk delete operation.

mobile/lib/services/auth_service.dart-98-132 (1)

98-132: Same error handling and duplicate parsing issues as login method.

The signup method has the same issues identified in login: missing try-catch for network/JSON exceptions and duplicate User.fromJson calls on lines 116 and 124. Apply the same fix pattern for consistency.

mobile/lib/providers/transactions_provider.dart-41-59 (1)

41-59: Add try-catch for network exceptions.

Same issue: wrap the service call in try-catch to handle network failures gracefully.

mobile/lib/services/auth_service.dart-141-168 (1)

141-168: Wrap in try-catch for network and JSON parsing errors.

Same issue applies here: jsonDecode on line 153 can throw if the response is not valid JSON.

mobile/lib/providers/transactions_provider.dart-16-39 (1)

16-39: Missing try-catch for network exceptions.

Unlike AccountsProvider.fetchAccounts, this method lacks a try-catch block. If the service call throws (network error, timeout), the exception will propagate uncaught.

   Future<void> fetchTransactions({
     required String accessToken,
     String? accountId,
   }) async {
     _isLoading = true;
     _error = null;
     notifyListeners();

+    try {
       final result = await _transactionsService.getTransactions(
         accessToken: accessToken,
         accountId: accountId,
       );

       _isLoading = false;

       if (result['success']) {
         _transactions = result['transactions'];
         _error = null;
       } else {
         _error = result['error'];
       }
+    } catch (e) {
+      _isLoading = false;
+      _error = 'Connection error. Please check your internet connection.';
+    }

     notifyListeners();
   }
mobile/lib/services/auth_service.dart-31-71 (1)

31-71: Missing error handling for JSON parsing and network failures.

The jsonDecode call on line 40 and AuthTokens.fromJson on line 44 can throw exceptions if the server returns invalid JSON or an unexpected response structure. Additionally, duplicate User.fromJson calls occur on lines 49 and 57. Consider wrapping in try-catch and reusing the parsed user.

+    try {
       final response = await http.post(
         url,
         headers: {
           'Content-Type': 'application/json',
           'Accept': 'application/json',
         },
         body: jsonEncode(body),
       );

       final responseData = jsonDecode(response.body);

       if (response.statusCode == 200) {
         // Store tokens
         final tokens = AuthTokens.fromJson(responseData);
         await _saveTokens(tokens);
         
         // Store user data
+        User? user;
         if (responseData['user'] != null) {
-          final user = User.fromJson(responseData['user']);
+          user = User.fromJson(responseData['user']);
           await _saveUser(user);
         }
         
         return {
           'success': true,
           'tokens': tokens,
-          'user': responseData['user'] != null 
-              ? User.fromJson(responseData['user']) 
-              : null,
+          'user': user,
         };
       } else if (response.statusCode == 401 && responseData['mfa_required'] == true) {
         return {
           'success': false,
           'mfa_required': true,
           'error': responseData['error'],
         };
       } else {
         return {
           'success': false,
           'error': responseData['error'] ?? responseData['errors']?.join(', ') ?? 'Login failed',
         };
       }
+    } catch (e) {
+      return {
+        'success': false,
+        'error': 'Connection error: ${e.toString()}',
+      };
+    }
🟡 Minor comments (6)
mobile/docs/SIGNING_SETUP.md-9-9 (1)

9-9: Clarify the keystore-base64.txt file location.

The documentation references "项目根目录的 keystore-base64.txt 文件" (keystore-base64.txt in the project root). Since the mobile project is in the /mobile folder per the PR description, clarify whether this file is at the repository root or within the /mobile directory.

mobile/docs/SIGNING_SETUP.md-58-58 (1)

58-58: Correct the path reference to include the mobile folder.

The path android/app/upload-keystore.jks should be mobile/android/app/upload-keystore.jks to reflect the actual project structure.

Apply this diff:

-- **文件位置**: `android/app/upload-keystore.jks`
+- **文件位置**: `mobile/android/app/upload-keystore.jks`
mobile/docs/SIGNING_SETUP.md-63-63 (1)

63-63: Correct the path reference to include the mobile folder.

The path android/key.properties should be mobile/android/key.properties for consistency.

Apply this diff:

-- 这些信息只存储在本地的 `android/key.properties` 文件中(已添加到 .gitignore)
+- 这些信息只存储在本地的 `mobile/android/key.properties` 文件中(已添加到 .gitignore)
mobile/docs/SIGNING_SETUP.md-41-41 (1)

41-41: Correct the path reference to include the mobile folder.

The path android/key.properties should be mobile/android/key.properties to reflect the actual project structure where the mobile app is in the /mobile folder.

Apply this diff:

-本地构建已经配置好,`android/key.properties` 文件包含签名信息。
+本地构建已经配置好,`mobile/android/key.properties` 文件包含签名信息。
mobile/lib/screens/transactions_list_screen.dart-281-302 (1)

281-302: Async deletion in onDismissed may cause inconsistent UI state.

The onDismissed callback removes the item from the UI before the API call completes. If deleteTransaction fails, the item has already been visually removed with no rollback mechanism.

Consider moving the deletion logic to confirmDismiss and only allowing dismissal on success, or implementing an undo/rollback pattern.

-                  onDismissed: (direction) async {
-                    if (transaction.id != null) {
-                      final scaffoldMessenger = ScaffoldMessenger.of(context);
-                      final authProvider = Provider.of<AuthProvider>(context, listen: false);
-                      final accessToken = await authProvider.getValidAccessToken();
-                      if (accessToken != null) {
-                        final success = await transactionsProvider.deleteTransaction(
-                          accessToken: accessToken,
-                          transactionId: transaction.id!,
-                        );
-
-                        if (mounted && success) {
-                          scaffoldMessenger.showSnackBar(
-                            const SnackBar(
-                              content: Text('Transaction deleted'),
-                              backgroundColor: Colors.green,
-                            ),
-                          );
-                        }
-                      }
-                    }
-                  },
+                  onDismissed: (direction) {
+                    // Item already removed, show undo option
+                    final removedTransaction = transaction;
+                    ScaffoldMessenger.of(context).showSnackBar(
+                      SnackBar(
+                        content: const Text('Transaction deleted'),
+                        action: SnackBarAction(
+                          label: 'Undo',
+                          onPressed: () => _loadTransactions(),
+                        ),
+                      ),
+                    );
+                    // Perform deletion in background
+                    _performDeletion(removedTransaction.id!);
+                  },

Committable suggestion skipped: line range outside the PR's diff.

mobile/lib/services/transactions_service.dart-31-42 (1)

31-42: Guard JSON decoding when response bodies may not be valid JSON

jsonDecode(response.body) is called unconditionally in createTransaction and deleteTransaction. If the backend returns an empty body or non‑JSON error payload for some status codes, this will throw and be reported as a generic “Network error: FormatException”, losing the more accurate server context. Consider defensive decoding (e.g., try/catch around jsonDecode or checking response.body.isNotEmpty) and treating JSON parse failures as a separate error case.

-    try {
-      final response = await http.post(
+    try {
+      final response = await http.post(
         url,
         headers: {
           'Content-Type': 'application/json',
           'Accept': 'application/json',
           'Authorization': 'Bearer $accessToken',
         },
         body: jsonEncode(body),
       );
-
-      final responseData = jsonDecode(response.body);
+      Map<String, dynamic>? responseData;
+      if (response.body.isNotEmpty) {
+        try {
+          final decoded = jsonDecode(response.body);
+          if (decoded is Map<String, dynamic>) {
+            responseData = decoded;
+          }
+        } catch (_) {
+          // leave responseData as null; fall back to generic messages
+        }
+      }
@@
-      } else {
-        return {
-          'success': false,
-          'error': responseData['error'] ?? 'Failed to create transaction',
-        };
-      }
+      } else {
+        return {
+          'success': false,
+          'error': responseData?['error'] ?? 'Failed to create transaction',
+        };
+      }

Apply similar guarded decoding in deleteTransaction before accessing responseData['error'].

Also applies to: 55-59, 144-159

🧹 Nitpick comments (30)
mobile/assets/images/.gitkeep (1)

1-2: Consider moving documentation outside of .gitkeep.

.gitkeep files are conventionally empty. While the documentation here is helpful, it's unconventional to include content. Consider creating a separate README.md in this directory or documenting the asset structure elsewhere (e.g., in the mobile app's main README).

Alternatively, if keeping this minimal documentation is intentional, you may leave it as-is—it does provide value for developers exploring the project.

If you'd like to move the documentation, create mobile/assets/images/README.md with the same content and remove the text from .gitkeep:

- # Placeholder for assets
- This directory contains image assets for the Sure Mobile app.

Then ensure .gitkeep remains empty.

mobile/test/widget_test.dart (1)

4-8: Consider adding meaningful widget tests.

While the placeholder smoke test satisfies the CI requirements, consider adding tests for key components like AccountCard widget rendering, theme application, or navigation flows as the app matures.

mobile/android/app/build.gradle (1)

7-33: Make release signing behavior clearer when key.properties is absent

Right now signingConfigs.release is always defined, even if key.properties does not exist, which can lead to slightly confusing Gradle errors when trying to assemble a release build.

Consider either:

  • only creating signingConfigs.release when keystorePropertiesFile.exists(), or
  • adding a clear failure (e.g., throw new GradleException("key.properties missing for release signing")) when a release build is requested and the file is not present.

This keeps the “debug just works, release needs signing vars” story explicit for contributors and CI.

mobile/lib/models/transaction.dart (1)

22-33: Confirm that silent defaults for missing fields (especially nature) are intentional

Transaction.fromJson falls back to empty strings and, for nature, to 'expense':

nature: json['nature']?.toString() ?? 'expense',

If the backend ever omits or mis‑spells nature, this will quietly turn such rows into expenses, which can skew reports and make debugging harder.

You might want to instead:

  • treat a missing/invalid nature as an error, or
  • make nature nullable and handle the “unknown” case explicitly in UI/logic.

Similar questions apply to other fields defaulting to '' or '0'. If this is by design and matches the API contract, then it’s fine — but it’s worth double‑checking.

mobile/lib/services/api_config.dart (1)

8-36: Align in‑memory base URL changes with persisted config (and consider platform defaults)

ApiConfig.initialize() loads backend_url from SharedPreferences, but setBaseUrl only updates the in‑memory _baseUrl and doesn’t write it back. Unless some other code is persisting the same key, users’ backend URL changes will be lost on app restart.

One option is to add a helper that both updates and persists:

static Future<void> persistBaseUrl(String url) async {
  _baseUrl = url;
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('backend_url', url);
}

and use that from your settings/backend‑config screen.

Also note that the default 'http://10.0.2.2:3000' works for Android emulators but not for the iOS simulator; you may want to:

  • choose a platform‑specific default, or
  • prominently document that iOS users must configure the backend URL on first run.

It might also help to log (or otherwise surface) failures in initialize() instead of silently swallowing them, to make backend URL issues easier to debug.

mobile/docs/TECHNICAL_GUIDE.md (3)

9-9: Use markdown link syntax for bare URLs.

Bare URLs should be wrapped in proper markdown link syntax for better rendering and accessibility across different markdown parsers.

Apply this diff:

-This application is a client app for the Sure Finance Management System and requires connection to the Sure backend server (Rails API) to function properly. Backend project: https://github.com/we-promise/sure
+This application is a client app for the Sure Finance Management System and requires connection to the Sure backend server (Rails API) to function properly. Backend project: [Sure](https://github.com/we-promise/sure)

87-107: Add language specifier to flow diagram code blocks.

The ASCII flow diagrams should have a language specifier (e.g., text or plaintext) for markdown linting compliance. This applies to code blocks at lines 60, 87, 113, 148, and 178.

Example fix for the startup flow:

-```
+```text
 App Launch
     ↓
 Initialize ApiConfig (load saved backend URL)

453-457: Convert bare URLs to markdown links.

The related links section should use proper markdown link syntax for consistency with documentation standards.

 ## Related Links

-- **Backend Project**: https://github.com/we-promise/sure
-- **Flutter Official Documentation**: https://docs.flutter.dev
-- **Dart Language Documentation**: https://dart.dev/guides
+- **Backend Project**: [Sure](https://github.com/we-promise/sure)
+- **Flutter Official Documentation**: [Flutter Docs](https://docs.flutter.dev)
+- **Dart Language Documentation**: [Dart Guides](https://dart.dev/guides)
mobile/lib/screens/transactions_list_screen.dart (2)

30-53: Confusing and fragile amount sign calculation logic.

The logic for determining positive/negative amounts by counting minus signs and adjusting based on isAsset is unintuitive and error-prone. This approach assumes the backend may send amounts with multiple negative signs (e.g., "--100"), which is unusual.

Consider clarifying the expected input format or simplifying to parse the amount as a number directly.

   Map<String, dynamic> _getAmountDisplayInfo(String amount, bool isAsset) {
-    // 計算負號個數
-    int negativeCount = '-'.allMatches(amount).length;
-
-    // Asset 帳戶需要在負號個數上 +1 進行微調
-    if (isAsset) {
-      negativeCount += 1;
-    }
-
-    // 移除所有負號以獲取純數字
-    String cleanAmount = amount.replaceAll('-', '');
-
-    // 偶數個負號 = 正數,奇數個負號 = 負數
-    bool isPositive = negativeCount % 2 == 0;
+    // Parse the amount as a double for proper sign handling
+    final parsedAmount = double.tryParse(amount) ?? 0.0;
+    String cleanAmount = parsedAmount.abs().toStringAsFixed(2);
+    
+    // For asset accounts: positive amount = income (green), negative = expense (red)
+    // For liability accounts: positive amount = debt increase (red), negative = payment (green)
+    bool isPositive = isAsset ? parsedAmount >= 0 : parsedAmount < 0;

     return {
       'isPositive': isPositive,
       'displayAmount': cleanAmount,
       'color': isPositive ? Colors.green : Colors.red,
       'icon': isPositive ? Icons.arrow_upward : Icons.arrow_downward,
       'prefix': isPositive ? '' : '-',
     };
   }

324-391: Duplicate computation of amount display info.

_getAmountDisplayInfo is called twice with the same arguments for each transaction item (once for the icon container and once for the amount text). Consider computing it once and reusing the result.

                   child: Padding(
                     padding: const EdgeInsets.all(16),
-                        child: Row(
+                        child: Builder(
+                          builder: (context) {
+                            final displayInfo = _getAmountDisplayInfo(
+                              transaction.amount,
+                              widget.account.isAsset,
+                            );
+                            return Row(
                           children: [
                             if (_isSelectionMode)
                               // ... checkbox code ...
-                            Builder(
-                              builder: (context) {
-                                final displayInfo = _getAmountDisplayInfo(
-                                  transaction.amount,
-                                  widget.account.isAsset,
-                                );
-                                return Container(
+                            Container(
                                   // ... use displayInfo ...
-                                );
-                              },
-                            ),
+                            ),
                             // ... rest of row using same displayInfo ...
+                            );
+                          },
                         ),
mobile/lib/screens/login_screen.dart (2)

145-154: Email validation is minimal.

The current validation only checks for the presence of @, which would accept invalid emails like @ or test@. Consider using a more robust email validation pattern.

                   validator: (value) {
                     if (value == null || value.isEmpty) {
                       return 'Please enter your email';
                     }
-                    if (!value.contains('@')) {
+                    // Basic email pattern check
+                    final emailRegex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
+                    if (!emailRegex.hasMatch(value)) {
                       return 'Please enter a valid email';
                     }
                     return null;
                   },

42-46: Local MFA state may desync with provider.

The local _showOtpField state is initialized to false but depends on authProvider.mfaRequired. If the widget is remounted while the provider retains mfaRequired = true, the OTP field won't show until another failed login attempt.

Consider initializing _showOtpField from the provider in initState or deriving it directly from the provider in the build method.

+  @override
+  void initState() {
+    super.initState();
+    // Sync with provider's MFA state on mount
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      final authProvider = Provider.of<AuthProvider>(context, listen: false);
+      if (authProvider.mfaRequired && !_showOtpField) {
+        setState(() => _showOtpField = true);
+      }
+    });
+  }
mobile/lib/screens/backend_config_screen.dart (2)

78-81: Raw exception messages may expose internal details.

Displaying e.toString() directly to users could leak internal implementation details or confusing technical messages. Consider providing user-friendly error messages.

     } catch (e) {
       setState(() {
-        _errorMessage = 'Connection failed: ${e.toString()}';
+        _errorMessage = 'Connection failed. Please check the URL and your network connection.';
       });
     } finally {

111-114: Same raw exception exposure in save error handling.

Apply the same user-friendly error message pattern here.

     } catch (e) {
       setState(() {
-        _errorMessage = 'Failed to save URL: ${e.toString()}';
+        _errorMessage = 'Failed to save configuration. Please try again.';
       });
     } finally {
mobile/lib/screens/dashboard_screen.dart (3)

66-87: USD and TWD use the same currency symbol.

Both USD and TWD return $ as the symbol. While TWD does use the dollar sign, the New Taiwan Dollar symbol is typically NT$ to distinguish it from other dollar currencies.

       case 'USD':
         return '\$';
       case 'TWD':
-        return '\$';
+        return 'NT\$';
       case 'BTC':

50-64: Consider using a record type instead of List<String>.

Returning List<String> for a fixed two-element tuple loses type information. Dart 3 records provide better type safety.

-  List<String> _formatCurrencyItem(String currency, double amount) {
+  ({String currency, String formatted}) _formatCurrencyItem(String currency, double amount) {
     final symbol = _getCurrencySymbol(currency);
     // ... formatting logic ...
-    return [currency, '$symbol$finalAmount'];
+    return (currency: currency, formatted: '$symbol$finalAmount');
   }

Then update the _SummaryCard callback signature accordingly.


40-43: String-based error type comparison is fragile.

Comparing errorMessage == 'unauthorized' relies on an exact string match. Consider using an enum or constant to represent error types for better maintainability.

mobile/lib/models/account.dart (1)

32-40: Consider logging malformed balance values instead of silently returning 0.0.

The try-catch silently swallows parsing failures, which could mask data quality issues from the backend. While the fallback is safe, consider adding debug logging to help identify problematic data.

 double get balanceAsDouble {
   try {
     final cleanedBalance = balance.replaceAll(RegExp(r'[^\d.-]'), '');
     return double.parse(cleanedBalance);
   } catch (e) {
+    // Consider: debugPrint('Failed to parse balance: $balance');
     return 0.0;
   }
 }
mobile/lib/providers/auth_provider.dart (1)

68-82: Add defensive checks for map key access to prevent runtime errors.

The result map access assumes keys like 'success', 'tokens', 'mfa_required' always exist. If AuthService changes or returns unexpected data, this could fail.

-      if (result['success'] == true) {
-        _tokens = result['tokens'] as AuthTokens?;
-        _user = result['user'] as User?;
+      if (result['success'] as bool? ?? false) {
+        _tokens = result['tokens'] as AuthTokens?;
+        _user = result['user'] as User?;
         _isLoading = false;
         notifyListeners();
         return true;
       } else {
-        if (result['mfa_required'] == true) {
+        if (result['mfa_required'] as bool? ?? false) {
           _mfaRequired = true;
         }
-        _errorMessage = result['error'] as String?;
+        _errorMessage = result['error'] as String? ?? 'Login failed';
mobile/lib/main.dart (1)

31-92: Consider extracting shared theme properties to reduce duplication.

The light and dark themes share identical appBarTheme, cardTheme, inputDecorationTheme, and elevatedButtonTheme. Extract common properties into a helper function or constants:

// Example: Extract common theme data
ThemeData _buildTheme(Brightness brightness) {
  final colorScheme = ColorScheme.fromSeed(
    seedColor: const Color(0xFF6366F1),
    brightness: brightness,
  );
  return ThemeData(
    colorScheme: colorScheme,
    useMaterial3: true,
    appBarTheme: const AppBarTheme(centerTitle: true, elevation: 0),
    cardTheme: CardThemeData(
      elevation: 2,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
    ),
    // ... other shared properties
  );
}
mobile/lib/models/auth_tokens.dart (2)

47-52: Consider adding a small buffer for clock skew in isExpired.

Token validation without buffer could cause failures if client/server clocks differ slightly. A 30-60 second buffer is common practice:

 bool get isExpired {
+  const clockSkewBuffer = 60; // seconds
   final expirationTime = DateTime.fromMillisecondsSinceEpoch(
-    (createdAt + expiresIn) * 1000,
+    (createdAt + expiresIn - clockSkewBuffer) * 1000,
   );
   return DateTime.now().isAfter(expirationTime);
 }

27-35: _parseToInt helper correctly handles int/String but consider handling doubles.

If the backend ever sends expires_in as a double (e.g., 3600.0), this will throw. Adding double handling would improve robustness:

 static int _parseToInt(dynamic value) {
   if (value is int) {
     return value;
   } else if (value is String) {
     return int.parse(value);
+  } else if (value is double) {
+    return value.toInt();
   } else {
     throw FormatException('Cannot parse $value to int');
   }
 }
mobile/lib/services/auth_service.dart (1)

205-215: Consider using user.toJson() for consistency.

Unlike _saveTokens which uses tokens.toJson(), _saveUser manually constructs the JSON object. This can lead to inconsistencies if the User model changes. If User has a toJson() method, use it for consistency.

   Future<void> _saveUser(User user) async {
     await _storage.write(
       key: _userKey,
-      value: jsonEncode({
-        'id': user.id,
-        'email': user.email,
-        'first_name': user.firstName,
-        'last_name': user.lastName,
-      }),
+      value: jsonEncode(user.toJson()),
     );
   }
mobile/lib/services/device_service.dart (2)

18-25: Hardcoded app_version should be dynamic.

The app_version is hardcoded to '1.0.0'. Consider using the package_info_plus package to retrieve the actual app version dynamically.


27-32: Weak device ID generation - consider using UUID.

The current implementation derives both parts of the ID from the same timestamp, providing no additional entropy. timestamp.hashCode is deterministic, so two devices generating IDs at the same millisecond would get the same ID.

+import 'package:uuid/uuid.dart';
+
 String _generateDeviceId() {
-  // Generate a unique device ID
-  final timestamp = DateTime.now().millisecondsSinceEpoch;
-  final random = timestamp.toString().hashCode.abs();
-  return 'sure_mobile_${timestamp}_$random';
+  return 'sure_mobile_${const Uuid().v4()}';
 }

Alternatively, if you prefer not to add a dependency, use Random.secure():

import 'dart:math';

String _generateDeviceId() {
  final random = Random.secure();
  final bytes = List<int>.generate(16, (_) => random.nextInt(256));
  return 'sure_mobile_${bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join()}';
}
mobile/lib/screens/transaction_form_screen.dart (4)

31-39: Make date handling more robust and state‑driven

Right now the selected date is stored only as a formatted string and transformed for the API via split('/'), and the date picker always opens on DateTime.now() instead of the currently selected date. Consider tracking a DateTime _selectedDate in state and deriving both _dateController.text and the API yyyy-MM-dd string from it. This avoids potential RangeError if the text ever deviates from the expected format and improves UX by reopening the picker on the last selection.

-  final _dateController = TextEditingController();
+  final _dateController = TextEditingController();
+  DateTime _selectedDate = DateTime.now();

  @override
  void initState() {
    super.initState();
-   final now = DateTime.now();
-   final formattedDate = DateFormat('yyyy/MM/dd').format(now);
-   _dateController.text = formattedDate;
+   _dateController.text = DateFormat('yyyy/MM/dd').format(_selectedDate);
    _nameController.text = 'SureApp';
  }

  Future<void> _selectDate() async {
-   final DateTime? picked = await showDatePicker(
+   final DateTime? picked = await showDatePicker(
      context: context,
-     initialDate: DateTime.now(),
+     initialDate: _selectedDate,
      firstDate: DateTime(2000),
      lastDate: DateTime(2100),
    );

    if (picked != null) {
      setState(() {
-       _dateController.text = DateFormat('yyyy/MM/dd').format(picked);
+       _selectedDate = picked;
+       _dateController.text = DateFormat('yyyy/MM/dd').format(picked);
      });
    }
  }

-  // Convert date format from yyyy/MM/dd to yyyy-MM-dd
-  final dateParts = _dateController.text.split('/');
-  final apiDate = '${dateParts[0]}-${dateParts[1]}-${dateParts[2]}';
+  final apiDate = DateFormat('yyyy-MM-dd').format(_selectedDate);

Also applies to: 66-79, 107-110


49-64: Consider enforcing numeric input format for amount

Validation is solid, but without an inputFormatter, users can still enter non‑numeric characters (or locale‑specific separators like ,) that will fail double.tryParse. Consider adding a FilteringTextInputFormatter.allow for digits and . or using a locale‑aware money input widget to reduce validation errors at the source.

Also applies to: 295-306


81-105: Align unauthorized handling and session‑expired UX

You handle a missing/expired token (accessToken == null) with a clear “Session expired” snackbar plus logout(), but a 401 from the create API surfaces the raw 'unauthorized' string and separately calls logout(). For consistency and clearer UX, consider mapping the 'unauthorized' error path to the same user‑friendly message and flow as the null‑token case (and optionally closing the sheet after logout).

-        } else {
-          final error = result['error'] ?? 'Failed to create transaction';
-          ScaffoldMessenger.of(context).showSnackBar(
-            SnackBar(
-              content: Text(error),
-              backgroundColor: Colors.red,
-            ),
-          );
-
-          if (error == 'unauthorized') {
-            await authProvider.logout();
-          }
-        }
+        } else {
+          final error = result['error'] ?? 'Failed to create transaction';
+
+          if (error == 'unauthorized') {
+            ScaffoldMessenger.of(context).showSnackBar(
+              const SnackBar(
+                content: Text('Session expired. Please login again.'),
+                backgroundColor: Colors.red,
+              ),
+            );
+            await authProvider.logout();
+            Navigator.of(context).pop(); // optionally close the sheet
+          } else {
+            ScaffoldMessenger.of(context).showSnackBar(
+              SnackBar(
+                content: Text(error),
+                backgroundColor: Colors.red,
+              ),
+            );
+          }
+        }

Also applies to: 122-143


25-26: Consider injecting TransactionsService for testability

Creating TransactionsService directly in state ties the widget to a concrete implementation and makes it harder to mock in tests. If you expect to unit‑test this screen or swap implementations, consider providing TransactionsService via Provider/GetIt or passing it in through the widget constructor.

mobile/lib/services/transactions_service.dart (1)

168-200: Multi‑delete aggregation is straightforward; consider exposing partial success info

Running deleteTransaction calls in parallel and aggregating the results with allSuccess keeps the implementation simple. If you need more granular feedback later, you might extend the error branch to also report how many succeeded (and/or which IDs failed), but the current shape is adequate for a first pass.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88952e4 and 6dc597c.

⛔ Files ignored due to path filters (6)
  • mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png is excluded by !**/*.png
  • mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png is excluded by !**/*.png
  • mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png is excluded by !**/*.png
  • mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png is excluded by !**/*.png
  • mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • mobile/assets/icon/app_icon.png is excluded by !**/*.png
📒 Files selected for processing (52)
  • .github/workflows/flutter-build.yml (1 hunks)
  • mobile/.gitignore (1 hunks)
  • mobile/README.md (1 hunks)
  • mobile/analysis_options.yaml (1 hunks)
  • mobile/android/app/build.gradle (1 hunks)
  • mobile/android/app/src/main/AndroidManifest.xml (1 hunks)
  • mobile/android/app/src/main/kotlin/com/sure/mobile/MainActivity.kt (1 hunks)
  • mobile/android/app/src/main/res/drawable/launch_background.xml (1 hunks)
  • mobile/android/app/src/main/res/values/styles.xml (1 hunks)
  • mobile/android/build.gradle (1 hunks)
  • mobile/android/gradle.properties (1 hunks)
  • mobile/android/gradle/wrapper/gradle-wrapper.properties (1 hunks)
  • mobile/android/settings.gradle (1 hunks)
  • mobile/assets/images/.gitkeep (1 hunks)
  • mobile/docs/SIGNING_SETUP.md (1 hunks)
  • mobile/docs/TECHNICAL_GUIDE.md (1 hunks)
  • mobile/docs/iOS_BUILD.md (1 hunks)
  • mobile/ios/Flutter/AppFrameworkInfo.plist (1 hunks)
  • mobile/ios/Flutter/Debug.xcconfig (1 hunks)
  • mobile/ios/Flutter/Release.xcconfig (1 hunks)
  • mobile/ios/Podfile (1 hunks)
  • mobile/ios/Runner.xcodeproj/project.pbxproj (1 hunks)
  • mobile/ios/Runner/AppDelegate.swift (1 hunks)
  • mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (1 hunks)
  • mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json (1 hunks)
  • mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard (1 hunks)
  • mobile/ios/Runner/Base.lproj/Main.storyboard (1 hunks)
  • mobile/ios/Runner/GeneratedPluginRegistrant.h (1 hunks)
  • mobile/ios/Runner/GeneratedPluginRegistrant.m (1 hunks)
  • mobile/ios/Runner/Info.plist (1 hunks)
  • mobile/ios/Runner/Runner-Bridging-Header.h (1 hunks)
  • mobile/lib/main.dart (1 hunks)
  • mobile/lib/models/account.dart (1 hunks)
  • mobile/lib/models/auth_tokens.dart (1 hunks)
  • mobile/lib/models/transaction.dart (1 hunks)
  • mobile/lib/models/user.dart (1 hunks)
  • mobile/lib/providers/accounts_provider.dart (1 hunks)
  • mobile/lib/providers/auth_provider.dart (1 hunks)
  • mobile/lib/providers/transactions_provider.dart (1 hunks)
  • mobile/lib/screens/backend_config_screen.dart (1 hunks)
  • mobile/lib/screens/dashboard_screen.dart (1 hunks)
  • mobile/lib/screens/login_screen.dart (1 hunks)
  • mobile/lib/screens/transaction_form_screen.dart (1 hunks)
  • mobile/lib/screens/transactions_list_screen.dart (1 hunks)
  • mobile/lib/services/accounts_service.dart (1 hunks)
  • mobile/lib/services/api_config.dart (1 hunks)
  • mobile/lib/services/auth_service.dart (1 hunks)
  • mobile/lib/services/device_service.dart (1 hunks)
  • mobile/lib/services/transactions_service.dart (1 hunks)
  • mobile/lib/widgets/account_card.dart (1 hunks)
  • mobile/pubspec.yaml (1 hunks)
  • mobile/test/widget_test.dart (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-24T16:56:30.669Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Account model is a Rails delegated type with subtypes: Depository, Investment, Crypto, Property, Vehicle, OtherAsset (assets) and CreditCard, Loan, OtherLiability (liabilities)

Applied to files:

  • mobile/lib/models/account.dart
📚 Learning: 2025-11-24T16:54:59.198Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.198Z
Learning: Domain model structure: User → has many Accounts → has many Transactions; Account types include checking, savings, credit cards, investments, crypto, loans, properties

Applied to files:

  • mobile/lib/models/account.dart
🪛 Clang (14.0.6)
mobile/ios/Runner/Runner-Bridging-Header.h

[error] 1-1: 'Flutter/Flutter.h' file not found

(clang-diagnostic-error)

mobile/ios/Runner/GeneratedPluginRegistrant.h

[error] 1-1: 'Flutter/Flutter.h' file not found

(clang-diagnostic-error)

🪛 LanguageTool
mobile/README.md

[uncategorized] ~146-~146: The official name of this software platform is spelled with a capital “H”.
Context: ...des automated CI/CD via GitHub Actions (.github/workflows/flutter-build.yml): - **Tri...

(GITHUB)

🪛 markdownlint-cli2 (0.18.1)
mobile/docs/TECHNICAL_GUIDE.md

9-9: Bare URL used

(MD034, no-bare-urls)


60-60: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


87-87: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


113-113: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


148-148: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


178-178: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


455-455: Bare URL used

(MD034, no-bare-urls)


456-456: Bare URL used

(MD034, no-bare-urls)


457-457: Bare URL used

(MD034, no-bare-urls)

mobile/README.md

34-34: Bare URL used

(MD034, no-bare-urls)


85-85: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

dwvwdv pushed a commit to dwvwdv/sure that referenced this pull request Dec 8, 2025
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.
dwvwdv added a commit to dwvwdv/sure that referenced this pull request Dec 8, 2025
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.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (2)
mobile/lib/services/auth_service.dart (2)

141-145: Overly broad exception handling leaks internal details.

Same issue as in the login method—all exceptions are caught and internal error details are exposed.

Apply the same fix as suggested for the login method to categorize exceptions properly.


184-188: Overly broad exception handling leaks internal details.

Same exception handling issue as in login and signup.

Apply the same categorized exception handling fix.

🧹 Nitpick comments (10)
mobile/lib/providers/transactions_provider.dart (1)

6-6: Consider dependency injection for TransactionsService.

The service instance is hardcoded, making testing difficult and reducing flexibility. Consider accepting the service via constructor parameter to enable dependency injection and improve testability.

Apply this diff to inject the service:

-class TransactionsProvider with ChangeNotifier {
-  final TransactionsService _transactionsService = TransactionsService();
+class TransactionsProvider with ChangeNotifier {
+  final TransactionsService _transactionsService;
+
+  TransactionsProvider({TransactionsService? transactionsService})
+      : _transactionsService = transactionsService ?? TransactionsService();
mobile/lib/services/transactions_service.dart (2)

91-99: Document or standardize the API response format.

The code handles both array and object-with-transactions-key responses. This flexibility suggests the API contract is unclear or inconsistent. If possible, standardize the backend to return a consistent shape.

If standardization isn't feasible, add a comment explaining why both formats are supported (e.g., versioning, multiple endpoints).


173-178: Consider fail-fast behavior for batch deletions.

Future.wait executes all deletions in parallel. If the first deletion fails due to an expired token (401), the remaining deletions will also fail but are still attempted. Consider checking the first failure and short-circuiting for auth errors.

Alternatively, document that this behavior is intentional (e.g., for resilience or partial success tracking).

mobile/lib/services/auth_service.dart (1)

226-236: Prefer user.toJson() for consistency and maintainability.

Unlike _saveTokens (which uses tokens.toJson()), _saveUser manually constructs the JSON map. This inconsistency increases maintenance overhead—if the User model adds fields, this method may not persist them.

If User has a toJson() method, use it:

   Future<void> _saveUser(User user) async {
     await _storage.write(
       key: _userKey,
-      value: jsonEncode({
-        'id': user.id,
-        'email': user.email,
-        'first_name': user.firstName,
-        'last_name': user.lastName,
-      }),
+      value: jsonEncode(user.toJson()),
     );
   }

If not, consider adding toJson() to the User model for consistency.

mobile/lib/providers/accounts_provider.dart (6)

5-17: Consider injecting AccountsService and returning an unmodifiable accounts view

Two small robustness tweaks here:

  • Make _accountsService injectable (e.g., via a constructor parameter) to ease testing and future mocking.
  • Expose accounts as an unmodifiable view so callers can’t mutate the backing list without going through the provider (and without notifyListeners()).

For example:

-import '../models/account.dart';
-import '../services/accounts_service.dart';
+import '../models/account.dart';
+import '../services/accounts_service.dart';
+import 'dart:collection';
@@
-class AccountsProvider with ChangeNotifier {
-  final AccountsService _accountsService = AccountsService();
+class AccountsProvider with ChangeNotifier {
+  final AccountsService _accountsService;
+
+  AccountsProvider({AccountsService? accountsService})
+      : _accountsService = accountsService ?? AccountsService();
@@
-  List<Account> get accounts => _accounts;
+  UnmodifiableListView<Account> get accounts =>
+      UnmodifiableListView(_accounts);

This keeps state changes centralized in the provider and makes it harder to misuse from the UI layer.


18-28: Derived asset/liability lists look good; watch for repeated sorting cost only if lists grow large

The filtering + _sortAccounts call on a copied list is clean and side-effect free on _accounts. The only trade-off is repeated sorting every time the getter is accessed; that’s fine for small lists, but if you end up with many accounts and these getters are hit frequently (e.g., in rebuild-heavy widgets), you might later want to:

  • Cache sorted results until _accounts changes, or
  • Pre-sort _accounts and filter without re-sorting.

No change strictly needed now; just something to keep in mind if performance ever becomes a concern.


30-44: Currency total logic is correct; consider a small helper to DRY up asset/liability aggregation

Both totals getters implement the same aggregation pattern with only the predicate differing. You could centralize that into a private helper for readability and to avoid divergence if the logic evolves:

-  Map<String, double> get assetTotalsByCurrency {
-    final totals = <String, double>{};
-    for (var account in _accounts.where((a) => a.isAsset)) {
-      totals[account.currency] = (totals[account.currency] ?? 0.0) + account.balanceAsDouble;
-    }
-    return totals;
-  }
+  Map<String, double> _totalsByCurrency(bool Function(Account) filter) {
+    final totals = <String, double>{};
+    for (final account in _accounts.where(filter)) {
+      totals[account.currency] =
+          (totals[account.currency] ?? 0.0) + account.balanceAsDouble;
+    }
+    return totals;
+  }
+
+  Map<String, double> get assetTotalsByCurrency =>
+      _totalsByCurrency((a) => a.isAsset);
@@
-  Map<String, double> get liabilityTotalsByCurrency {
-    final totals = <String, double>{};
-    for (var account in _accounts.where((a) => a.isLiability)) {
-      totals[account.currency] = (totals[account.currency] ?? 0.0) + account.balanceAsDouble;
-    }
-    return totals;
-  }
+  Map<String, double> get liabilityTotalsByCurrency =>
+      _totalsByCurrency((a) => a.isLiability);

Pure readability/maintenance win; functionality stays the same.


46-63: Sorting comparator is clear; confirm that the chosen priority matches product expectations

The multi-key sort (type → currency → balance desc → name) is explicit and well-commented. Since this impacts what users see first, it’s worth double-checking:

  • That accountType and currency values are stable and won’t change shape (e.g., backend renaming or localization), and
  • That this ordering matches how accounts are sorted in other clients (web/PWA) so users get a consistent experience.

If there’s a domain enum for type or classification, consider sorting on that instead of raw strings to make the intent more robust.


65-99: fetchAccounts flow is solid; suggest typed result and finally to centralize loading-state reset

Behavior-wise this looks correct: loading flag is set before the call, accounts/pagination are updated only on success, errors are surfaced via _errorMessage, and the method returns a simple success flag.

Two improvements to consider:

  1. Avoid stringly-typed result maps from AccountsService

Using result['success'], result['accounts'], result['pagination'], result['error'] is fragile and forces casts like cast<Account>(). If feasible, change AccountsService.getAccounts to return a typed object or record, e.g.:

class AccountsFetchResult {
  final bool success;
  final List<Account> accounts;
  final Map<String, dynamic>? pagination;
  final String? error;
  // ...
}

Then this method can become strongly typed and safer to refactor.

  1. Use finally to guarantee _isLoading reset and avoid duplication

You currently repeat _isLoading = false; notifyListeners(); in all branches. A finally block centralizes that and protects you if new return paths are added later:

   Future<bool> fetchAccounts({
@@
-    try {
+    try {
       final result = await _accountsService.getAccounts(
@@
-      if (result['success'] == true && result.containsKey('accounts')) {
-        _accounts = (result['accounts'] as List<dynamic>?)?.cast<Account>() ?? [];
-        _pagination = result['pagination'] as Map<String, dynamic>?;
-        _isLoading = false;
-        notifyListeners();
-        return true;
-      } else {
-        _errorMessage = result['error'] as String? ?? 'Failed to fetch accounts';
-        _isLoading = false;
-        notifyListeners();
-        return false;
-      }
-    } catch (e) {
-      _errorMessage = 'Connection error. Please check your internet connection.';
-      _isLoading = false;
-      notifyListeners();
-      return false;
-    }
+      if (result['success'] == true && result.containsKey('accounts')) {
+        _accounts =
+            (result['accounts'] as List<dynamic>?)?.cast<Account>() ?? [];
+        _pagination = result['pagination'] as Map<String, dynamic>?;
+        return true;
+      } else {
+        _errorMessage =
+            result['error'] as String? ?? 'Failed to fetch accounts';
+        return false;
+      }
+    } catch (e) {
+      _errorMessage =
+          'Connection error. Please check your internet connection.';
+      return false;
+    } finally {
+      _isLoading = false;
+      notifyListeners();
+    }

One UX point to double-check: on failure you keep the previous _accounts and _pagination, which can be nice for offline/temporary-error behavior. Confirm that this is intentional versus wanting to show an explicitly “empty” state on error.


101-111: Clear helpers are fine; consider resetting _isLoading and reusing clearError()

clearAccounts and clearError do what they say. Two minor polish ideas:

  • clearAccounts might also reset _isLoading to false in case it’s ever called during a loading state (e.g., on logout while a fetch is in-flight), so the UI can’t get stuck showing a spinner.
  • You can reuse clearError() from clearAccounts() to avoid duplicating the error-reset logic.

For example:

  void clearAccounts() {
    _accounts = [];
    _pagination = null;
-    _errorMessage = null;
-    notifyListeners();
+    _isLoading = false;
+    clearError();
   }

And keep clearError() as-is.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6dc597c and 652db66.

📒 Files selected for processing (6)
  • .github/workflows/flutter-build.yml (1 hunks)
  • mobile/lib/providers/accounts_provider.dart (1 hunks)
  • mobile/lib/providers/transactions_provider.dart (1 hunks)
  • mobile/lib/services/accounts_service.dart (1 hunks)
  • mobile/lib/services/auth_service.dart (1 hunks)
  • mobile/lib/services/transactions_service.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • mobile/lib/services/accounts_service.dart
  • .github/workflows/flutter-build.yml
🔇 Additional comments (6)
mobile/lib/providers/transactions_provider.dart (1)

32-32: Verify the cast operation is safe.

The code casts result['transactions'] to List<dynamic> and then calls .cast<Transaction>(). This assumes the service already returns a list of Transaction objects. If the service returns raw JSON maps instead, the cast will fail at runtime. Verify that TransactionsService.getTransactions properly deserializes the response into Transaction objects before returning.

mobile/lib/services/auth_service.dart (5)

1-11: LGTM! Clean setup.

The imports, storage initialization, and constant definitions follow Flutter best practices.


192-217: LGTM! Robust read operations with appropriate error handling.

The logout, getStoredTokens, and getStoredUser methods handle storage operations and JSON parsing errors gracefully by returning null on failures.


219-224: LGTM! Consistent serialization approach.

Uses tokens.toJson() for serialization, which is the standard Flutter pattern.


20-20: Verify HTTPS is enforced in ApiConfig.baseUrl.

Credentials (email/password) are transmitted in the request body. Ensure that ApiConfig.baseUrl strictly uses HTTPS to prevent credential exposure over insecure channels. Check the ApiConfig definition to confirm the baseUrl is hardcoded with HTTPS protocol and cannot be overridden with HTTP.


226-236: Verify User model has toJson() method.

Check whether the User model provides a toJson() method to enable consistent serialization instead of manually constructing the JSON object in _saveUser().

@jjmata jjmata self-requested a review December 8, 2025 15:57
@jjmata jjmata added this to the v0.6.6 milestone Dec 8, 2025
@jjmata jjmata changed the title feat: mobile support. Mobile native client via Flutter Dec 8, 2025
@jjmata
Copy link
Collaborator

jjmata commented Dec 8, 2025

@jjmata Currently, it needs to be configured with the environment variables required for Android signing.

Will register Google Play credentials and set this up. 👍

Thanks for kicking things off!

@jjmata jjmata moved this to Backlog in Next release (v0.6.6) Dec 8, 2025
@chardinson
Copy link

chardinson commented Dec 9, 2025

Login fails when account has Two-Factor Authentication enabled

Issue:
The login screen throws a setState() called after dispose() error and the app stops working when attempting to log in with an account that has Two-Factor Authentication enabled.

Steps to reproduce:

  1. Enter email and password for an account with 2FA enabled
  2. Press "Sign In"
  3. Error occurs and the app becomes unresponsive

Error message:

setState() called after dispose(): _LoginScreenState#0b3f5(lifecycle state: defunct, not mounted)

@joaofrenor
Copy link

@jjmata I can help publishing on iOS if you guys need

@jjmata
Copy link
Collaborator

jjmata commented Dec 10, 2025

@jjmata I can help publishing on iOS if you guys need

Excellent! What I really want to jump on is making the chatbot available under the Flutter codebase. Want to look into that? ;-)

dwvwdv and others added 4 commits December 11, 2025 23:35
- 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
- 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
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
mobile/lib/providers/transactions_provider.dart (3)

7-7: Consider injecting TransactionsService for testability and flexibility.

Directly instantiating TransactionsService here couples the provider to a concrete implementation and makes testing harder. Consider constructor injection so you can pass a mock or alternate implementation in tests or in different environments.

-class TransactionsProvider with ChangeNotifier {
-  final TransactionsService _transactionsService = TransactionsService();
+class TransactionsProvider with ChangeNotifier {
+  final TransactionsService _transactionsService;
+
+  TransactionsProvider({TransactionsService? transactionsService})
+      : _transactionsService = transactionsService ?? TransactionsService();

17-41: Streamline fetchTransactions state updates and confirm error handling.

You currently call notifyListeners() three times: once for starting load, once when setting _isLoading = false, and once after updating _transactions / _error. This causes extra rebuilds and briefly exposes a “not loading but still old data” state. You can update all post‑request state in one block and notify once.

Also, if TransactionsService.getTransactions can throw (e.g., network error), _isLoading may remain true and listeners won’t see an error. Please confirm the service always returns a result map and never throws, or wrap the call in try/catch and set _isLoading / _error accordingly.

   Future<void> fetchTransactions({
     required String accessToken,
     String? accountId,
   }) async {
     _isLoading = true;
     _error = null;
     notifyListeners();
 
-    final result = await _transactionsService.getTransactions(
-      accessToken: accessToken,
-      accountId: accountId,
-    );
-
-    _isLoading = false;
-    notifyListeners();
-
-    if (result['success'] == true && result.containsKey('transactions')) {
-      _transactions = (result['transactions'] as List<dynamic>?)?.cast<Transaction>() ?? [];
-      _error = null;
-    } else {
-      _error = result['error'] as String? ?? 'Failed to fetch transactions';
-    }
-
-    notifyListeners();
+    try {
+      final result = await _transactionsService.getTransactions(
+        accessToken: accessToken,
+        accountId: accountId,
+      );
+
+      if (result['success'] == true && result.containsKey('transactions')) {
+        _transactions =
+            (result['transactions'] as List<dynamic>?)?.cast<Transaction>() ?? [];
+        _error = null;
+      } else {
+        _error = result['error'] as String? ?? 'Failed to fetch transactions';
+      }
+    } catch (_) {
+      _error = 'Failed to fetch transactions';
+    } finally {
+      _isLoading = false;
+      notifyListeners();
+    }
   }

Please double‑check TransactionsService.getTransactions and adjust the try/catch if it already normalizes all failures into the result map.


63-80: Optimize membership check when deleting multiple transactions.

Using transactionIds.contains(t.id) inside removeWhere makes the operation O(n × m) for n local transactions and m IDs. Converting transactionIds to a Set first gives O(1) membership checks and improves performance for larger lists.

   Future<bool> deleteMultipleTransactions({
     required String accessToken,
     required List<String> transactionIds,
   }) async {
+    final idsSet = transactionIds.toSet();
+
     final result = await _transactionsService.deleteMultipleTransactions(
       accessToken: accessToken,
-      transactionIds: transactionIds,
+      transactionIds: transactionIds,
     );
 
     if (result['success'] == true) {
-      _transactions.removeWhere((t) => transactionIds.contains(t.id));
+      _transactions.removeWhere((t) => idsSet.contains(t.id));
       notifyListeners();
       return true;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 652db66 and 8882871.

📒 Files selected for processing (3)
  • mobile/lib/providers/transactions_provider.dart (1 hunks)
  • mobile/lib/services/auth_service.dart (1 hunks)
  • mobile/lib/services/transactions_service.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • mobile/lib/services/transactions_service.dart
  • mobile/lib/services/auth_service.dart
🔇 Additional comments (1)
mobile/lib/providers/transactions_provider.dart (1)

9-15: Good use of encapsulation for transactions and loading/error state.

Exposing an unmodifiable view for transactions and read‑only getters for isLoading and error fits well with the provider pattern and avoids accidental external mutation.

dwvwdv and others added 7 commits December 12, 2025 00:05
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
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
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).
…ucmZ4ShNS9G7vBrGF

Fix 2FA login crash caused by setState after dispose
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
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
claude and others added 3 commits December 12, 2025 07:50
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.
…qyLpzjV7z46V3JMMau

Fix mobile 2FA login requiring double password entry
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
test_2fa_flow.md (1)

20-27: Specify language for code block.

The code block on line 20 lacks a language identifier for syntax highlighting. While the static content is clear, adding a language specifier improves readability.

Apply this diff:

-3. **Check the console output for these debug messages:**
-```
+3. **Check the console output for these debug messages:**
+```text
 Login response status: 401
 Login response body: {"error":"Two-factor authentication required","mfa_required":true}
 Login result: {success: false, mfa_required: true, error: Two-factor authentication required}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8882871 and cfd3096.

📒 Files selected for processing (5)
  • mobile/lib/main.dart (1 hunks)
  • mobile/lib/providers/auth_provider.dart (1 hunks)
  • mobile/lib/screens/login_screen.dart (1 hunks)
  • mobile/lib/services/auth_service.dart (1 hunks)
  • test_2fa_flow.md (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • mobile/lib/providers/auth_provider.dart
  • mobile/lib/main.dart
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-24T16:56:44.818Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/self_improve.mdc:0-0
Timestamp: 2025-11-24T16:56:44.818Z
Learning: Check for consistent error handling patterns across the codebase

Applied to files:

  • mobile/lib/services/auth_service.dart
🪛 markdownlint-cli2 (0.18.1)
test_2fa_flow.md

20-20: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (3)
mobile/lib/services/auth_service.dart (2)

16-115: Excellent error handling and optimizations.

The login method has been significantly improved based on previous feedback:

  • Lines 54-65: User parsing is now optimized (parse once, reuse)
  • Lines 78-114: Comprehensive exception handling with specific catch blocks for SocketException, TimeoutException, HttpException, FormatException, and TypeError
  • All exceptions are logged with stack traces using debugPrint
  • User-facing error messages are safe and don't leak implementation details

The implementation now follows best practices for error handling. Based on learnings, the consistent error handling pattern across authentication methods is excellent.


323-333: Review comment is incorrect and should be dismissed.

The User model does not have a toJson() method. While fromJson() exists for deserialization, there is no corresponding toJson() method in the User class. The manual serialization in _saveUser is the appropriate approach given the current model structure. The suggestion to use user.toJson() cannot be applied.

If consistency with other models (e.g., AuthTokens, Transaction) is desired, consider adding a toJson() method to the User model instead.

Likely an incorrect or invalid review comment.

mobile/lib/screens/login_screen.dart (1)

203-203: Verify Flutter version supports withValues API.

The withValues(alpha: 0.3) syntax is a Flutter 3.27+ API that replaces the older withOpacity method. Ensure the project's Flutter version in pubspec.yaml and CI/CD configuration supports this API.

Run the following script to check the Flutter version constraints:

#!/bin/bash
# Check Flutter SDK version constraints in pubspec.yaml

echo "=== Checking Flutter SDK version in pubspec.yaml ==="
cat mobile/pubspec.yaml | grep -A 2 "environment:"

echo ""
echo "=== Checking if withValues is documented (Flutter 3.27+) ==="
echo "Please verify that the project uses Flutter 3.27 or later."

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

dwvwdv commented Dec 13, 2025

Login fails when account has Two-Factor Authentication enabled

Issue: The login screen throws a setState() called after dispose() error and the app stops working when attempting to log in with an account that has Two-Factor Authentication enabled.

Steps to reproduce:

  1. Enter email and password for an account with 2FA enabled
  2. Press "Sign In"
  3. Error occurs and the app becomes unresponsive

Error message:

setState() called after dispose(): _LoginScreenState#0b3f5(lifecycle state: defunct, not mounted)

Thank you for your message. I have fixed it, and 2FA verification should now work correctly.
@jjmata I think the code can be merged now.
If there are any further problems, please let me know and I will make adjustments. :)

@jjmata
Copy link
Collaborator

jjmata commented Dec 13, 2025

@jjmata I think the code can be merged now.

Excellent! Let me engage with it over the weekend and merge as soon as my developer accounts are approved/have signing keys for both builds. Google Play didn't like my docs so I have no Google Play account yet. 😦

@jjmata
Copy link
Collaborator

jjmata commented Dec 13, 2025

One thing that would be nice is if we make the mobile build optional, since most people won't need it/there is no need to rebuild when there are no changes in mobile/ right? Haven't actually looked at the CI/CD scripts, maybe you already do that?

@dwvwdv
Copy link
Author

dwvwdv commented Dec 13, 2025

One thing that would be nice is if we make the mobile build optional, since most people won't need it/there is no need to rebuild when there are no changes in mobile/ right? Haven't actually looked at the CI/CD scripts, maybe you already do that?

Not yet. I did disable Ruby's workflow during development.
You're right. I'll check how to do it.

I just checked, and an adjustment was made during the migration to mobile/. The current settings should be correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

5 participants