Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
06db5ac
Add username unavailability indicator and validation to Edit Profile …
4555jan Oct 27, 2025
847dd64
fix:mistakenly made another variable for username checking in control…
4555jan Nov 8, 2025
8e79723
changed the minimum length to 7
4555jan Nov 11, 2025
be91e5a
kept the milliseconds to 800 like before
4555jan Nov 11, 2025
e9851b9
solved a localization error
4555jan Nov 11, 2025
4602600
Merge branch 'dev' into feat-username-validation
4555jan Nov 19, 2025
0a0ec5f
Fix username validation and add proper checking state
4555jan Nov 19, 2025
1c427f9
separate username validation error messages and removed unnecessary …
4555jan Nov 23, 2025
ec564d6
removed an unnecessary string
4555jan Nov 23, 2025
d024bd3
fix: added description for last four strings
4555jan Nov 23, 2025
9c49bb7
fix: disabled the save changes button when the username is invalid
4555jan Nov 23, 2025
a8926cd
fix: Remove username availability check and added try catch block
4555jan Nov 23, 2025
1f65dea
Merge branch 'dev' into feat-username-validation
4555jan Nov 25, 2025
0bf8294
Fix single try and catch block
4555jan Nov 25, 2025
9a70110
removed the snakebars and moved to the validator
4555jan Jan 4, 2026
31665f5
Fix fixed save profile button and icon when users
4555jan Jan 6, 2026
cbc7a09
fix: reduced unnecessary conditions
4555jan Jan 7, 2026
55aee19
fix: translations
4555jan Jan 7, 2026
86ef78d
fix: username validation showing red X if username is avilable
4555jan Jan 10, 2026
5f08373
fix: make sure that save button doesnt call api on current username a…
4555jan Jan 10, 2026
683c6c3
fix: checks were failing for usernameavilable so had to edit testfile…
4555jan Jan 10, 2026
1ea8357
fix: checks were failing
4555jan Jan 10, 2026
29392b1
fix: added test for both functions and circular controller added back
4555jan Jan 11, 2026
f44d70c
fix: added mock for another user
4555jan Jan 11, 2026
844fc6d
fix: rethrow showSuccessSnackbar
4555jan Jan 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 15 additions & 34 deletions lib/controllers/edit_profile_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class EditProfileController extends GetxController {

RxBool isLoading = false.obs;
Rx<bool> usernameAvailable = false.obs;

Rx<bool> usernameChecking = false.obs;
bool removeImage = false;
bool showSuccessSnackbar = false;

Expand Down Expand Up @@ -259,49 +259,30 @@ class EditProfileController extends GetxController {

// Update USERNAME
if (isUsernameChanged()) {
var usernameAvail = await isUsernameAvailable(
usernameController.text.trim(),
);
if (!usernameAvail) {
usernameAvailable.value = false;
customSnackbar(
AppLocalizations.of(Get.context!)!.usernameUnavailable,
AppLocalizations.of(Get.context!)!.usernameInvalidOrTaken,
LogType.error,
);

SemanticsService.announce(
AppLocalizations.of(Get.context!)!.usernameInvalidOrTaken,
TextDirection.ltr,
);
return;
}

// Create new doc of New Username
await tables.createRow(
databaseId: userDatabaseID,
tableId: usernameTableID,
rowId: usernameController.text.trim(),
data: {'email': authStateController.email},
);

try {
// Delete Old Username doc, so Username can be re-usable
await tables.createRow(
databaseId: userDatabaseID,
tableId: usernameTableID,
rowId: usernameController.text.trim(),
data: {'email': authStateController.email},
);

await tables.deleteRow(
databaseId: userDatabaseID,
tableId: usernameTableID,
rowId: oldUsername,
);

await tables.updateRow(
databaseId: userDatabaseID,
tableId: usersTableID,
rowId: authStateController.uid!,
data: {"username": usernameController.text.trim()},
);
} catch (e) {
log(e.toString());
}
Comment on lines 270 to 294
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent error handling may mask username update failures.

The try-catch block silently logs errors during username updates. If createRow succeeds but deleteRow or updateRow fails, the operation is partially complete, yet showSuccessSnackbar may still be true from line 209, misleading the user.

Consider re-throwing or setting showSuccessSnackbar = false on error so the user receives accurate feedback.

Suggested improvement
         try {
           await tables.createRow(
             databaseId: userDatabaseID,
             tableId: usernameTableID,
             rowId: usernameController.text.trim(),
             data: {'email': authStateController.email},
           );

           await tables.deleteRow(
             databaseId: userDatabaseID,
             tableId: usernameTableID,
             rowId: oldUsername,
           );

           await tables.updateRow(
             databaseId: userDatabaseID,
             tableId: usersTableID,
             rowId: authStateController.uid!,
             data: {"username": usernameController.text.trim()},
           );
         } catch (e) {
           log(e.toString());
+          showSuccessSnackbar = false;
+          rethrow; // Let the outer catch handle user notification
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
// Delete Old Username doc, so Username can be re-usable
await tables.createRow(
databaseId: userDatabaseID,
tableId: usernameTableID,
rowId: usernameController.text.trim(),
data: {'email': authStateController.email},
);
await tables.deleteRow(
databaseId: userDatabaseID,
tableId: usernameTableID,
rowId: oldUsername,
);
await tables.updateRow(
databaseId: userDatabaseID,
tableId: usersTableID,
rowId: authStateController.uid!,
data: {"username": usernameController.text.trim()},
);
} catch (e) {
log(e.toString());
}
try {
await tables.createRow(
databaseId: userDatabaseID,
tableId: usernameTableID,
rowId: usernameController.text.trim(),
data: {'email': authStateController.email},
);
await tables.deleteRow(
databaseId: userDatabaseID,
tableId: usernameTableID,
rowId: oldUsername,
);
await tables.updateRow(
databaseId: userDatabaseID,
tableId: usersTableID,
rowId: authStateController.uid!,
data: {"username": usernameController.text.trim()},
);
} catch (e) {
log(e.toString());
showSuccessSnackbar = false;
rethrow; // Let the outer catch handle user notification
}
🤖 Prompt for AI Agents
In @lib/controllers/edit_profile_controller.dart around lines 270 - 292, The
try-catch around tables.createRow / tables.deleteRow / tables.updateRow swallows
errors and can leave showSuccessSnackbar true even when later steps fail; modify
the catch in the block containing createRow/deleteRow/updateRow to either
rethrow the caught exception or explicitly set showSuccessSnackbar = false (and
update any UI state) before returning, and ensure any upstream success path (the
code that calls showSuccessSnackbar) only runs after all three operations
complete without error; reference the existing tables.createRow,
tables.deleteRow, tables.updateRow calls and the showSuccessSnackbar variable to
locate where to add the state change or rethrow.


await tables.updateRow(
databaseId: userDatabaseID,
tableId: usersTableID,
rowId: authStateController.uid!,
data: {"username": usernameController.text.trim()},
);
}

//Update user DISPLAY-NAME
Expand Down
21 changes: 19 additions & 2 deletions lib/l10n/app_en.arb
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add translations for these strings for your native languages

Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,7 @@
"@stable": {
"description": "A label indicating a stable, non-beta version of the app."
},
"usernameCharacterLimit": "Username should contain more than 5 characters.",
"usernameCharacterLimit": "Username should contain more than 7 characters.",
"@usernameCharacterLimit": {
"description": "Error message when a chosen username is too short."
},
Expand Down Expand Up @@ -1767,7 +1767,24 @@
"description": "Confirmation text asking the user if they want to delete a message."
},
"thisMessageWasDeleted": "This message was deleted",
"failedToDeleteMessage": "Failed to delete message"
"@thisMessageWasDeleted": {
"description": "Status text shown when a previously sent message has been deleted."
},

"failedToDeleteMessage": "Failed to delete message",
"@failedToDeleteMessage": {
"description": "Error message shown when the system is unable to delete a message."
},

"usernameInvalidFormat": "Please enter a valid username. Only letters, numbers, dots, underscores, and hyphens are allowed.",
"@usernameInvalidFormat": {
"description": "Validation error displayed when the user enters a username with unsupported characters."
},

"usernameAlreadyTaken": "This username is already taken. Try a different one.",
"@usernameAlreadyTaken": {
"description": "Error shown when the chosen username is unavailable because another user has already registered it."
}


}
10 changes: 9 additions & 1 deletion lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ class AppLocalizationsEn extends AppLocalizations {

@override
String get usernameCharacterLimit =>
'Username should contain more than 5 characters.';
'Username should contain more than 7 characters.';

@override
String get submit => 'Submit';
Expand Down Expand Up @@ -1355,4 +1355,12 @@ class AppLocalizationsEn extends AppLocalizations {

@override
String get failedToDeleteMessage => 'Failed to delete message';

@override
String get usernameInvalidFormat =>
'Please enter a valid username. Only letters, numbers, dots, underscores, and hyphens are allowed.';

@override
String get usernameAlreadyTaken =>
'This username is already taken. Try a different one.';
}
97 changes: 71 additions & 26 deletions lib/views/screens/edit_profile_screen.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loading_animation_widget/loading_animation_widget.dart';
import 'package:resonate/themes/theme_controller.dart';
import 'package:resonate/utils/debouncer.dart';
import 'package:resonate/utils/enums/log_type.dart';
import 'package:resonate/utils/ui_sizes.dart';
import 'package:resonate/views/widgets/loading_dialog.dart';
import 'package:resonate/views/widgets/snackbar.dart';
import 'package:resonate/l10n/app_localizations.dart';

import '../../controllers/auth_state_controller.dart';
Expand All @@ -22,6 +24,7 @@ class EditProfileScreen extends StatelessWidget {
final AuthStateController authStateController = Get.put(
AuthStateController(),
);
final debouncer = Debouncer(milliseconds: 800);

@override
Widget build(BuildContext context) {
Expand All @@ -30,9 +33,7 @@ class EditProfileScreen extends StatelessWidget {
!(editProfileController.isLoading.value ||
editProfileController.isThereUnsavedChanges()),
onPopInvokedWithResult: (didPop, result) async {
if (didPop) {
return;
}
if (didPop) return;

if (!editProfileController.isLoading.value &&
editProfileController.isThereUnsavedChanges()) {
Expand Down Expand Up @@ -112,16 +113,25 @@ class EditProfileScreen extends StatelessWidget {
keyboardType: TextInputType.text,
autocorrect: false,
decoration: InputDecoration(
// hintText: "Name",
labelText: AppLocalizations.of(context)!.name,
prefixIcon: Icon(Icons.abc_rounded),
),
),
SizedBox(height: UiSizes.height_20),
Obx(
() => TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
maxLength: 36,
validator: (value) {
if (value!.length > 5) {
if (value!.length >= 7) {
final validUsername = RegExp(
r'^[a-zA-Z0-9._-]+$',
).hasMatch(value.trim());
if (!validUsername) {
return AppLocalizations.of(
context,
)!.usernameInvalidFormat;
}
return null;
} else {
return AppLocalizations.of(
Expand All @@ -130,23 +140,54 @@ class EditProfileScreen extends StatelessWidget {
}
},
controller: controller.usernameController,
onChanged: (value) async {
if (value.length > 5) {
controller.usernameAvailable.value =
await controller.isUsernameAvailable(
value.trim(),
);
} else {
onChanged: (value) {
Get.closeCurrentSnackbar();

if (value.length < 7) {
controller.usernameAvailable.value = false;
return;
}

controller.usernameChecking.value = true;
controller.usernameAvailable.value = false;

debouncer.run(() async {
final available = await controller
.isUsernameAvailable(value.trim());

controller.usernameChecking.value = false;
controller.usernameAvailable.value = available;

if (!available) {
customSnackbar(
AppLocalizations.of(
context,
)!.usernameUnavailable,
AppLocalizations.of(
context,
)!.usernameAlreadyTaken,
LogType.error,
snackbarDuration: 1,
);
}
});
},

keyboardType: TextInputType.text,
autocorrect: false,
decoration: InputDecoration(
// hintText: "Username",
labelText: AppLocalizations.of(context)!.username,
prefixIcon: const Icon(Icons.person),
suffixIcon: controller.usernameAvailable.value
suffixIcon:
!controller.usernameChecking.value &&
controller.usernameAvailable.value &&
controller.usernameController.text
.trim()
.length >=
7 &&
RegExp(r'^[a-zA-Z0-9._-]+$').hasMatch(
controller.usernameController.text.trim(),
)
? const Icon(
Icons.verified_outlined,
color: Colors.green,
Expand Down Expand Up @@ -182,11 +223,20 @@ class EditProfileScreen extends StatelessWidget {
() => SizedBox(
width: double.maxFinite,
child: ElevatedButton(
onPressed: () async {
if (!controller.isLoading.value) {
await controller.saveProfile();
}
},
onPressed:
(!controller.isLoading.value &&
controller.usernameAvailable.value &&
controller.usernameController.text
.trim()
.length >=
7 &&
RegExp(r'^[a-zA-Z0-9._-]+$').hasMatch(
controller.usernameController.text.trim(),
))
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of using all these conditions, just set usernameAvailable to false if any of the conditions are violated (length/composition)

? () async {
await controller.saveProfile();
}
: null,
child: controller.isLoading.value
? Center(
child:
Expand Down Expand Up @@ -298,12 +348,9 @@ class EditProfileScreen extends StatelessWidget {
Column(
children: [
IconButton(
tooltip: AppLocalizations.of(
context,
)!.clickPictureCamera,
tooltip: AppLocalizations.of(context)!.clickPictureCamera,
onPressed: () {
Navigator.pop(context);
// Display Loading Dialog
loadingDialog(context);
editProfileController.pickImageFromCamera();
},
Expand All @@ -322,8 +369,6 @@ class EditProfileScreen extends StatelessWidget {
tooltip: AppLocalizations.of(context)!.pickImageGallery,
onPressed: () {
Navigator.pop(context);

// Display Loading Dialog
loadingDialog(context);
editProfileController.pickImageFromGallery();
},
Expand Down