From 62c7f53de0bcbee57615acba60104aa52154095d Mon Sep 17 00:00:00 2001 From: Prince Date: Mon, 22 Jun 2026 10:06:51 +0200 Subject: [PATCH] minor bug fixes --- WORKSPACE_ID_MIGRATION_AUDIT.md | 160 ++++++++++++++++++ lib/screens/home/home_screen.dart | 9 + .../onboarding/phone/phone_screen.dart | 21 --- lib/screens/pay/pay_controller.dart | 11 +- nginx.conf | 7 + 5 files changed, 185 insertions(+), 23 deletions(-) create mode 100644 WORKSPACE_ID_MIGRATION_AUDIT.md diff --git a/WORKSPACE_ID_MIGRATION_AUDIT.md b/WORKSPACE_ID_MIGRATION_AUDIT.md new file mode 100644 index 0000000..cbd5203 --- /dev/null +++ b/WORKSPACE_ID_MIGRATION_AUDIT.md @@ -0,0 +1,160 @@ +# Audit: Migrating from `userId` to `workspaceId` + +This document audits every place the project uses `userId` (and `user_id`) so the +migration to a `workspaceId`-scoped model can be done completely. There are +currently **no references to `workspaceId`** anywhere in the codebase — this is a +greenfield rename/replacement. + +## How scoping currently works (the mental model) + +1. **Source of truth set at login** — `login_controller.dart:70` stores + `prefs.setString('userId', response['uuid'])`. The backend's `uuid` becomes + the locally cached `userId`. +2. **Anonymous fallback** — `home_screen.dart:127,162` generate a random + `Uuid().v4()` and store it as `userId` when none exists (anonymous/guest flow). +3. **Consumption** — Almost everywhere else reads `prefs.getString("userId")` + and passes it to the backend as: + - a **query param** (`?userId=...` or `_queryParams['userId']`), + - a **request body field** (`BatchActionRequest.userId`, `FormData.userId`), + - a **model field** deserialized from `json['userId']`. + +> Migration decision needed (see "Open questions"): is `workspaceId` a NEW value +> the backend returns at login, or simply a rename of the existing `uuid`? And +> will the API now accept/scope by a header (e.g. `X-Workspace-Id`) instead of a +> query param? The plan below assumes a rename + the option to centralize scoping. + +--- + +## Inventory of changes required + +### A. Persistence / key name (the keystone) + +The SharedPreferences key string `"userId"` is the single most important thing to +change consistently. If the key string is changed in one place but not another, +reads will silently return `null`. + +| File | Line | Current | Action | +|---|---|---|---| +| `lib/screens/onboarding/login/login_controller.dart` | 70 | `prefs.setString('userId', response['uuid'])` | Write `workspaceId`. Confirm backend field — may become `response['workspaceId']`. | +| `lib/screens/home/home_screen.dart` | 127, 162 | `prefs.setString("userId", Uuid().v4())` | Rename key to `workspaceId` (anonymous fallback). | +| `lib/screens/home/home_screen.dart` | 126, 130, 151, 152, 161, 165 | `prefs.getString("userId")` | Rename key reads to `workspaceId`. | +| `lib/screens/onboarding/phone/phone_controller.dart` | 48 | `"tempUid": prefs.get("userId")` | Rename key read; confirm whether `tempUid` body field name should change too. | + +> Recommendation: introduce a single constant, e.g. `const kWorkspaceIdKey = 'workspaceId';` +> and a small helper (`Prefs.workspaceId`) so the key string is never duplicated again. +> A migration shim should also copy any existing `userId` value to `workspaceId` on +> first launch so logged-in users aren't logged out. + +### B. Query-param / URL usages (sent to backend) + +| File | Line | Current | +|---|---|---| +| `lib/screens/recipient/recipients_screen.dart` | 69, 271 | `_queryParams['userId'] = prefs.getString("userId")!` | +| `lib/screens/history/history_controller.dart` | 119, 146 | `'userId': userId` (query map) | +| `lib/screens/groups/controllers/group_controller.dart` | 72, 197, 233, 269 | `'userId': userId` and `?userId=$userId` in URLs | +| `lib/screens/groups/controllers/batch_controller.dart` | 369, 528, 584 | `?userId=$userId` in URLs | +| `lib/screens/home/home_controller.dart` | 158 | `...&userId=$userId` in URL | + +Action: rename the param key to `workspaceId` (or move scoping into a header — see C). + +### C. HTTP layer (`lib/http/http.dart`) + +Currently the HTTP wrapper only adds `Authorization: Bearer ` headers +(`getHeaders()` around lines 28–36). It does **not** know about `userId`. + +Action / opportunity: if the backend now scopes by a workspace header, this is the +single best place to inject `X-Workspace-Id` once, eliminating the dozens of +per-call query params. Otherwise, leave the per-call params and just rename them. + +### D. Request-body model fields & method params (Dart-side, hand-written) + +These are the hand-written declarations and call sites to rename +(`userId` -> `workspaceId`): + +- `lib/screens/groups/models/group_batch_model.dart` — fields at lines 9, 90, 117, 134 and constructors 26, 100, 121, 141 (includes `BatchActionRequest.userId`). +- `lib/screens/groups/models/recipient_group_model.dart` — fields 11, 33, 58 and constructors 18, 42, 64. +- `lib/screens/groups/models/batch_item_update_model.dart` — field 38, constructor 40. +- `lib/screens/transactions/transaction_model.dart` — field 53, constructor 101. +- `lib/screens/transaction_controller.dart` — defaults/params 41, 71, 152 (the freezed `FormData.userId`). +- `lib/screens/groups/controllers/group_controller.dart` — method params 56, 118, 123, 193, 229, 244, 257 and internal calls 120, 128, 251. +- `lib/screens/groups/controllers/batch_controller.dart` — method params 363, 466, 494, 522, 545, 581, 598 and `BatchActionRequest(userId: ...)` at 473, 501, 552; recursive call 606. +- `lib/screens/history/history_controller.dart` — method params 110, 141. +- `lib/screens/home/home_controller.dart` — method param 151. + +### E. Call sites in screens (read prefs -> pass through) + +- `lib/screens/pay/pay_controller.dart` — 101 (`userId: prefs.getString("userId") ?? ''` into FormData). +- `lib/screens/groups/screens/group_create_screen.dart` — 84, 91. +- `lib/screens/groups/screens/group_detail_screen.dart` — 555, 678, 703 (`widget.group.userId`). +- `lib/screens/groups/screens/batch_detail_screen.dart` — local `_userId` at 36, 88 and uses at 90, 139, 155, 434, 487, 675, 688, 1438. +- `lib/screens/groups/screens/create_group_from_file_screen.dart` — 68, 72. +- `lib/screens/groups/screens/groups_screen.dart` — 33, 39, 40, 44, 46, 52, 75. +- `lib/screens/groups/screens/batch_create_screen.dart` — 92, 96, 118, 133, 157. +- `lib/screens/history/history_screen.dart` — 67, 74, 230, 330, 354, 499, 512. + +### F. Generated files (DO NOT hand-edit — regenerate) + +These contain `userId` derived from `@JsonKey`/freezed annotations in the +hand-written sources above. After renaming the source fields (and the JSON keys), +regenerate with build_runner: + +- `lib/screens/transaction_controller.g.dart` +- `lib/screens/transaction_controller.freezed.dart` +- `lib/screens/groups/models/batch_item_update_model.g.dart` +- `lib/screens/groups/models/recipient_group_model.g.dart` +- `lib/screens/groups/models/group_batch_model.g.dart` +- `lib/screens/transactions/transaction_model.g.dart` + +Regenerate command: +``` +dart run build_runner build --delete-conflicting-outputs +``` + +--- + +## JSON wire-key decision (critical) + +The generated serializers currently read/write the JSON key `'userId'` +(e.g. `json['userId']`, `'userId': instance.userId`). You must decide whether the +**backend JSON contract** also changes to `workspaceId`: + +- If the API now expects/returns `workspaceId`: rename the Dart field AND let the + serializer use the new key (or add `@JsonKey(name: 'workspaceId')`). +- If the API still uses `userId` on the wire but you only want the Dart field + renamed: keep the wire key with `@JsonKey(name: 'userId')` on the renamed field. + +This must be coordinated with the backend team before regenerating. + +--- + +## Recommended migration order + +1. Confirm backend contract (see Open questions) — header vs query param, and the + JSON key name. +2. Add a `kWorkspaceIdKey` constant + prefs helper, plus a one-time migration shim + that copies existing `userId` -> `workspaceId`. +3. (Optional but recommended) Inject `X-Workspace-Id` centrally in `http.dart`. +4. Rename hand-written model fields / method params (sections D & E). +5. Update query-param/URL usages (section B), or remove them if moved to header. +6. Update the prefs key reads/writes (section A). +7. Regenerate build_runner outputs (section F). +8. `flutter analyze` and test the anonymous flow, login flow, history, groups, + batches, recipients, and pay flows end-to-end. + +## Open questions for the team + +- Is `workspaceId` a brand-new value from the backend (e.g. `response['workspaceId']`), + or just a rename of the existing `uuid`/`userId`? +- Should scoping move to a request **header** (`X-Workspace-Id`) so it can be set + once in `http.dart`, instead of ~15 per-call query params/body fields? +- Does the JSON wire contract change to `workspaceId`, or stay `userId`? +- For the anonymous/guest flow (`home_screen.dart`), is a client-generated + `Uuid().v4()` still acceptable as a `workspaceId`, or must the backend mint it? +- Does `phone_controller`'s `tempUid` body key need renaming too? + +## Summary counts + +- Non-generated `.dart` files to edit: **21** +- Generated files to regenerate: **6** +- Total non-generated `userId`/`user_id` occurrences: **~112** +- Existing `workspaceId` references: **0** diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index 7cd6614..8002a5f 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -31,6 +31,10 @@ class _HomeScreenState extends State with TickerProviderStateMixin { String initials = ''; String _selectedCurrency = 'USD'; + /// Incremented to force [AccountBalanceWidget] to recreate and re-read + /// auth state from disk (e.g. after logout or manual refresh). + int _balanceWidgetRefreshKey = 0; + late AnimationController _providersAnimController; late Animation _providersSlideAnimation; late Animation _providersFadeAnimation; @@ -143,6 +147,9 @@ class _HomeScreenState extends State with TickerProviderStateMixin { } Future _onRefresh() async { + setState(() { + _balanceWidgetRefreshKey++; + }); await homeController.getAccounts(); await homeController.getCategories(); await homeController.getProviders(_selectedCurrency); @@ -394,6 +401,7 @@ class _HomeScreenState extends State with TickerProviderStateMixin { crossAxisAlignment: CrossAxisAlignment.start, children: [ AccountBalanceWidget( + key: ValueKey(_balanceWidgetRefreshKey), selectedCurrency: _selectedCurrency, onCurrencySelected: _onCurrencySelected, ), @@ -691,6 +699,7 @@ class _HomeScreenState extends State with TickerProviderStateMixin { prefs.clear(); setState(() { _isLoggedIn = false; + _balanceWidgetRefreshKey++; }); authState.refresh(); setupDataAndTransitions(); diff --git a/lib/screens/onboarding/phone/phone_screen.dart b/lib/screens/onboarding/phone/phone_screen.dart index 853899a..a6b9df1 100644 --- a/lib/screens/onboarding/phone/phone_screen.dart +++ b/lib/screens/onboarding/phone/phone_screen.dart @@ -61,27 +61,6 @@ class _PhoneScreenState extends State { context, listen: false, ); - // Pre-fill the email field if it has already been captured. - final existingEmail = onboardingController.model.email; - if (existingEmail != null && existingEmail.isNotEmpty) { - _emailController.text = existingEmail; - } - // Pre-fill the password field if it has already been captured. - final existingPassword = onboardingController.model.password; - if (existingPassword != null && existingPassword.isNotEmpty) { - _passwordController.text = existingPassword; - _confirmPasswordController.text = existingPassword; - } - // Pre-fill the full name from firstName + lastName if previously captured. - final firstName = onboardingController.model.firstName; - final lastName = onboardingController.model.lastName; - final combined = [ - if (firstName != null && firstName.isNotEmpty) firstName, - if (lastName != null && lastName.isNotEmpty) lastName, - ].join(' '); - if (combined.isNotEmpty) { - _fullNameController.text = combined; - } } @override diff --git a/lib/screens/pay/pay_controller.dart b/lib/screens/pay/pay_controller.dart index 9bb7966..9cb5d82 100644 --- a/lib/screens/pay/pay_controller.dart +++ b/lib/screens/pay/pay_controller.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:provider/provider.dart'; @@ -129,14 +130,20 @@ class PayController extends ChangeNotifier { return ApiResponse.failure(model.errorMessage!); } } on DioException catch (e, s) { - logger.e(s); + if (kDebugMode) { + logger.e(e); + logger.e(s); + } model.isLoading = false; notifyListeners(); return ApiResponse.failure( "Network error. Please try again or contact support", ); } catch (e, s) { - logger.e(s); + if (kDebugMode) { + logger.e(e); + logger.e(s); + } model.isLoading = false; notifyListeners(); return ApiResponse.failure( diff --git a/nginx.conf b/nginx.conf index 942fd10..d7e26e3 100644 --- a/nginx.conf +++ b/nginx.conf @@ -17,6 +17,13 @@ http { gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml; gzip_min_length 1000; + # Deny access to hidden/dot files (.env, .git, etc.) + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + # Cache static assets location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg|json)$ { expires 6M;