minor bug fixes
This commit is contained in:
160
WORKSPACE_ID_MIGRATION_AUDIT.md
Normal file
160
WORKSPACE_ID_MIGRATION_AUDIT.md
Normal file
@@ -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 <token>` 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**
|
||||||
@@ -31,6 +31,10 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
String initials = '';
|
String initials = '';
|
||||||
String _selectedCurrency = 'USD';
|
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 AnimationController _providersAnimController;
|
||||||
late Animation<Offset> _providersSlideAnimation;
|
late Animation<Offset> _providersSlideAnimation;
|
||||||
late Animation<double> _providersFadeAnimation;
|
late Animation<double> _providersFadeAnimation;
|
||||||
@@ -143,6 +147,9 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onRefresh() async {
|
Future<void> _onRefresh() async {
|
||||||
|
setState(() {
|
||||||
|
_balanceWidgetRefreshKey++;
|
||||||
|
});
|
||||||
await homeController.getAccounts();
|
await homeController.getAccounts();
|
||||||
await homeController.getCategories();
|
await homeController.getCategories();
|
||||||
await homeController.getProviders(_selectedCurrency);
|
await homeController.getProviders(_selectedCurrency);
|
||||||
@@ -394,6 +401,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
AccountBalanceWidget(
|
AccountBalanceWidget(
|
||||||
|
key: ValueKey(_balanceWidgetRefreshKey),
|
||||||
selectedCurrency: _selectedCurrency,
|
selectedCurrency: _selectedCurrency,
|
||||||
onCurrencySelected: _onCurrencySelected,
|
onCurrencySelected: _onCurrencySelected,
|
||||||
),
|
),
|
||||||
@@ -691,6 +699,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
prefs.clear();
|
prefs.clear();
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoggedIn = false;
|
_isLoggedIn = false;
|
||||||
|
_balanceWidgetRefreshKey++;
|
||||||
});
|
});
|
||||||
authState.refresh();
|
authState.refresh();
|
||||||
setupDataAndTransitions();
|
setupDataAndTransitions();
|
||||||
|
|||||||
@@ -61,27 +61,6 @@ class _PhoneScreenState extends State<PhoneScreen> {
|
|||||||
context,
|
context,
|
||||||
listen: false,
|
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
|
@override
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -129,14 +130,20 @@ class PayController extends ChangeNotifier {
|
|||||||
return ApiResponse.failure(model.errorMessage!);
|
return ApiResponse.failure(model.errorMessage!);
|
||||||
}
|
}
|
||||||
} on DioException catch (e, s) {
|
} on DioException catch (e, s) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
logger.e(e);
|
||||||
logger.e(s);
|
logger.e(s);
|
||||||
|
}
|
||||||
model.isLoading = false;
|
model.isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return ApiResponse.failure(
|
return ApiResponse.failure(
|
||||||
"Network error. Please try again or contact support",
|
"Network error. Please try again or contact support",
|
||||||
);
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
logger.e(e);
|
||||||
logger.e(s);
|
logger.e(s);
|
||||||
|
}
|
||||||
model.isLoading = false;
|
model.isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return ApiResponse.failure(
|
return ApiResponse.failure(
|
||||||
|
|||||||
@@ -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_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
|
||||||
gzip_min_length 1000;
|
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
|
# Cache static assets
|
||||||
location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg|json)$ {
|
location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg|json)$ {
|
||||||
expires 6M;
|
expires 6M;
|
||||||
|
|||||||
Reference in New Issue
Block a user