* feat(mobile): add privacy mode to mask money values
Adds an app-wide "privacy mode" so users can hide monetary amounts from
over-the-shoulder view.
- PrivacyProvider (ChangeNotifier) backed by PreferencesService, so the
choice persists across launches and every money widget rebuilds on
toggle.
- MoneyMasker.mask() collapses an amount's numeric portion into a short
fixed run of bullets while keeping the currency symbol and sign
(e.g. CA$1,234.56 -> CA$••••). A fixed run avoids leaking the value's
magnitude and reads cleanly without stray separators. Currency- and
locale-agnostic — it operates on already-formatted strings.
- Masking applied at every money render site: net worth + per-currency
totals + breakdown sheet (NetWorthCard), account balances (AccountCard,
AccountDetailHeader, transaction form account selector), and transaction
amounts (transactions list, recent transactions, calendar).
- Two entry points: a "Hide amounts" switch in Settings -> Security, and a
quick eye toggle in the top bar (visible on every tab).
Tests: MoneyMasker unit tests (fixed-run mask, magnitude hidden, symbol/
sign kept, passthrough, idempotent) + a widget test asserting the net
worth masks/unmasks as the provider flips; account_card_test updated to
provide the new provider. flutter analyze: no new issues; full suite
(123) green.
* fix(mobile): address privacy-mode review feedback
- Startup masking (Codex P1): read the privacy preference in main() before
runApp and seed PrivacyProvider with it, so the first frame already has
the correct value — money is never briefly rendered unmasked for a user
who enabled "Hide amounts". Provider stays fail-closed otherwise: starts
masked, SureApp's no-arg default is masked, and a failed read keeps it
masked. A late-completing initial load no longer clobbers an explicit
user toggle.
- setHidden() reverts the in-memory state (and logs) if persistence fails,
keeping the UI consistent with what's actually stored.
- Mask the cash-balance detail chip in AccountDetailHeader (was leaking
the cash position in privacy mode).
- Privacy top-bar toggle gets a "Toggle privacy" tooltip + icon semantic
label for accessibility (kept as an InkWell to match the adjacent
settings control).
- Tests: assert fail-closed initial state; assert the exact masked count;
test the persistence round-trip (set -> reload); add
PreferencesService.resetForTest() and reset between tests so the cached
singleton can't leak state.
125 tests pass; flutter analyze: no new issues.
* refactor(mobile): thread hideAmounts through calendar tiles
Per review: the calendar tile builders read PrivacyProvider via
context.read, relying implicitly on the parent build()'s context.watch to
rebuild them — fragile if a tile is later extracted or wrapped in a
RepaintBoundary. Pass hideAmounts down explicitly instead, matching the
recent_transactions_screen pattern:
- build() (context.watch) -> _buildCalendar -> _buildDayCell
- _showTransactionsDialog reads once when the modal opens ->
_buildTransactionTile
No more context.read inside tile methods. 125 tests pass; analyze clean.
* fix(mobile): watch PrivacyProvider inside calendar dialog builder
Moving the hideAmounts read inside the showDialog builder and switching
from context.read to context.watch ensures the dialog re-masks transaction
amounts if the user toggles privacy mode while the dialog is open.
* feat(mobile): add SureFontWeights design tokens
Introduce named font-weight tokens so widgets reference a DS tier instead
of a raw FontWeight, mirroring the web design system's Tailwind weight
utilities (font-medium / font-semibold).
- design/tokens/sure.tokens.json: add font.weight.medium (500) and
.semibold (600) as the canonical source.
- generate_sure_tokens.mjs: emit SureTokens.weightMedium / weightSemibold
(with a font-weight resolver + validation), regenerate sure_tokens.dart.
- sure_tokens_test.dart: assert the generated weights match canonical.
- Migrate the dashboard-area money/hierarchy sites onto the tokens
(net_worth_card, account_card, dashboard, recent_transactions,
transactions_list): FontWeight.w500 -> SureTokens.weightMedium,
FontWeight.w600 -> SureTokens.weightSemibold.
Weight-only; other screens adopt the tokens in their own polish PRs.
166 tests pass; flutter analyze: no new issues; token generator --check
is clean.
* build(tokens): regenerate web _generated.css for new font weights
Adding font.weight.medium/semibold to sure.tokens.json also flows through
the web token generator (bin/tokens.mjs), which emits --font-weight-medium
and --font-weight-semibold. Commit the regenerated _generated.css so the
tokens:check gate stays green (node bin/tokens.mjs + git diff --quiet).
* test(system): de-flake account edit via account-menu
AccountsTest#assert_account_created opened the account menu and clicked
Edit immediately after visiting the account page. That page issues a
Turbo morph refresh shortly after load (turbo_refreshes_with :morph
reacting to a family-stream broadcast), which can detach the menu node
mid-click — "Selenium::WebDriver::Error: Node with given id does not
belong to the document" — or wipe the just-opened edit form. Capybara
does not auto-retry that inspector error, so the test flaked
intermittently in CI.
Extract open_account_edit_dialog, which retries the menu interaction
until the edit form is present, rescuing the transient detach/stale
errors. Mirrors the existing PropertyTest#open_account_edit_dialog
precedent for the same morph race, but also tolerates the click itself
raising while the refresh is in flight.
* feat(mobile): add Flutter i18n foundation (gen_l10n Phase 1)
- Wire flutter_localizations + gen_l10n (pubspec.yaml + l10n.yaml)
- Bump intl ^0.18.1 → ^0.20.2 for Flutter 3.32 pin
- Add lib/l10n/app_en.arb with 9 seed keys (appTitle, 3 common*, 4 chatSuggestion*)
- Generate AppLocalizations via flutter gen-l10n
- Replace MaterialApp title: with onGenerateTitle + localizationsDelegates + supportedLocales
- Migrate suggested_questions.dart from const list to BuildContext function
- Update chat_conversation_screen.dart call site (one-line change at pre-designed seam)
* feat(mobile): migrate all hardcoded UI strings to gen_l10n ARB (Phase 2)
Extends the i18n foundation from Phase 1 to cover every remaining
hardcoded user-facing string across the mobile app.
Files changed (21 source files + ARB/generated):
- lib/l10n/app_en.arb: 226 keys (+~80 new keys covering all screens)
- lib/screens/dashboard_screen.dart
- lib/screens/calendar_screen.dart
- lib/screens/transactions_list_screen.dart
- lib/screens/backend_config_screen.dart
- lib/screens/recent_transactions_screen.dart
- lib/screens/biometric_lock_screen.dart
- lib/screens/chat_conversation_screen.dart
- lib/screens/chat_list_screen.dart
- lib/screens/log_viewer_screen.dart
- lib/screens/login_screen.dart
- lib/screens/main_navigation_screen.dart
- lib/screens/more_screen.dart
- lib/screens/settings_screen.dart
- lib/screens/sso_onboarding_screen.dart
- lib/screens/transaction_edit_screen.dart
- lib/screens/transaction_form_screen.dart
- lib/widgets/account_detail_header.dart
- lib/widgets/category_filter.dart
- lib/widgets/connectivity_banner.dart
- lib/widgets/currency_filter.dart
- lib/widgets/custom_proxy_headers_editor.dart
Notable patterns:
- ICU plural for connectivity pending-sync count
- Parameterised keys for greeting (firstName), update dialog
(version), cash chip (amount), delete confirmation (name),
show-N menu (count), proxy-headers count, multi-delete count
- AppLocalizations captured before async gaps; dialog builders
re-obtain from their own BuildContext
- const removed only from Text/InputDecoration/SnackBar/Row
widgets that directly reference localisation getters; sibling
const widgets preserved
116/116 unit tests pass; flutter analyze --no-fatal-infos: 0 errors.
* feat(mobile): migrate the remaining secondary UI strings to gen_l10n ARB
Completes the i18n migration by covering the lower-traffic strings the
earlier passes left behind — secondary dialogs, snackbars, settings
rows, validators, helper texts, and tooltips across 11 files.
- settings_screen.dart: reset/delete-account dialogs, clear-data and
proxy-header snackbars, list-tile titles/subtitles, theme-segment
tooltips, biometric prompt reason, debug-logs semantics label
- transaction_form_screen.dart: amount validators, session/success/
failure/generic-error snackbars, More/Less toggle, date/name/category
fields + helper texts, create button
- transaction_edit_screen.dart: session/update snackbars, validators,
category/merchant/tag fallbacks, synced-only notice
- login_screen.dart: API-key dialog, demo/sign-up TextSpan, MFA info +
validator, divider, server-URL heading, API-key + backend tooltip
- chat_list_screen.dart: multi-delete plural, result snackbars, error
heading, relative-time fallback
- chat_conversation_screen.dart: edit-title dialog, refresh tooltip,
load-error heading
- connectivity_banner.dart: sign-in / sync-success / sync-failure /
auth-failure snackbars (also resolves the const removals cleanly)
- main_navigation_screen.dart: enable-AI dialog + failure snackbar
- log_viewer_screen.dart: clear-logs confirmation dialog
- biometric_lock_screen.dart, transactions_list_screen.dart: snackbar
and edit tooltip
Adds 98 ARB keys (now 324 total), reusing common* and existing
screen keys where text matched. Interpolations use String/int
placeholders; multi-delete uses an ICU plural. AppLocalizations is
captured before async gaps and re-obtained inside dialog builders;
const removed only where a localisation getter is introduced, with
const restored on still-const siblings.
116/116 unit tests pass; flutter analyze --no-fatal-infos: 0 errors
(6 pre-existing infos unchanged). No remaining hardcoded user-facing
strings (verified by grep; only a doc-comment example remains).
* fix(mobile): address i18n review feedback and prune dead ARB keys
Resolves the still-valid CodeRabbit/Codex review findings on the i18n
migration; the majority were already handled by earlier commits.
- pubspec: switch intl to `any` so flutter_localizations selects the
SDK-pinned version (0.19.x on <3.32, 0.20.x on 3.32+). Pinning
^0.20.2 conflicted with the declared `flutter: '>=3.27.0'` floor and
would break `flutter pub get` on pre-3.32 SDKs.
- sso_onboarding: localize "Signed in as {email}" / "Google account
verified" / the link- and create-form notes; fix the first/last-name
validators that wrongly returned the "Email is required" message.
- backend_config: localize _testConnection / _saveAndContinue /
_validateUrl messages and the headers helper text (validator now
takes AppLocalizations; async methods capture it before awaits).
- recent_transactions: localize "Unknown Account"; locale-aware
timestamp.
- account_detail_header: localize the unavailable-details message via a
render-time flag (avoids touching inherited widgets in the
initState-driven load path); treat empty/whitespace securityName as
missing for the holding-name fallback.
- dashboard: wrap syncing/refreshing snackbar text in Expanded to avoid
overflow with long translations; use the distinct dashboardSyncError
key on the exception path.
- chat_list: localize relative-time labels (plural keys) and use a
locale-aware fallback date; calendar: locale-aware displayed dates and
weekday letters (map-key formats left fixed; Sunday-anchored grid
preserved).
- connectivity_banner: capture ScaffoldMessenger before the async gap
(removes use_build_context_synchronously); log_viewer: const Duration.
- Prune 55 unused ARB keys (never-wired scaffolding + superseded
duplicates). ARB now 298 keys, 0 unused.
116/116 tests pass; flutter analyze --no-fatal-infos: 0 errors, only 3
pre-existing infos in the untouched intro_screen_web.dart.
* fix(mobile): localize the log viewer empty-state string
The 'No logs yet' empty state was the last hardcoded user-facing string
(the logViewerEmpty key had been pruned before this string was migrated).
Re-add the key and route the empty state through AppLocalizations.
Closes the final CodeRabbit i18n-coverage thread. 0 analyze errors,
116 tests pass, ARB at 299 keys with 0 unused.
* fix(mobile): address i18n review feedback
- sso_onboarding: the "Accept Invitation" tab and submit buttons showed
the Terms-of-Service string (ssoOnboardingAcceptTerms) when a household
invitation was pending. Add ssoOnboardingAcceptInvitation and use it in
both spots; drop the now-unused ToS key (no ToS checkbox exists).
- transaction_form: restore the required-field indicator on the amount
label ("Amount *") that the migration dropped.
- settings: localize the "a newer version" fallback used in the update
dialog when the store version is unknown.
- ARB plurals: use the CLDR `one{}` category instead of the exact-match
`=1{}` so future non-English locales pluralize correctly. English output
is unchanged.
ARB at 300 keys, 0 unused. 116 tests pass; flutter analyze: no new issues.
* fix(mobile): reconcile i18n with design-system primitives after merge
The merge of main into this branch pulled in the design-system primitive
migrations (SureCard/SureChip/SureSegmentedControl/SureTextField/
SureListGroup), which collided with the i18n string migration and left
several files broken (compile errors / un-localized text).
Re-apply the i18n migration on top of the design-system versions:
- more_screen: SureListGroup/SureListRow rows now use l.more* keys
(was a mangled mix of _buildMenuItem + SureListGroup).
- transaction_form_screen: SureSegmentedControl segments + amount/date/
name/category labels, helpers, validators, and snackbars localized;
add transactionFormAmountHelper ("Required") for the amount helper text.
- custom_proxy_headers_editor: SureTextField label/hint params localized.
- currency_filter: the "All" chip is a SureChip again (the merge had left
it as a Material FilterChip) and uses l.commonAll.
- currency_filter_test / custom_proxy_headers_editor_test: add the
localization delegates the widgets now require.
ARB at 301 keys, 0 unused. 165 tests pass; flutter analyze: 0 errors.
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* feat(mobile): add SureSegmentedControl primitive and migrate the transaction type selector
Adds SureSegmentedControl<T>, the segmented-control primitive completing the
mobile form-control set after SureTextField and SureChip: an inset track of
equal-width segments where the selected segment is a raised surface (container
fill + the subtle DS shadow) and unselected segments are flat textSecondary
labels, resolving from the active SureColors palette. Each segment is
keyboard/switch focusable (FocusableActionDetector, mirroring SureButton) with
button + selected semantics; the selected fill is brightness-aware because the
palette has no single raised-surface token that reads correctly in both modes.
Migrates the transaction-form Type selector off Material SegmentedButton as
proof (selection behavior preserved).
Part of #2235.
* fix(mobile): assert unique SureSegmentedControl segment values
Selection is value-based (segment.value == selected), so duplicate
segment values would render multiple segments as selected at once. Guard
against that with a debug assert in build() (a const constructor can't
host the non-constant uniqueness check). Addresses CodeRabbit review.
* feat(mobile): add SureChip primitive and migrate the currency filter
Adds SureChip, the filter-chip primitive following SureTextField in the mobile
design-system sequence: a tokenized selectable pill resolving from the active
SureColors palette — a bordered transparent pill when unselected and a filled
neutral (buttonPrimary + inverse label) pill when selected — replacing the
Material FilterChip primaryContainer tint + blue primary border. Includes
selected-state semantics (announced as a button only when interactive), a 44px
min tap target, and single-line ellipsised labels.
Migrates the currency filter off Material FilterChip as proof (selection logic
preserved). The segmented control will follow as a separate PR.
Part of #2235.
* fix(mobile): normalize stale codes in currency filter All detection
Length-based All detection could misfire when selectedCurrencies holds
stale codes no longer in availableCurrencies. Intersect with the
available set before comparing, and require containsAll before emitting
the empty (All) selection. Addresses CodeRabbit review.
Adds SureTextField, the form-control primitive following SureButton in the
mobile design-system sequence: a TextFormField wrapper that builds a complete,
brightness-aware InputDecoration from the active SureColors palette (filled
bg-container, borderSecondary hairline -> borderPrimary on focus, destructive
error border, textSubdued placeholder, radiusLg corners) with a DS-style label
rendered above the field and associated to it for screen readers (Material
labelText parity via ExcludeSemantics + Semantics(label:)).
Migrates the custom proxy-headers editor off raw TextFormField as proof.
Filter chips and a segmented control will follow as separate PRs.
Part of #2235.
* feat(mobile): add SureListGroup/SureListRow primitives and migrate the More menu
Adds the grouped inset-list primitives that follow SureCard in the mobile
design-system sequence: SureListGroup (tokenized container — bg-container +
borderSecondary hairline + radiusLg corners + shadowXs, with clipped inset
dividers and an optional section header) and SureListRow (leading / title /
subtitle / trailing slots, a DS chevron disclosure affordance, and a
destructive variant). Both resolve from the active SureColors palette, so
they're brightness-aware and stay in lockstep with sure.tokens.json.
Migrates the More menu off raw Material ListTile/Divider (which fell back to
colorScheme defaults) as proof. Adds the lucide chevron-right asset and
SureIcons.chevronRight for the row disclosure indicator.
Part of #2235.
* fix(mobile): restore button semantics on SureListRow + harden grouped list
Adversarial + CodeRabbit review follow-ups on the SureListGroup primitives:
- a11y: a tappable SureListRow lost the button/enabled semantics the migrated
Material ListTile exposed (a bare InkWell emits only a tap action). Wrap the
tappable content in MergeSemantics + Semantics(button) so screen readers
announce each row as a single button again. Adds a semantics regression test.
- SureListGroup collapses to SizedBox.shrink() for an empty children list
instead of painting a 2px bordered/shadowed box.
- More menu: restore the leading icon-badge tint to textPrimary (the migration
had silently dropped it to the lower-contrast textSecondary).
- Tests: extend divider / title / subtitle / destructive assertions to dark mode
(CodeRabbit nitpick), plus the new semantics + empty-group cases.
* feat(mobile): add SureCard primitive and migrate account cards
SureCard mirrors the web card chrome — `bg-container` + a hairline border +
rounded corners + the subtle DS shadow (`shadowXs`, from the scale shipped in
#2349) — resolved from the active SureColors palette so it stays in lockstep with
`sure.tokens.json` in light and dark. An optional `onTap` gives a flat ink
response clipped to the card radius.
Migrates AccountCard off the Material `Card` to `SureCard`.
Refs #2235.
* fix(coinstats): deterministic wallet batch order in bulk fetches
The bulk balance/transaction fetches built the "blockchain:address" param in
linked-account query order, which isn't stable across runs — so the batched
param (and its mocked expectations) varied seed to seed, intermittently failing
CoinstatsItem::ImporterTest and red-flagging unrelated PRs. Sort the wallets
before joining so the batch order is deterministic, and align the test
expectations to the sorted order.
* fix(mobile): tokenize swipe-reveal radius + cover SureCard dark theme
Address PR review:
- The account-card swipe-reveal background still used a hardcoded radius (12)
that mismatched SureCard's radiusLg (10) once the card was migrated — tokenize
it so the reveal corners line up during the swipe.
- Parameterize the SureCard chrome test over light + dark, since the card is
brightness-aware (catches palette regressions in either mode).
* test(mobile): assert SureCard onTap ink is clipped to the card radius
Address review nit: the onTap test claimed the ink is clipped to the card but
only checked the InkWell exists. Now it asserts the InkWell's borderRadius equals
SureTokens.radiusLg (tester.widget already enforces a single InkWell).
* feat(mobile): in-app update prompt via upgrader package
- Add upgrader ^13.5.0 dependency for automatic update checks
- Show UpgradeAlert after authentication, with a 5-minute delay so
users can settle in before being prompted
- Dialog shows at most once every 7 days per store version
- Only "Later" and "Update now" buttons — ignore button removed via showIgnore: false
- Custom _SureUpgraderMessages with mustache body template (v13 API)
- Add "Check for updates" tile in Settings for manual checks:
calls updateVersionInfo() and shows an AlertDialog with store URL
if an update is available, or a snackbar if already up to date
- Timer resets on logout so the delay restarts on next sign-in
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(mobile): address upgrader review comments
- Mount UpgradeAlert unconditionally so MainNavigationScreen is never
remounted when the timer fires, preserving navigation state
- Dispose _manualUpgrader in SettingsScreen to remove its lifecycle
observer when the screen is torn down
- Show update dialog even when store version metadata is incomplete
(falls back to 'a newer version'); disable Update now button rather
than silently no-oping when store URL is unavailable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(mobile): show snackbar when store URL cannot be opened
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(mobile): dispose manual upgrader if unmounted after initialize
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend mobile/tool/generate_sure_tokens.mjs to emit, from the canonical design/tokens/sure.tokens.json: a mode-aware box-shadow scale (shadowXs..shadowXl, parsed from shadow.xs..xl into List<BoxShadow>), plus the focus-ring (color.focus-ring) and bg-inverse (utility.bg-inverse) colors on SureTokenPalette. Regenerates mobile/lib/theme/sure_tokens.dart (import dart:ui -> package:flutter/painting.dart for BoxShadow/Offset); keeps the --check parity gate; adds token tests for the new values incl. the negative xl spread.
Radii are already complete (the source only defines md/lg); a type-scale is not in the token source, so both are intentionally untouched. The shadow-border composites are deferred (derivable in-widget as a shadow + a 1px border from existing border tokens). This unblocks SureCard/elevated surfaces.
Part of #2235.
On screens not yet redesigned with SureColors (the Chats list, etc.), Material's
primary/secondary *container* roles were unset, so they fell back to defaults
that read as blue on the FAB, unread-badge, and avatar surfaces.
- Pin `primaryContainer`/`secondaryContainer` to a neutral Sure surface
(`surfaceInset` + `textPrimary`), and add a `FloatingActionButtonThemeData` so
FABs use Sure's neutral primary action color (`buttonPrimary`). `primary` /
`secondary` stay `link` / `info`, so existing link/accent callers are unchanged.
- Add a `SureLogo` widget that renders `logomark.svg` with the wordmark's
`currentColor` strokes tinted to the theme's secondary text color, and route
every logomark consumer (nav bar, login) through it — so the mark stays legible
in dark mode (the hardcoded grey was too dim) and no caller renders the strokes
as the flutter_svg default (black). The green brand mark keeps its fill.
Refs #2235.
Introduce a SureIcon design-system primitive that renders bundled Lucide SVGs via flutter_svg (already a dependency — no new dep), mirroring the web `icon` helper / DS::FilledIcon: tokenized size (SureIconSize xs–2xl = 12–32), color inherited from the ambient IconTheme by default, and accessibility that distinguishes decorative icons (excluded from the semantics tree) from meaningful ones (semanticLabel). A SureIcons registry keeps call sites typo-safe and limited to bundled assets.
Migrate the dashboard surface off Material Icons.* onto SureIcon — 24 call sites across net_worth_card, account_card, and dashboard_screen (account-type glyphs, asset/liability trend icons, sync/empty/error states, refresh, collapsible section headers, expand chevrons). Bundle the 17 Lucide SVGs the surface needs under assets/icons/lucide/ with the ISC license. The swipe Undo control and the rest of the app's icons follow in later slices.
Part of #2235.
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Migrate the dashboard text tier off the off-convention bold (700) and semibold (600) to the design-system Medium (500): 14 weight-only sites across 5 files — net-worth headline + breakdown modal + asset/liability filter balances, account-card name + balance, collapsible-type section header + count badge, the stale "Outdated" badge, and the recent + list transaction names + amounts.
The web DS uses font-medium/500 for emphasis (essentially no bold or semibold), so this lands one uniform Medium tier and removes the inverted hierarchy where a money amount rendered lighter than its own label. Sizes, colors, and structure are untouched (zero layout shift); money keeps SureMoney.tabular and semantic color. The swipe "Undo" button affordance (w600 + hardcoded color) is intentionally left for a future button-primitive slice.
Part of #2235.
Introduce a custom (non-Material) SureButton mirroring the web DS::Button: variants (primary, secondary, destructive, outline, ghost), sizes (sm/md/lg ≈ 28/36/48), optional leading widget, fullWidth, loading, and disabled. Colors resolve from the SureColors palette and the shape from radius tokens (radiusMd/radiusLg); flat custom press feedback (no Material ripple). Dimensions mirror the web DS::Buttonish::SIZES (the web composes these from Tailwind's standard scale rather than brand tokens, so they're not in sure.tokens.json).
Migrate the three main-view login buttons onto it — Sign In (primary, lg, loading), Sign in with Google (outline + SVG leading), and API-Key Login (ghost). The two buttons inside the API-key modal dialog are left for a follow-up.
Part of #2235.
* feat(mobile): standardize money typography and semantic amount color
Add a brightness-aware SureColors theme extension and a MoneyText/SureMoney
primitive (semantic success/destructive/subdued tokens + tabular figures for
column-aligned digits), then migrate the transaction lists and balance cards
off raw Colors.green/red/grey.
Step 2 of the mobile design-system sequence (#2235), after #2237's theme
foundation. Primitive-first: screens consume shared tokens/typography.
* fix: review feedback — brightness-aware token fallback, de-flake Setting tests, Pipelock localhost FP
- SureColors.of falls back to the palette matching the active brightness (not
always light) when the extension is missing, so dark surfaces stay correct.
- Clear the rails-settings-cached cache before each test; its in-memory cache
survives the per-test transaction rollback, leaking Setting.* across tests and
flaking Settings::HostingsControllerTest (stale empty string vs nil).
Full unit suite: 4952 runs, 0 failures.
- Suppress the localhost test-DB DATABASE_URL false positive with line-level
`# pipelock:ignore` in ci.yml + llm-evals.yml instead of excluding whole files,
so those workflows stay scanned for real secrets.
* feat(mobile): align theme foundation with Sure tokens
Generate typed Flutter token constants from the canonical Sure token JSON and route the app through shared light/dark ThemeData construction. This keeps the first mobile design-system step scoped to foundation wiring and regression tests without restyling screens directly.
* fix(mobile): define Sure error container colors
Populate the manual Flutter ColorScheme error container pair from Sure tokens so validation and connection banners keep visible icon/text contrast. Add theme assertions for both light and dark modes.
* fix(mobile): preserve full-width action buttons
* feat(mobile): add transaction metadata editing
* fix(mobile): preserve explicit metadata clears
* fix(mobile): derive persisted tag metadata state
* fix(mobile): avoid logging transaction details
* fix(mobile): harden transaction edit sync
Pass the edited transaction context through provider updates, refresh or fall back after empty update responses, surface field-level API errors, and avoid forced metadata refetches on every edit screen open.
* fix(mobile): keep transaction edit selects ci-compatible
Use the DropdownButtonFormField API supported by the Flutter version pinned in upstream mobile CI.
The message input container bottom padding now adds MediaQuery.paddingOf(context).bottom
so the input row clears the Android system navigation bar in edge-to-edge mode
(Android 15+, 3-button nav bar). Value is 0 in non-edge-to-edge mode so existing
behaviour is unchanged.
* feat(mobile): add mass delete for chats
Long-press any chat to enter selection mode, tap items to select/deselect,
use Select All to toggle all, then delete with a single confirmation.
Swipe-to-delete continues to work outside selection mode.
* fix(mobile): address PR review comments on mass delete
- Wrap each deleteChat call in its own try-catch so a single network
failure doesn't abort the entire Future.wait operation
- Add null-safe casting for deletedCount and failedIds in provider
- Fix misleading error snackbar copy ("Some chats could not be deleted"
implied partial failure; provider only returns false on total failure)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(mobile): add suggested questions to empty chat screen
- New constants file (lib/constants/suggested_questions.dart) for the 4
suggested question chips, kept separate from screen logic with a clear
l10n upgrade path noted in comments
- Empty chat screen now shows a personalised greeting and tappable
OutlinedButton chips; tapping one pre-fills and sends the message
- Optimistic message insertion in ChatProvider.sendMessage so the user
message and typing indicator appear instantly on tap, with rollback on
failure
- Full AI response revealed only once polling detects stable content
(2 consecutive polls with no growth), preventing partial responses
from flashing on screen
- fetchChat stops any in-progress polling before fetching so a manual
refresh always shows the authoritative server response
- Fixed updateChatTitle silently wiping messages when the title-update
API response omits the messages array
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(mobile): address PR review comments
- Extract _rollbackOptimisticMessage helper to eliminate duplicated
rollback logic in sendMessage failure and catch branches
- Replace raw 'Error: ${e.toString()}' user-facing strings with a
generic message; retain technical details via debugPrint in each
catch block
- Replace inline ternary in updateChatTitle with explicit if/else for
readability while preserving message-preservation behaviour
- Fix non-reactive AuthProvider read inside Consumer<ChatProvider>
builder (listen: false → listen: true) so greeting updates when
user's firstName changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add mobile custom proxy headers
* Clear login placeholders on focus
Email/password fields ship with example values pre-filled. Tapping the
field now clears the placeholder so users don't have to delete it
manually. Skips clearing if the user has already edited the value.
* Push Configuration as a route from Sign in
Opening Configuration from the Sign in screen now uses Navigator.push
instead of toggling a state flag, so Android back returns to Sign in
instead of quitting the app. Saving the URL auto-pops the route.
* Address PR review on custom proxy headers
- Test Connection no longer leaves global ApiConfig headers mutated;
unsaved edits are restored in a finally block after the probe.
- _loadSavedUrl / _loadCustomHeaders wrap storage reads in try/catch and
always finish initialization with sensible defaults.
- Sanitization is now a single CustomProxyHeader.sanitize() reused by
ApiConfig.setCustomProxyHeaders and CustomProxyHeadersService.
- Brief comment on redactedValue explaining the length-obscuring design.
* Harden custom proxy header validation and load path
- validateValue now rejects ASCII control characters (CR/LF/tab/etc.)
to prevent header-injection via crafted values.
- loadHeaders moves the secure-storage read inside the try block so
platform exceptions are caught the same way JSON parse errors are.
* Improve chat LLM error messages
* Fix chat visibility regression in tests
* Harden chat error handling for review feedback
* Fix rubocop private method indentation
* Fix nil presentable_error_message, i18n strings, bare rescue
- Guard `presentable_error_message` with `return nil if error.blank?` so
chats with no error return nil instead of the fallback string; this
prevents the API serialisers from emitting a spurious error message and
stops the mobile polling guard from firing on every successful chat
- Move all hardcoded user-facing error strings into
config/locales/models/chat/en.yml and reference them via I18n.t()
- Replace bare `rescue` in `error_message_for` with `rescue StandardError`
to avoid swallowing system-level exceptions
- Update tests to reference I18n keys instead of raw strings, and add
tests for the nil-error case and the unrecognized-error fallback
https://claude.ai/code/session_01YFMjEds5WVyKPL42xBqMCX
---------
Co-authored-by: SureBot <sure-bot@we-promise.com>
Co-authored-by: Claude <noreply@anthropic.com>
* 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>