Dependencies androidx.browser:1.9.0 and androidx.core:1.17.0 now
require AGP >= 8.9.1. Bumps the Gradle wrapper to 8.12 to satisfy
the AGP compatibility requirement.
* feat(mobile): lock chat input while bot is responding + 20s timeout
- Add _isWaitingForResponse flag to ChatProvider; set in _startPolling,
cleared in _stopPolling so it covers the full polling lifecycle not
just the initial HTTP POST
- Add _pollingStartTime + 20s timeout in _pollForUpdates; if the bot
never responds the flag resets, errorMessage is surfaced, and input
unlocks automatically
- Gate send button and keyboard shortcut on isSendingMessage ||
isWaitingForResponse so users cannot queue up multiple messages
while a response is in flight
(adding an interrupt like with other chat bots would require a larger rewrite of the backend structure)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(mobile): make polling timeout measure inactivity not total duration
Reset _pollingStartTime whenever assistant content grows so the 20s
timeout only fires if no new content has arrived in that window.
Prevents cutting off a slow-but-streaming response mid-generation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(mobile): lock input for full polling duration, not just until first chunk
- Add isPolling getter to ChatProvider (true while _pollingTimer is active)
- Gate send button and intent on isPolling in addition to isWaitingForResponse
so users cannot submit overlapping prompts while a response is still streaming
- Also auto-scroll while polling is active
* Fix chat polling timeout race and send re-entry guard
Polling timeout was evaluated before the network attempt, allowing it
to fire just as a response became ready. Timeout check now runs after
each poll attempt and only when no progress was made; network errors
fall through to the same check instead of silently swallowing the tick.
Added _isSendInFlight boolean to prevent rapid taps from re-entering
_sendMessage() during the async token fetch window before provider
flags are set. Guard is set synchronously at the top of the method and
cleared in a finally block.
* fix(mobile): prevent overlapping polls and empty-placeholder stop
Add _isPollingRequestInFlight guard so Timer.periodic ticks are
skipped if a getChat request is still in flight, preventing stale
results from resetting state out of order.
Fix empty assistant placeholder incorrectly triggering _stopPolling:
stable is only declared when a previously observed length exists and
hasn't grown. An initial empty message keeps polling until content
arrives or the timeout fires.
* fix(mobile): reset _lastAssistantContentLength in _stopPolling
Prevents stale content-length state from a prior polling session
bleeding into the next one.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Feature: Biometric lock for app resume
User enables "Biometric Lock" in Settings → prompted to verify fingerprint/face first.
User backgrounds the app → _isLocked = true.
User returns → lock screen appears, auto-triggers biometric prompt.
Success → app unlocks, state preserved underneath.
Can retry or log out as fallback.
Add USE_BIOMETRIC permission to AndroidManifest.
* Fix: Remove duplicate local_auth entry in pubspec.lock and add NSFaceIDUsageDescription to iOS Info.plist
* fix(mobile) : Remove duplicate local auth files first
* fix(mobile): keep MainNavigationScreen in the Stack, let the lock screen float above, no unmounting
* Updtae: Swap out Flutter Activity for FlutterFragmentActivity that extends the lock scan feature
* fix(mobile): address biometric lock PR review feedback
* fix(mobile): only require biometric auth when enabling lock, not disabling
Prevents users from getting locked out if biometrics start failing —
they can now disable the lock without needing to pass biometric auth.
* fix(mobile): add missing closing brace in setBiometricEnabled
---------
Signed-off-by: Tristan Katana <50181095+felixmuinde@users.noreply.github.com>
* Move debug logs button from Home to Settings page, remove refresh/logout from Home AppBar
- Remove Debug Logs, Refresh, and Sign Out buttons from DashboardScreen AppBar
- Add Debug Logs ListTile entry in SettingsScreen under app info section
- Remove unused _handleLogout method from DashboardScreen
- Remove unused log_viewer_screen.dart import from DashboardScreen
https://claude.ai/code/session_017XQZdaEwUuRS75tJMcHzB9
* Add category picker to Android transaction form
Implements category selection when creating transactions in the mobile app.
Uses the existing /api/v1/categories endpoint to fetch categories and sends
category_id when creating transactions via the API.
New files:
- Category model, CategoriesService, CategoriesProvider
Updated:
- Transaction/OfflineTransaction models with categoryId/categoryName
- TransactionsService/Provider to pass category_id
- DB schema v2 migration for category columns
- TransactionFormScreen with category dropdown in "More" section
Closes#78https://claude.ai/code/session_01Dgj8tYrCkoUaLW2WrQ3vMJ
* Fix ambiguous Category import in CategoriesProvider
Hide Flutter's built-in Category annotation from foundation.dart
to resolve name collision with our Category model.
https://claude.ai/code/session_01Dgj8tYrCkoUaLW2WrQ3vMJ
* Add category filter on Dashboard, clear categories on data reset, fix ambiguous imports
- Add CategoryFilter widget (horizontal chip row like CurrencyFilter)
- Show category filter on Dashboard below currency filter (2nd row)
- Add "Show Category Filter" toggle in Settings > Display section
- Clear CategoriesProvider on "Clear Local Data" and "Reset Account"
- Fix Category name collision: hide Flutter's Category from material.dart
- Add getShowCategoryFilter/setShowCategoryFilter to PreferencesService
https://claude.ai/code/session_01Dgj8tYrCkoUaLW2WrQ3vMJ
* Fix Category name collision using prefixed imports
Use 'import as models' instead of 'hide Category' to avoid
undefined_hidden_name warnings with flutter/material.dart.
https://claude.ai/code/session_01Dgj8tYrCkoUaLW2WrQ3vMJ
* Fix duplicate column error in SQLite migration
Check if category_id/category_name columns exist before running
ALTER TABLE, preventing crashes when the DB was already at v2
or the migration had partially succeeded.
https://claude.ai/code/session_01Dgj8tYrCkoUaLW2WrQ3vMJ
* Move CategoryFilter from dashboard to transaction list screen
CategoryFilter was filtering accounts on the dashboard but accounts
are already grouped by type. Moved it to TransactionsListScreen where
it filters transactions by category, which is the correct placement.
https://claude.ai/code/session_01Dgj8tYrCkoUaLW2WrQ3vMJ
* Add category tag badge next to transaction name
Shows an oval-bordered category label after each transaction's
name for quick visual identification of transaction types.
https://claude.ai/code/session_01Dgj8tYrCkoUaLW2WrQ3vMJ
* Address review findings for category feature
1. Category.fromJson now recursively parses parent chain;
displayName walks all ancestors (e.g. "Grandparent > Parent > Child")
2. CategoriesProvider.fetchCategories guards against concurrent/duplicate
calls by checking _isLoading and _hasFetched early
3. CategoryFilter chips use displayName to distinguish subcategories
4. Transaction badge resolves full displayName from CategoriesProvider
with overflow ellipsis for long paths
5. Offline storage preserves local category values when server response
omits them (coalesce with ??)
https://claude.ai/code/session_01Dgj8tYrCkoUaLW2WrQ3vMJ
* Fix missing closing brace in PreferencesService causing theme_provider analyze errors
https://claude.ai/code/session_01Dgj8tYrCkoUaLW2WrQ3vMJ
* Fix sync category upload, empty-state refresh, badge reactivity, and preferences syntax
- Add categoryId to SyncService pending transaction upload payload
- Replace non-scrollable Center with ListView for empty filter state so
RefreshIndicator works when no transactions match
- Use listen:true for CategoriesProvider in badge display so badges
rebuild when categories finish loading
- Fix missing closing brace in PreferencesService.setShowCategoryFilter
https://claude.ai/code/session_01Dgj8tYrCkoUaLW2WrQ3vMJ
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* feat(mobile): render assistant messages as markdown, keep user text plain
Add flutter_markdown dependency and conditionally render chat bubbles:
- User messages use plain Text to avoid formatting markdown characters
- Assistant messages use MarkdownBody with styled headings, bold, italic,
lists and code blocks matching the existing color scheme
- Bump Dart SDK constraint to >=3.3.0 to satisfy flutter_markdown 0.7.2
* fix(mobile): address markdown rendering review comments
- Extract MarkdownStyleSheet into _markdownStyle() helper to avoid
rebuilding TextStyles on every message render
- Replace deprecated imageBuilder with sizedImageBuilder; block http/https
image URIs to prevent unsolicited remote fetches from AI-generated content
- Commit updated pubspec.lock with flutter_markdown 0.7.2 resolved
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix tests
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
* feat(mobile): Add animated TypingIndicator widget for AI chat responses
Replaces the static CircularProgressIndicator + "AI is thinking..." text
with an animated TypingIndicator showing pulsing dots while the AI generates
a response. Respects the app color scheme so it works in light and dark themes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix: Normalize stagger progress to [0,1) in TypingIndicator to prevent negative opacity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(mobile): fix typing indicator visibility and run pub get
The typing indicator was only visible for the duration of the HTTP
POST (~instant) because it was tied to `isSendingMessage`. It now
tracks the full AI response lifecycle via a new `isWaitingForResponse`
state that stays true through polling until the response stabilises.
- Add `isWaitingForResponse` to ChatProvider; set on poll start,
clear on poll stop with notifyListeners so the UI reacts correctly
- Move TypingIndicator inside the ListView as an assistant bubble
so it scrolls naturally with the conversation
- Add provider listener that auto-scrolls on every update while
waiting for a response
- Redesign TypingIndicator: 3-dot sequential bounce animation
(classic chat style) replacing the simultaneous fade
* feat(mobile): overhaul new-chat flow and fix typing indicator bugs
chat is created lazily
on first send, eliminating all pre-conversation flashes and crashes
- Inject user message locally into _currentChat immediately on createChat
so it renders before the first poll completes
- Hide thinking indicator the moment the first assistant content arrives
(was waiting one extra 2s poll cycle before disappearing)
- Fix double-spinner on new chat: remove manual showDialog spinner and
use a local _isCreating flag on the FAB instead
* fix(mboile) : address PR review — widget lifecycle safety and new-chat regression
* Fic(mobile): Add mounted check in post-frame callback
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Feature: Add Theme selection in Settings page
* Fix: Theme provider exception handling.
* feat(mobile): Show theme selection option in settings screen.
* BuildID version 9
---------
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* Move debug logs button from Home to Settings page, remove refresh/logout from Home AppBar
- Remove Debug Logs, Refresh, and Sign Out buttons from DashboardScreen AppBar
- Add Debug Logs ListTile entry in SettingsScreen under app info section
- Remove unused _handleLogout method from DashboardScreen
- Remove unused log_viewer_screen.dart import from DashboardScreen
https://claude.ai/code/session_017XQZdaEwUuRS75tJMcHzB9
* Fix home page double AppBar inconsistency with settings/more pages
The DashboardScreen had its own AppBar with a sync success icon, while
MainNavigationScreen already provides a shared AppBar (logo + settings)
for all tabs. This caused the home page to render a double top bar,
inconsistent with the settings and more screens which have no extra
AppBar. Remove the dashboard's AppBar and move the sync indicator into
the body as an inline banner.
https://claude.ai/code/session_0155XXsvt5zKLBpasmdkhPiF
* Fix sync success cloud icon not appearing after sync
The cloud icon only showed when pending transaction count decreased
(local→server uploads). For normal pull-to-refresh syncs that download
from server, pendingCount stays at 0 so the icon never triggered.
Fix by also detecting when TransactionsProvider.isLoading transitions
from true to false (any sync completion), and by triggering the icon
directly after successful manual sync instead of showing a redundant
snackbar.
https://claude.ai/code/session_0155XXsvt5zKLBpasmdkhPiF
* Address PR review: fix Timer leak, sync error check, false triggers
1. Timer leak (CodeRabbit): Replace Future.delayed with a cancellable
Timer field (_syncSuccessTimer). Cancel existing timer before
starting a new one, and clean up in dispose().
2. Sync error not checked (CodeRabbit): _performManualSync now checks
transactionsProvider.error after syncTransactions() returns. Shows
error SnackBar on failure instead of false success indicator.
3. False positive triggers (Codex): Remove isLoading transition
detection from _onTransactionsChanged since isLoading also toggles
for fetchTransactions (non-sync paths). Keep only pendingDecreased
for background sync detection; manual sync uses direct call path.
https://claude.ai/code/session_0155XXsvt5zKLBpasmdkhPiF
* Update mobile/lib/screens/dashboard_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>
---------
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Remove Debug Logs, Refresh, and Sign Out buttons from DashboardScreen AppBar
- Add Debug Logs ListTile entry in SettingsScreen under app info section
- Remove unused _handleLogout method from DashboardScreen
- Remove unused log_viewer_screen.dart import from DashboardScreen
https://claude.ai/code/session_017XQZdaEwUuRS75tJMcHzB9
Co-authored-by: Claude <noreply@anthropic.com>
* Add GET /api/v1/summary endpoint and display net worth on mobile home
- Create SummaryController that leverages existing BalanceSheet model to
return net_worth, assets, and liabilities (with currency conversion)
- Add SummaryService in mobile to call the new endpoint
- Update AccountsProvider to fetch summary data alongside accounts
- Replace "Net Worth — coming soon" placeholder in NetWorthCard with
the actual formatted net worth value from the API
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Bump mobile version to 0.7.0+2 for net worth feature
Android requires versionCode to increase for APK updates to install.
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Fix version to 0.6.9+2
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Rename /api/v1/summary to /api/v1/balance_sheet
Address PR #1145 review feedback:
- Rename SummaryController to BalanceSheetController to align with the
BalanceSheet domain model and follow existing API naming conventions
- Rename mobile SummaryService to BalanceSheetService with updated endpoint
- Fix unsafe type casting: use `as String?` instead of `as String` for
currency field to handle null safely
- Fix balance sheet fetch to run independently of account sync success,
so net worth displays even with cached/offline accounts
- Update tests to use API key authentication instead of Doorkeeper OAuth
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Add rswag OpenAPI spec, fix error message, add docstrings, revert version bump
- Add spec/requests/api/v1/balance_sheet_spec.rb with Money and
BalanceSheet schemas in swagger_helper.rb
- Replace raw e.toString() in balance_sheet_service.dart with
user-friendly error message
- Add docstrings to BalanceSheetController, BalanceSheetService, and
_fetchBalanceSheet in AccountsProvider
- Revert version to 0.6.9+1 (no version change in this PR)
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Fix route controller mapping and secret scanner trigger
- Add controller: :balance_sheet to singular resource route, since
Rails defaults to plural BalanceSheetsController otherwise
- Use ApiKey.generate_secure_key + plain_key pattern in test to avoid
pipelock secret scanner flagging display_key as a credential
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Exclude balance sheet test from pipelock secret scanner
False positive: test creates ephemeral API keys via
ApiKey.generate_secure_key for integration testing, not real credentials.
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Revert pipelock exclusion; use display_key pattern in test
Revert the pipelock.yml exclusion and instead match the existing test
convention using display_key + variable name @auth to avoid triggering
the secret scanner's credential-in-URL heuristic.
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Fix rswag scope and show stale balance sheet indicator
- Use read_write scope in rswag spec to match other API specs convention
- Add isBalanceSheetStale flag to AccountsProvider: set on fetch failure,
cleared on success, preserves last known values
- Show amber "Outdated" badge and yellow net worth text in NetWorthCard
when balance sheet data is stale, so users know the displayed value
may not reflect the latest state
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Use theme colorScheme instead of hardcoded amber for stale indicator
Replace Colors.amber with colorScheme.secondaryContainer (badge bg)
and colorScheme.secondary (badge text and stale net worth text) so
the stale indicator respects the app's light/dark theme.
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Check for pending invitations before creating new Family during SSO account creation
When a user signs in via Google SSO and doesn't have an account yet, the
system now checks for pending invitations before creating a new Family.
If an invitation exists, the user joins the invited family instead.
- OidcAccountsController: check Invitation.pending in link/create_user
- API AuthController: check pending invitations in sso_create_account
- SessionsController: pass has_pending_invitation to mobile SSO callback
- Web view: show "Accept Invitation" button when invitation exists
- Flutter: show "Accept Invitation" tab/button when invitation pending
https://claude.ai/code/session_019Tr6edJa496V1ErGmsbqFU
* Fix external assistant tests: clear Settings cache to prevent test pollution
The tests relied solely on with_env_overrides to clear configuration, but
rails-settings-cached may retain stale Setting values across tests when
the cache isn't explicitly invalidated. Ensure both ENV vars AND Setting
values are cleared with Setting.clear_cache before assertions.
https://claude.ai/code/session_019Tr6edJa496V1ErGmsbqFU
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add Google SSO onboarding flow for Flutter mobile app
Previously, mobile users attempting Google SSO without a linked OIDC
identity received an error telling them to link from the web app first.
This adds the same account linking/creation flow that exists on the PWA.
Backend changes:
- sessions_controller: Cache pending OIDC auth with a linking code and
redirect back to the app instead of returning an error
- api/v1/auth_controller: Add sso_link endpoint to link Google identity
to an existing account via email/password, and sso_create_account
endpoint to create a new SSO-only account (respects JIT config)
- routes: Add POST auth/sso_link and auth/sso_create_account
Flutter changes:
- auth_service: Detect account_not_linked callback status, add ssoLink
and ssoCreateAccount API methods
- auth_provider: Track SSO onboarding state, expose linking/creation
methods and cancelSsoOnboarding
- sso_onboarding_screen: New screen with tabs to link existing account
or create new account, pre-filled with Google profile data
- main.dart: Show SsoOnboardingScreen when ssoOnboardingPending is true
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Fix broken SSO tests: use MemoryStore cache and correct redirect param
- Sessions test: check `status` param instead of `error` since
handle_mobile_sso_onboarding sends linking info with status key
- API auth tests: swap null_store for MemoryStore so cache-based
linking code validation works in test environment
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Delay linking-code consumption until SSO link/create succeeds
Split validate_and_consume_linking_code into validate_linking_code
(read-only) and consume_linking_code! (delete). The code is now only
consumed after password verification (sso_link) or successful user
save (sso_create_account), so recoverable errors no longer burn the
one-time code and force a full Google SSO roundtrip.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Make linking-code consumption atomic to prevent race conditions
Move consume_linking_code! (backed by Rails.cache.delete) to after
recoverable checks (bad password, policy rejection) but before
side-effecting operations (identity/user creation). Only the first
caller to delete the cache key gets true, so concurrent requests
with the same code cannot both succeed.
- sso_link: consume after password auth, before OidcIdentity creation
- sso_create_account: consume after allow_account_creation check,
before User creation
- Bad password still preserves the code for retry
- Add single-use regression tests for both endpoints
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Add missing sso_create_account test coverage for blank code and validation failure
- Test blank linking_code returns 400 (bad_request) with proper error
- Test duplicate email triggers user.save failure → 422 with validation errors
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Verify cache payload in mobile SSO onboarding test with MemoryStore
The test environment uses :null_store which silently discards cache
writes, so handle_mobile_sso_onboarding's Rails.cache.write was never
verified. Swap in a MemoryStore for this test and assert the full
cached payload (provider, uid, email, name, device_info,
allow_account_creation) at the linking_code key from the redirect URL.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Add rswag/OpenAPI specs for sso_link and sso_create_account endpoints
POST /api/v1/auth/sso_link: documents linking_code + email/password
params, 200 (tokens), 400 (missing code), 401 (invalid creds/expired).
POST /api/v1/auth/sso_create_account: documents linking_code +
optional first_name/last_name params, 200 (tokens), 400 (missing code),
401 (expired code), 403 (creation disabled), 422 (validation errors).
Note: RAILS_ENV=test bundle exec rake rswag:specs:swaggerize should be
run to regenerate docs/api/openapi.yaml once the runtime environment
matches the Gemfile Ruby version.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Preserve OIDC issuer through mobile SSO onboarding flow
handle_mobile_sso_onboarding now caches the issuer from
auth.extra.raw_info.iss so it survives the linking-code round trip.
build_omniauth_hash populates extra.raw_info.iss from the cached
issuer so OidcIdentity.create_from_omniauth stores it correctly.
Previously the issuer was always nil for mobile SSO-created identities
because build_omniauth_hash passed an empty raw_info OpenStruct.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Block MFA users from bypassing second factor via sso_link
sso_link authenticated with email/password but never checked
user.otp_required?, allowing MFA users to obtain tokens without
a second factor. The mobile SSO callback already rejects MFA users
with "mfa_not_supported"; apply the same guard in sso_link before
consuming the linking code or creating an identity.
Returns 401 with mfa_required: true, consistent with the login
action's MFA response shape.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Fix NoMethodError in SSO link MFA test
Replace non-existent User.generate_otp_secret class method with
ROTP::Base32.random(32), matching the pattern used in User#setup_mfa!.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Assert linking code survives rejected SSO create account
Add cache persistence assertion to "should reject SSO create account
when not allowed" test, verifying the linking code is not consumed on
the 403 path. This mirrors the pattern used in the invalid-password
sso_link test.
The other rejection tests (expired/missing linking code) don't have a
valid cached code to check, so no assertion is needed there.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Chat improvements
* Delete/reset account via API for Flutter app
* Fix tests.
* Add "contact us" to settings
* Update mobile/lib/screens/chat_conversation_screen.dart
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Juan José Mata <jjmata@jjmata.com>
* Improve LLM special token detection
* Deactivated user shouldn't have API working
* Fix tests
* API-Key usage
* Flutter app launch failure on no network
* Handle deletion/reset delays
* Local cached data may become stale
* Use X-Api-Key correctly!
---------
Signed-off-by: Juan José Mata <jjmata@jjmata.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Wire ui layout and AI flags into mobile auth
Include ui_layout and ai_enabled in mobile login/signup/SSO payloads,
add an authenticated endpoint to enable AI from Flutter, and gate
mobile navigation based on intro layout and AI consent flow.
* Linter
* Ensure write scope on enable_ai
* Make sure AI is available before enabling it
* Test improvements
* PR comment
* Fix review issues: test assertion bug, missing coverage, and Dart defaults (#985)
- Fix login test to use ai_enabled? (method) instead of ai_enabled (column)
to match what mobile_user_payload actually serializes
- Add test for enable_ai when ai_available? returns false (403 path)
- Default aiEnabled to false when user is null in AuthProvider to avoid
showing AI as available before authentication completes
- Remove extra blank lines in auth_provider.dart and auth_service.dart
https://claude.ai/code/session_01LEYYmtsDBoqizyihFtkye4
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add mobile SSO support to sessions controller
Add /auth/mobile/:provider route and mobile_sso_start action that
captures device params in session and renders an auto-submitting POST
form to OmniAuth (required by omniauth-rails_csrf_protection).
Modify openid_connect callback to detect mobile_sso session, issue
Doorkeeper tokens via MobileDevice, and redirect to sureapp://oauth/callback
with tokens. Handles MFA users and unlinked accounts with error redirects.
Validates provider name against configured SSO providers and device info
before proceeding.
* Add SSO auth flow to Flutter service and provider
Add buildSsoUrl() and handleSsoCallback() to AuthService for
constructing the mobile SSO URL and parsing tokens from the deep
link callback.
Add startSsoLogin() and handleSsoCallback() to AuthProvider for
launching browser-based SSO and processing the redirect.
* Register deep link listener for SSO callback
Listen for sureapp://oauth/* deep links via app_links package,
handling both cold start (getInitialLink) and warm (uriLinkStream)
scenarios. Routes callbacks to AuthProvider.handleSsoCallback().
* Add Google Sign-In button to Flutter login screen
Add "or" divider and outlined Google Sign-In button that triggers
browser-based SSO via startSsoLogin('google_oauth2').
Add app_links and url_launcher dependencies to pubspec.yaml.
* Fix mobile SSO failure handling to redirect back to app
When OmniAuth fails during mobile SSO flow, redirect to
sureapp://oauth/callback with the error instead of the web login page.
Cleans up mobile_sso session data on failure.
* Address PR review feedback for mobile SSO flow
- Use strong params for device info in mobile_sso_start
- Guard against nil session data in handle_mobile_sso_callback
- Add error handling for AppLinks initialization and stream
- Handle launchUrl false return value in SSO login
- Use user-friendly error messages instead of exposing exceptions
- Reject empty token strings in SSO callback validation
* Consolidate mobile device token logic into MobileDevice model
Extract duplicated device upsert and token issuance code from
AuthController and SessionsController into MobileDevice. Add
CALLBACK_URL constant and URL builder helpers to eliminate repeated
deep-link strings. Add mobile SSO integration tests covering the
full flow, MFA rejection, unlinked accounts, and failure handling.
* Fix CI: resolve Brakeman redirect warnings and rubocop empty line
Move mobile SSO redirect into a private controller method with an
inline string literal so Brakeman can statically verify the target.
Remove unused URL builder helpers from MobileDevice. Fix extra empty
line at end of AuthController class body.
* Use authorization code exchange for mobile SSO and add signup error handling
Replace passing plaintext tokens in mobile SSO redirect URLs with a
one-time authorization code pattern. Tokens are now stored server-side
in Rails.cache (5min TTL) and exchanged via a secure POST to
/api/v1/auth/sso_exchange. Also wraps device/token creation in the
signup action with error handling and sanitizes device error messages.
* Add error handling for login device registration and blank SSO code guard
* Address PR #860 review: fix SSO race condition, add OpenAPI spec, and cleanup
- Fix race condition in sso_exchange by checking Rails.cache.delete return
value to ensure only one request can consume an authorization code
- Use strong parameters (params.require) for sso_exchange code param
- Move inline HTML from mobile_sso_start to a proper view template
- Clear stale session[:mobile_sso] flag on web login paths to prevent
abandoned mobile flows from hijacking subsequent web SSO logins
- Add OpenAPI/rswag spec for all auth API endpoints
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix mobile SSO test to match authorization code exchange pattern
The test was asserting tokens directly in the callback URL, but the code
uses an authorization code exchange pattern. Updated to exchange the code
via the sso_exchange API endpoint. Also swaps in a MemoryStore for this
test since the test environment uses null_store which discards writes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor mobile OAuth to use single shared application
Replace per-device Doorkeeper::Application creation with a shared
"Sure Mobile" OAuth app. Device tracking uses mobile_device_id on
access tokens instead of oauth_application_id on mobile_devices.
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Flutter web support and web-safe storage
* Update mobile/web/index.html
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Juan José Mata <jjmata@jjmata.com>
* Product name instead of placeholder
---------
Signed-off-by: Juan José Mata <jjmata@jjmata.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* feat(mobile): optimize asset/liability display with filters
- Add NetWorthCard widget with placeholder for future net worth API
- Add side-by-side Assets/Liabilities display with tap-to-filter
- Implement CurrencyFilter widget for multi-select currency filtering
- Replace old _SummaryCard with new unified design
- Remove _CollapsibleSectionHeader in favor of filter-based navigation
The net worth section shows a placeholder as the API endpoint is not yet available.
Users can now filter accounts by type (assets/liabilities) and by currency.
https://claude.ai/code/session_01W8cQSCzmgTmTqwRJ8Ycpx3
* fix(mobile): remove unused variables and add const
- Remove unused _totalAssets, _totalLiabilities, _getPrimaryCurrency
- Add const to Text('All') widget
https://claude.ai/code/session_01W8cQSCzmgTmTqwRJ8Ycpx3
* feat(mobile): enhance dashboard with icons, long-press breakdown, and grouped view
- NetWorthCard: replace text labels with trending icons, add colored
bottom borders for asset (green) and liability (red) sections
- Add long-press gesture on asset/liability areas to show full currency
breakdown in a bottom sheet popup
- Add collapsible account type grouping (Crypto, Bank, Investment, etc.)
with type-specific icons and expand/collapse headers
- Add PreferencesService for persisting display settings
- Add "Group by Account Type" toggle in Settings screen
- Wire settings change to dashboard via GlobalKey for live updates
https://claude.ai/code/session_01W8cQSCzmgTmTqwRJ8Ycpx3
* refactor(mobile): remove welcome header from dashboard
Strip the Welcome greeting and subtitle to let the financial
overview take immediate focus.
https://claude.ai/code/session_01W8cQSCzmgTmTqwRJ8Ycpx3
* feat(mobile): compact filter buttons with scroll-wheel currency switcher
- Remove trending icons from asset/liability filter buttons
- Increase amount font size to titleMedium bold
- Reduce Net Worth section and filter button padding
- Show single currency at a time with ListWheelScrollView for
scrolling between currencies (wheel-picker style)
- Absorb scroll events via NotificationListener to prevent
triggering pull-to-refresh
- Keep icons in the long-press currency breakdown popup
https://claude.ai/code/session_01W8cQSCzmgTmTqwRJ8Ycpx3
* fix(mobile): prevent bottom sheet overflow with ConstrainedBox
Use ConstrainedBox + ListView.separated with shrinkWrap for the
currency breakdown popup. Few currencies: sheet sizes to content.
Many currencies: caps at 50% screen height and scrolls.
Also add isScrollControlled and useSafeArea to showModalBottomSheet.
https://claude.ai/code/session_01W8cQSCzmgTmTqwRJ8Ycpx3
* fix(mobile): reload dashboard preferences on any tab switch to Home
Previously only reloaded when navigating directly from Settings to
Home. Now reloads whenever the Home tab is selected, covering paths
like Settings -> More -> Home.
https://claude.ai/code/session_01W8cQSCzmgTmTqwRJ8Ycpx3
* chore(mobile): simplify net worth placeholder to single line
Replace the two-line Net Worth / -- placeholder with a compact
"Net Worth — coming soon" label while the API endpoint is pending.
https://claude.ai/code/session_01W8cQSCzmgTmTqwRJ8Ycpx3
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(mobile): Add transaction display on calendar date tap
Implement two-tap interaction for calendar dates:
- First tap selects a date (highlighted with thicker primary color border)
- Second tap on same date shows AlertDialog with transactions for that day
Each transaction displays with:
- Color-coded icon (red minus for expenses, green plus for income)
- Transaction name as title
- Notes as subtitle (if present)
- Amount with color matching expense/income
Selection is cleared when changing account, account type, or month.
https://claude.ai/code/session_019m7ZrCakU6h9xLwD1NTx9i
* feat(mobile): optimize asset/liability display with filters
- Add NetWorthCard widget with placeholder for future net worth API
- Add side-by-side Assets/Liabilities display with tap-to-filter
- Implement CurrencyFilter widget for multi-select currency filtering
- Replace old _SummaryCard with new unified design
- Remove _CollapsibleSectionHeader in favor of filter-based navigation
The net worth section shows a placeholder as the API endpoint is not yet available.
Users can now filter accounts by type (assets/liabilities) and by currency.
https://claude.ai/code/session_01W8cQSCzmgTmTqwRJ8Ycpx3
* fix(mobile): remove unused variables and add const
- Remove unused _totalAssets, _totalLiabilities, _getPrimaryCurrency
- Add const to Text('All') widget
https://claude.ai/code/session_01W8cQSCzmgTmTqwRJ8Ycpx3
* feat(mobile): enhance dashboard with icons, long-press breakdown, and grouped view
- NetWorthCard: replace text labels with trending icons, add colored
bottom borders for asset (green) and liability (red) sections
- Add long-press gesture on asset/liability areas to show full currency
breakdown in a bottom sheet popup
- Add collapsible account type grouping (Crypto, Bank, Investment, etc.)
with type-specific icons and expand/collapse headers
- Add PreferencesService for persisting display settings
- Add "Group by Account Type" toggle in Settings screen
- Wire settings change to dashboard via GlobalKey for live updates
https://claude.ai/code/session_01W8cQSCzmgTmTqwRJ8Ycpx3
* refactor(mobile): remove welcome header from dashboard
Strip the Welcome greeting and subtitle to let the financial
overview take immediate focus.
https://claude.ai/code/session_01W8cQSCzmgTmTqwRJ8Ycpx3
* feat(mobile): compact filter buttons with scroll-wheel currency switcher
- Remove trending icons from asset/liability filter buttons
- Increase amount font size to titleMedium bold
- Reduce Net Worth section and filter button padding
- Show single currency at a time with ListWheelScrollView for
scrolling between currencies (wheel-picker style)
- Absorb scroll events via NotificationListener to prevent
triggering pull-to-refresh
- Keep icons in the long-press currency breakdown popup
https://claude.ai/code/session_01W8cQSCzmgTmTqwRJ8Ycpx3
* feat: Add API key login option to mobile app
Add a "Via API Key Login" button on the login screen that opens a
dialog for entering an API key. The API key is validated by making a
test request to /api/v1/accounts with the X-Api-Key header, and on
success is persisted in secure storage. All HTTP services now use a
centralized ApiConfig.getAuthHeaders() helper that returns the correct
auth header (X-Api-Key or Bearer) based on the current auth mode.
https://claude.ai/code/session_01DnyCzdMjVpSsbBZK3XbzUH
* fix: Improve API key dialog context handling and controller disposal
- Use outer context for SnackBar so it displays on the main screen
instead of behind the dialog
- Explicitly dispose TextEditingController to prevent memory leaks
- Close dialog on failure before showing error SnackBar for better UX
- Avoid StatefulBuilder context parameter shadowing
https://claude.ai/code/session_01DnyCzdMjVpSsbBZK3XbzUH
* fix: Use user-friendly error message in API key login catch block
Log the technical exception details via LogService.instance.error and
show a generic "Unable to connect" message to the user instead of
exposing the raw exception string.
https://claude.ai/code/session_01DnyCzdMjVpSsbBZK3XbzUH
* fix: Use getValidAccessToken() in connectivity banner sync button
Replace direct authProvider.tokens?.accessToken access with
getValidAccessToken() so the Sync Now button works in API-key
auth mode where _tokens is null.
https://claude.ai/code/session_01DnyCzdMjVpSsbBZK3XbzUH
* Revert "fix: Use getValidAccessToken() in connectivity banner sync button"
This reverts commit 7015c160f0.
* Reapply "fix: Use getValidAccessToken() in connectivity banner sync button"
This reverts commit b29e010de3.
* fix: Use getValidAccessToken() in connectivity banner sync button
Replace direct authProvider.tokens?.accessToken access with
getValidAccessToken() so the Sync Now button works in API-key
auth mode where _tokens is null.
https://claude.ai/code/session_01DnyCzdMjVpSsbBZK3XbzUH
* fix(mobile): prevent bottom sheet overflow with ConstrainedBox
Use ConstrainedBox + ListView.separated with shrinkWrap for the
currency breakdown popup. Few currencies: sheet sizes to content.
Many currencies: caps at 50% screen height and scrolls.
Also add isScrollControlled and useSafeArea to showModalBottomSheet.
https://claude.ai/code/session_01W8cQSCzmgTmTqwRJ8Ycpx3
* fix: Prevent multiple syncs and handle auth errors in connectivity banner
Set _isSyncing immediately on tap to disable the button during token
refresh, wrap getValidAccessToken() in try/catch with user-facing error
snackbar, and await _handleSync so errors propagate correctly.
https://claude.ai/code/session_01GgVgjqwyXhWMZN3eWfaMCk
---------
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Implement two-tap interaction for calendar dates:
- First tap selects a date (highlighted with thicker primary color border)
- Second tap on same date shows AlertDialog with transactions for that day
Each transaction displays with:
- Color-coded icon (red minus for expenses, green plus for income)
- Transaction name as title
- Notes as subtitle (if present)
- Amount with color matching expense/income
Selection is cleared when changing account, account type, or month.
https://claude.ai/code/session_019m7ZrCakU6h9xLwD1NTx9i
Co-authored-by: Claude <noreply@anthropic.com>
* feat: Add API key login option to mobile app
Add a "Via API Key Login" button on the login screen that opens a
dialog for entering an API key. The API key is validated by making a
test request to /api/v1/accounts with the X-Api-Key header, and on
success is persisted in secure storage. All HTTP services now use a
centralized ApiConfig.getAuthHeaders() helper that returns the correct
auth header (X-Api-Key or Bearer) based on the current auth mode.
https://claude.ai/code/session_01DnyCzdMjVpSsbBZK3XbzUH
* fix: Improve API key dialog context handling and controller disposal
- Use outer context for SnackBar so it displays on the main screen
instead of behind the dialog
- Explicitly dispose TextEditingController to prevent memory leaks
- Close dialog on failure before showing error SnackBar for better UX
- Avoid StatefulBuilder context parameter shadowing
https://claude.ai/code/session_01DnyCzdMjVpSsbBZK3XbzUH
* fix: Use user-friendly error message in API key login catch block
Log the technical exception details via LogService.instance.error and
show a generic "Unable to connect" message to the user instead of
exposing the raw exception string.
https://claude.ai/code/session_01DnyCzdMjVpSsbBZK3XbzUH
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix mobile app to fetch all transactions with pagination
The mobile app was only fetching 25 transactions per account because:
1. TransactionsService didn't pass pagination parameters to the API
2. The backend defaults to 25 records per page when no per_page is specified
3. SyncService didn't implement pagination to fetch all pages
Changes:
- Updated TransactionsService.getTransactions() to accept page and perPage parameters
- Modified the method to extract and return pagination metadata from API response
- Updated SyncService.syncFromServer() to fetch all pages (up to 100 per page)
- Added pagination loop to continue fetching until all pages are retrieved
- Enhanced logging to show pagination progress
This ensures users see all their transactions in the mobile app, not just the first 25.
* Add clear local data feature and enhanced sync logging
Added features:
1. Clear Local Data button in Settings
- Allows users to clear all cached transactions and accounts
- Shows confirmation dialog before clearing
- Displays success/error feedback
2. Enhanced sync logging for debugging
- Added detailed logs in syncFromServer to track pagination
- Shows page-by-page progress with transaction counts
- Logs pagination metadata (total pages, total count, etc.)
- Tracks upsert progress every 50 transactions
- Added clear section markers for easier log reading
3. Simplified upsertTransactionFromServer logging
- Removed verbose debug logs to reduce noise
- Keeps only essential error/warning logs
This will help users troubleshoot sync issues by:
- Clearing stale data and forcing a fresh sync
- Providing detailed logs to identify where sync might fail
* Fix transaction accountId parsing from API response
The mobile app was only showing 25 transactions per account because:
- The backend API returns account info in nested format: {"account": {"id": "xxx"}}
- The mobile Transaction model expected flat format: {"account_id": "xxx"}
- When parsing, accountId was always empty, so database queries by account_id returned incomplete results
Changes:
1. Updated Transaction.fromJson to handle both formats:
- New format: {"account": {"id": "xxx", "name": "..."}}
- Old format: {"account_id": "xxx"} (for backward compatibility)
2. Fixed classification/nature field parsing:
- Backend sends "classification" field (income/expense)
- Mobile uses "nature" field
- Now handles both fields correctly
3. Added debug logging to identify empty accountId issues:
- Logs first transaction's accountId when syncing
- Counts and warns about transactions with empty accountId
- Shows critical errors when trying to save with empty accountId
This ensures all transactions from the server are correctly associated with their accounts in the local database.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Calendar View:
Monthly calendar displaying daily balance changes for selected account
Account selection dropdown to switch between different accounts
Month navigation with previous/next buttons
Visual indicators (green for income, red for expenses)
Monthly profit/loss summary at the top
Auto-calculation of daily transaction totals
Recent Transactions:
View recent N transactions across all accounts
Configurable display limit (10/20/50/100 transactions)
Pull-to-refresh functionality
Transaction details including account name, date, amount, and notes
Visual distinction between income and expenses
Sorted by date (most recent first)
Implementation details:
Created MoreScreen as menu hub for new features
Replaced PlaceholderScreen with functional MoreScreen
Leverages existing Provider pattern for state management
Uses offline-first approach with existing data providers
Full Chinese localization
Material Design 3 compliant UI
Files added:
mobile/lib/screens/more_screen.dart
mobile/lib/screens/calendar_screen.dart
mobile/lib/screens/recent_transactions_screen.dart
Files modified:
mobile/lib/screens/main_navigation_screen.dart
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>
Co-authored-by: dwvwdv <dwvwdv@protonmail.com>
Backend fixes:
- Fix duplicate AssistantResponseJob triggering causing duplicate AI responses
- UserMessage model already handles job triggering via after_create_commit callback
- Remove redundant job enqueue in chats_controller and messages_controller
Mobile app features:
- Implement complete AI chat interface and conversation management
- Add Chat, Message, and ToolCall data models
- Add ChatProvider for state management with polling mechanism
- Add ChatService to handle all chat-related API requests
- Add chat list screen (ChatListScreen)
- Add conversation detail screen (ChatConversationScreen)
- Refactor navigation structure with bottom navigation bar (MainNavigationScreen)
- Add settings screen (SettingsScreen)
- Optimize TransactionsProvider to support account filtering
Technical details:
- Implement message polling mechanism for real-time AI responses
- Support chat creation, deletion, retry and other operations
- Integrate Material Design 3 design language
- Improve user experience and error handling
Co-authored-by: dwvwdv <dwvwdv@protonmail.com>
* 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.
* Add offline-first architecture for mobile app
Implement comprehensive offline functionality for the Sure mobile app:
- SQLite local storage for transactions and accounts
- Network connectivity detection and monitoring
- Offline transaction creation with pending sync status
- Automatic synchronization when network is restored
- Visual indicators for offline status and pending syncs
New Services:
- ConnectivityService: Real-time network status monitoring
- DatabaseHelper: SQLite database management with schema
- OfflineStorageService: High-level API for local data operations
- SyncService: Bidirectional sync between local DB and server
New Models:
- OfflineTransaction: Extended transaction model with sync status
- SyncStatus enum: Track transaction sync state (synced/pending/failed)
New UI Components:
- ConnectivityBanner: Shows offline status and sync actions
- SyncStatusBadge: Displays sync status on transactions
Updated Components:
- TransactionsProvider: Offline-first data fetching and caching
- Main app: Wire up connectivity and sync services
- Dashboard: Display connectivity banner
Dependencies Added:
- sqflite: SQLite database
- connectivity_plus: Network status detection
- uuid: Generate local transaction IDs
- path: Database path utilities
The app now works seamlessly offline, saving all transactions locally
and automatically syncing when connectivity is restored.
* Fix lint errors and warnings in offline features
- Fix connectivity_service to handle ConnectivityResult (not List)
- Remove unused imports in database_helper
- Replace deprecated withOpacity with withValues in sync_status_badge
- Use super parameters in OfflineTransaction constructor
* Add offline support for AccountsProvider
- Implement offline-first architecture for accounts
- Load cached accounts from SQLite first for instant display
- Sync with server when online and save to cache
- Wire up ConnectivityService to AccountsProvider
- Show cached data even when offline
This fixes the issue where accounts couldn't be loaded offline.
* Add debug logging to track offline transaction sync issues
- Add detailed logging in TransactionsProvider.fetchTransactions
- Add logging in SyncService.syncFromServer
- Track transaction counts at each step
- Log server responses and database operations
This will help identify why transaction lists are empty.
* Fix critical offline sync issues and add auto-sync
Major fixes:
1. Fix accountId missing issue when syncing from server
- Server may not return account_id in transaction JSON
- Now use provided accountId parameter as fallback
- Ensures transactions are saved with correct account_id
2. Add automatic sync when connectivity restored
- Store last access token for auto-sync
- Automatically upload pending transactions when back online
- Prevent duplicate auto-sync attempts
3. Enhanced debug logging
- Track accountId from server vs provided parameter
- Log transaction upsert operations
- Better error tracking
This fixes:
- Empty transaction lists when viewing account details
- Offline transactions not auto-syncing when online
- Data not being properly saved to local database
* Add comprehensive debug logging for transaction storage
Added detailed logging to track:
- Account ID values from server vs provided parameter
- Effective account ID being used for storage
- Database insert/update operations
- Number of transactions retrieved from database
- Sample account IDs from stored transactions
This will help identify:
1. Whether server returns account_id in transaction JSON
2. If fallback accountId parameter is being used correctly
3. What values are actually stored in the database
4. Why queries might not be finding transactions
* Add detailed database query logging
Added SQL query logging to DatabaseHelper:
- Log all INSERT operations with account_id values
- Log SELECT queries with WHERE conditions
- Log result counts from queries
This will show:
- Exact account_id values being stored in database
- SQL WHERE clauses being used for queries
- Whether queries are finding matching records
Combined with OfflineStorageService logs, this gives complete
visibility into the data flow from server to database to UI.
* Add debug log viewer screen
- Create LogService for centralized log management
- Add LogViewerScreen with filtering and export capabilities
- Add debug logs button to dashboard
- Support log levels: ERROR, WARNING, INFO, DEBUG
- Features: filter by level, auto-scroll, copy to clipboard, clear logs
- Keep last 1000 log entries
* Add visual indicators for offline transactions and sync success
Implemented the following features:
1. Pending transactions are now displayed with 50% opacity (gray)
2. Sync status badges (orange sync icon) show next to pending transaction amounts
3. Green cloud icon appears in dashboard AppBar when sync completes successfully
4. Auto-hide sync success indicator after 3 seconds with fade animation
5. Transaction list now uses offlineTransactions to access sync status
Changes:
- transactions_list_screen.dart: Display pending status with opacity and badges
- dashboard_screen.dart: Add sync success indicator with listener
- transactions_provider.dart: Already had offline-first architecture from previous commit
* Fix offline transaction creation and add comprehensive logging
This commit fixes all three reported issues:
1. Log viewer now showing logs
- Added test log when log viewer opens
- Added LogService to all critical services
- Comprehensive logging throughout the stack
2. Offline transaction creation now working
- TransactionFormScreen now uses TransactionsProvider (offline-first)
- Previously was calling HTTP API directly, bypassing offline storage
- Transactions now save to SQLite immediately, then sync in background
3. Sync and cloud icon improvements
- Dashboard already has sync success indicator from previous commit
- Added connectivity logging to track online/offline state changes
- Sync will trigger automatically when connectivity restored
Changes:
- transaction_form_screen.dart: Use TransactionsProvider instead of TransactionsService
- offline_storage_service.dart: Add comprehensive logging with LogService
- connectivity_service.dart: Add logging for connectivity state changes
- log_viewer_screen.dart: Add test log on screen open
- main.dart: Add app startup log
All operations now properly logged and can be viewed in the debug log viewer.
* Fix transaction disappearing after sync and add manual sync
Critical fixes:
1. Fixed syncTransactionsFromServer deleting uploaded transactions
- Changed clearTransactions() to clearSyncedTransactions()
- Now only clears synced transactions, preserves pending/failed ones
- Added comprehensive logging to track sync process
2. Converted dashboard refresh button to manual sync
- Now triggers full sync: upload pending, download from server
- Shows progress indicator during sync
- Displays success/error messages
Technical details:
- database_helper.dart: Added clearSyncedTransactions() method
- offline_storage_service.dart: Use clearSyncedTransactions() in syncTransactionsFromServer()
- sync_service.dart: Added detailed logging to all sync operations
- dashboard_screen.dart: Refresh button now calls performManualSync()
Previous issue:
When connectivity restored, pending transactions were uploaded successfully,
but then syncTransactionsFromServer() would call clearTransactions() which
deleted ALL transactions including the just-uploaded ones, causing them to
disappear from the UI even though they were on the server.
Now fixed by only clearing synced transactions, keeping pending ones safe.
* Change sync to use upsert logic instead of clear+insert
Critical fix: syncTransactionsFromServer now uses upsert for each transaction
instead of clearing synced transactions first. This prevents the race condition
where:
1. Pending transaction uploaded and marked as synced
2. syncTransactionsFromServer clears all synced transactions (including just uploaded)
3. Server response doesn't include the new transaction yet
4. Transaction disappears from local database
Changes:
- offline_storage_service.dart: syncTransactionsFromServer now calls
upsertTransactionFromServer for each transaction
- Added logic to preserve existing accountId if server response has empty accountId
- Replaced debugPrint with LogService for better tracking
- Added detailed logging to track upsert operations
This ensures uploaded transactions are never deleted, only updated with
server data when available.
* Fix flutter analyze warnings
- Remove unused import 'package:flutter/foundation.dart' from offline_storage_service.dart
- Fix use_build_context_synchronously warnings in transactions_list_screen.dart
by capturing providers before async gap (showDialog)
* Fix offline transaction sync not uploading pending transactions
Critical bug: performFullSync was calling syncPendingTransactions which
immediately returned 'Sync already in progress' error because performFullSync
had already set _isSyncing = true.
This caused pending transactions to never be uploaded to the server.
Solution:
- Extract core upload logic into _syncPendingTransactionsInternal() method
- This internal method doesn't check _isSyncing flag
- performFullSync calls the internal method directly
- Public syncPendingTransactions() still checks _isSyncing for standalone calls
Now pending transactions will properly upload during full sync.
* Initial plan
* Fix lifecycle, logging, error handling, and accessibility issues
- Fixed provider lifecycle in dashboard_screen.dart to prevent accessing disposed providers
- Fixed connectivity listener tracking in transactions_provider.dart with proper cleanup
- Fixed async callback race conditions by checking mounted state
- Replaced all debugPrint with LogService for consistent production logging
- Added comprehensive error handling for database initialization with FlutterError reporting
- Added missing index on server_id column for improved query performance
- Optimized LogService memory usage (reduced from 1000 to 500 max logs)
- Added debounced notifications in LogService (only when log viewer is active)
- Added semantic labels to sync status badge icons for accessibility
- Added semantic label to debug logs button for screen readers
- Fixed misleading success message in transaction form (conditional based on sync status)
- Fixed fire-and-forget async error handling to properly surface errors to UI
- Improved error messages in accounts_provider with specific error types
Co-authored-by: dwvwdv <89256478+dwvwdv@users.noreply.github.com>
* Fix semantic labels and connectivity check issues from code review
- Added semantic label to non-compact sync status badge for consistency
- Fixed connectivity check in transaction form to use actual ConnectivityService.isOnline instead of lastSyncTime
Co-authored-by: dwvwdv <89256478+dwvwdv@users.noreply.github.com>
* Fix mounted check to properly track provider disposal state
- Added _isDisposed flag to track actual provider disposal
- Updated mounted getter to use _isDisposed instead of connectivity service availability
- Ensures disposed providers don't continue processing connectivity changes
Co-authored-by: dwvwdv <89256478+dwvwdv@users.noreply.github.com>
* Address code review nitpicks for better code quality
- Improved exception type checking in accounts_provider using instanceof instead of string contains
- Improved ternary operator formatting for better readability
- Added clarifying comment for fire-and-forget async pattern in connectivity change handler
Co-authored-by: dwvwdv <89256478+dwvwdv@users.noreply.github.com>
* Fix exception handling, error messages, and database index
- Removed HandshakeException instanceof check, use string-based check for SSL/certificate errors
- Improved error message clarity: 'Transaction upload failed' instead of 'Background sync failed'
- Optimized server_id index to exclude NULL values for better performance and reduced size
Co-authored-by: dwvwdv <89256478+dwvwdv@users.noreply.github.com>
* Remove partial index syntax for better SQLite compatibility
- Changed server_id index to standard syntax without WHERE clause
- SQLite handles NULL values efficiently without explicit exclusion
- Ensures compatibility across all SQLite versions
Co-authored-by: dwvwdv <89256478+dwvwdv@users.noreply.github.com>
* Fix Android release build to work without keystore
- Made signing config optional when key.properties is missing
- Prevents null pointer exception during release build
- APK will be unsigned if no keystore is configured (debug signing will be used)
Co-authored-by: dwvwdv <89256478+dwvwdv@users.noreply.github.com>
* Improve keystore validation in build.gradle
Enhanced keystore validation by checking keyAlias, keyPassword, and storePassword in addition to storeFile existence.
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>
* Enhance Flutter build workflow with keystore checks
Added checks for keystore secrets and adjusted APK build process based on their presence.
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>
* Revert "Improve keystore validation in build.gradle"
* Update mobile/lib/services/connectivity_service.dart
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>
* Update mobile/lib/services/sync_service.dart
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>
* Fix mobile provider null handling and connectivity issues
- Fix null handling in ChangeNotifierProxyProvider callbacks for AccountsProvider and TransactionsProvider
- Change connectivity service fallback from online to offline to prevent unwanted network calls
- Replace raw error messages with user-friendly messages in TransactionsProvider
- Add VPN and Bluetooth to online connectivity check
- Fix database close() method to properly reset static cache
- Add loading state and error handling to connectivity banner sync button
- Update dependencies to latest stable versions (sqflite 2.4.2, path 1.9.1, connectivity_plus 7.0.0, uuid 4.5.2)
* Replace all debugPrint with LogService in mobile app
- Replace debugPrint with LogService in transactions_list_screen.dart
- Replace debugPrint with LogService in auth_provider.dart
- Replace debugPrint with LogService in auth_service.dart (login, signup, refreshToken methods)
- Add LogService imports to all affected files
- Use appropriate log levels: debug for informational logs, error for exceptions
- Keep debugPrint only in LogService internal implementation
All debugPrint calls now properly use the centralized LogService for consistent logging.
* Fix Flutter analyze errors and warnings
- Remove unused flutter/foundation.dart import in auth_service.dart
- Update connectivity_service.dart to handle List<ConnectivityResult> for connectivity_plus 7.0.0
- Change StreamSubscription type from ConnectivityResult to List<ConnectivityResult>
- Update _updateConnectionStatus to handle list of results using .any()
- Check if any connectivity result indicates online status (mobile, wifi, ethernet, vpn, bluetooth)
- Fix BuildContext usage across async gaps in connectivity_banner.dart
- Use immediate mounted check before context usage (if (!mounted) return)
- Properly guard all ScaffoldMessenger calls after async operations
All flutter analyze errors and info warnings resolved.
* Update mobile/lib/providers/accounts_provider.dart
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>
* Update mobile/lib/providers/transactions_provider.dart
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Lazy Bone <89256478+dwvwdv@users.noreply.github.com>
* Update mobile/lib/screens/dashboard_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>
* Update mobile/lib/screens/dashboard_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>
* Add error logging and implement pending delete tracking for offline transactions
This commit adds comprehensive error logging and implements a robust pending
delete system for offline transactions, ensuring data integrity and proper
synchronization.
Changes:
- Add LogService error logging in AccountsProvider and DashboardScreen
- Add SyncStatus.pendingDelete enum for tracking offline deletions
- Implement markTransactionForDeletion in OfflineStorageService
- Update offline delete logic to mark transactions instead of immediate deletion
- Add _syncPendingDeletesInternal to SyncService for processing pending deletes
- Update performFullSync to process pending deletes before uploads
- Update hasPendingTransactions and pendingCount to include pending deletes
The pending delete system ensures that:
- Offline deletions are tracked and synced when connectivity is restored
- Transactions with server IDs are deleted from both local and server
- Transactions without server IDs (never synced) are deleted locally only
- Failed delete attempts are marked and retried on next sync
* Fix Flutter analyzer issues for pending delete feature
- Add SyncStatus.pendingDelete case to sync_status_badge.dart switch
- Use const constructor for SnackBar in dashboard_screen.dart
- Display "Deleting" badge with delete icon for pendingDelete status
* Add undo functionality for pending transactions
This commit adds the ability to undo pending operations (both creates and
deletes) while offline, providing users with better control over their
pending transactions.
Features:
- Gray out pending delete transactions (50% opacity) similar to pending creates
- Add "Undo" button on all pending transactions (both pending and pendingDelete)
- Implement undoPendingTransaction in OfflineStorageService:
- Pending creates: delete the transaction from local storage
- Pending deletes: restore transaction to synced status
- Add undoPendingTransaction method to TransactionsProvider
- Show appropriate confirmation dialogs for undo operations
- Display success/error messages after undo attempts
User experience:
- Pending transactions are visually distinct (grayed out with status badge)
- Users can easily revert offline actions before they sync
- Clear feedback on undo success or failure
---------
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>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>