Skip to content

Conversation

@pankajbaid567
Copy link

@pankajbaid567 pankajbaid567 commented Dec 13, 2025

Fix: Prevent duplicate API calls on Sign Up button rapid taps

Issue

Fixes #612

Problem

When users rapidly tap the Sign Up button multiple times, the app sends multiple API requests to Appwrite. The first request successfully creates the user, but subsequent requests fail with "User already exists" error, causing poor UX and unnecessary backend load.

Solution

Implemented a controller-level processing lock combined with reactive UI updates to prevent duplicate sign-up requests.

Flow Diagram

sequenceDiagram
    participant User
    participant SignupScreen
    participant AuthController
    participant AppwriteAPI

    User->>SignupScreen: Tap Sign Up
    SignupScreen->>AuthController: signup()
    AuthController->>AuthController: Check isLoading
    alt isLoading is true
        AuthController-->>SignupScreen: return false (blocked)
        Note right of SignupScreen: Duplicate tap ignored
    else isLoading is false
        AuthController->>AuthController: Set isLoading = true
        SignupScreen->>SignupScreen: Disable button & form fields
        SignupScreen->>SignupScreen: Show loading indicator
        AuthController->>AppwriteAPI: Create user
        AppwriteAPI-->>AuthController: Response
        AuthController->>AuthController: Set isLoading = false (finally)
        SignupScreen->>SignupScreen: Re-enable button & form fields
        AuthController-->>SignupScreen: return result
    end
Loading

Changes

lib/controllers/authentication_controller.dart

  • Added early return guard at the start of signup() method
  • If isLoading.value is already true, returns false immediately without making API call
Future<bool> signup(BuildContext context) async {
  // Early return if already processing - prevents duplicate API calls
  if (isLoading.value) return false;
  
  try {
    isLoading.value = true;
    // ... API call
  } finally {
    isLoading.value = false;
  }
}

lib/views/screens/signup_screen.dart

  • Wrapped Sign Up button with Obx for reactive state binding
  • Button is disabled when isLoading.value is true
  • Added enabled: !controller.isLoading.value to all form fields (email, password, confirm password)
  • Form fields are now disabled during signup processing

test/controllers/authentication_controller_test.dart

  • Added unit tests for the processing lock behavior:
    • Verify signup() returns false when already loading
    • Verify isLoading is reset after success/failure
    • Verify correct return values

Testing

  • Manual testing: Rapid button taps no longer trigger multiple API calls
  • Button shows loading indicator and is disabled during processing
  • Form fields are disabled during processing
  • Unit tests pass

Screenshots/Video

Add before/after video showing the fix in action

Summary by CodeRabbit

  • New Features

    • Sign-up now short-circuits when a submission is already in progress to prevent duplicate requests
  • Improvements

    • Email/password inputs and submit button disable automatically while signing up to improve feedback and avoid concurrent actions
  • Tests

    • Added tests covering signup outcomes and loading-state lifecycle (including early-return behavior)

✏️ Tip: You can customize this high-level summary in your review settings.

@github-actions
Copy link
Contributor

🎉 Welcome @pankajbaid567!
Thank you for your pull request! Our team will review it soon. 🔍

  • Please ensure your PR follows the contribution guidelines. ✅
  • All automated tests should pass before merging. 🔄
  • If this PR fixes an issue, link it in the description. 🔗

We appreciate your contribution! 🚀

@coderabbitai
Copy link

coderabbitai bot commented Dec 13, 2025

Walkthrough

Adds an early guard in the AuthenticationController.signup to prevent concurrent sign-up calls, disables form inputs and the submit button while loading in the sign-up screen, and adds tests validating the processing-lock and isLoading lifecycle.

Changes

Cohort / File(s) Summary
Controller signup guard
lib/controllers/authentication_controller.dart
Adds an early return when isLoading is true to prevent concurrent signup API calls; no public signatures changed.
UI input & submit-state binding
lib/views/screens/signup_screen.dart
Wraps input fields and submit button with Obx to bind enabled/disabled and button state to isLoading; submit logic toggles signUpIsAllowed and preserves existing validation, messaging, and loading indicator.
Signup flow tests
test/controllers/authentication_controller_test.dart
New test suite covering early-return when isLoading is true, isLoading lifecycle during success/failure, and signup outcome; uses mocked AuthStateController and test BuildContext scaffolding.
Manifest
pubspec.yaml
(Touched in PR manifest)

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as SignUp Screen (UI)
  participant C as AuthenticationController
  participant S as Auth Service / API

  UI->>C: onPressed(signup)
  alt controller.isLoading == true
    C-->>UI: return false (ignore)
  else
    UI->>UI: disable inputs & button (isLoading=true)
    C->>S: perform signup API call
    alt signup succeeds
      S-->>C: success
      C-->>UI: navigate / show success
    else signup fails
      S-->>C: error
      C-->>UI: show error / snackbar
    end
    C->>UI: set isLoading=false (re-enable inputs/button)
    C-->>UI: return result (true/false)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Review the early-return guard to ensure no required cleanup is skipped in any code path.
  • Confirm UI bindings cover all input fields and that semantic announcements/navigation occur only once.
  • Inspect tests for realistic mocking of async behavior and that they exercise lifecycle transitions during navigation.

Poem

🐰
Tap once, I say, and wait a beat,
Guards rise up soft on nimble feet,
Buttons hush, the network sings,
One bright request on steady wings,
No duplicates now—only one treat.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: preventing duplicate API calls on rapid Sign Up button taps, which is the primary objective of this pull request.
Linked Issues check ✅ Passed The pull request successfully implements all coding requirements from issue #612: early-return guard in signup() when isLoading is true, UI disabling of button/fields during loading, and unit tests verifying the processing lock behavior.
Out of Scope Changes check ✅ Passed All changes are directly related to the stated objective of preventing duplicate signup API calls. No unrelated modifications or refactoring are present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee24210 and e705134.

📒 Files selected for processing (1)
  • test/controllers/authentication_controller_test.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/controllers/authentication_controller_test.dart

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
test/controllers/authentication_controller_test.dart (1)

48-64: Test doesn't verify the stated behavior.

The test name claims to verify that isLoading is set to true at the start, but the assertion never actually checks this. The comment on lines 61-63 acknowledges the limitation.

Consider using a Completer to pause execution mid-signup and verify the loading state:

 test('signup sets isLoading to true at the start', () async {
   // Arrange
   authController.emailController.text = 'test@test.com';
   authController.passwordController.text = 'Password123';

+  final completer = Completer<void>();
   when(
     mockAuthStateController.signup(any, any),
-  ).thenAnswer((_) async {});
+  ).thenAnswer((_) => completer.future);

   // Act
   expect(authController.isLoading.value, false);
   final signupFuture = authController.signup(_MockBuildContext());

-  // Assert: isLoading should be true during the call
-  // Note: This is tricky to test synchronously, so we verify after completion
+  // Assert: isLoading should be true while waiting
+  expect(authController.isLoading.value, true);
+
+  completer.complete();
   await signupFuture;
+  expect(authController.isLoading.value, false);
 });
lib/views/screens/signup_screen.dart (1)

238-296: Solid dual-layer protection against duplicate submissions.

The combination of signUpIsAllowed (UI-level, synchronous) and isLoading (controller-level) provides robust protection. The immediate setting of signUpIsAllowed = false at line 251 before the await ensures no gap in protection.

One minor consideration: if any code between lines 256-275 throws (e.g., during navigation or snackbar), signUpIsAllowed won't reset. A try-finally pattern could make this more robust, though the risk is low:

                                    emailVerifyController
                                        .signUpIsAllowed
                                        .value = false;
+                                   try {
                                    var isSignedIn = await controller.signup(
                                      context,
                                    );
                                    if (isSignedIn) {
                                      Get.toNamed(AppRoutes.onBoarding);
                                      customSnackbar(
                                        // ... snackbar code
                                      );
                                      SemanticsService.announce(
                                        // ... announce code
                                      );
-                                     emailVerifyController
-                                         .signUpIsAllowed
-                                         .value = true;
-                                   } else {
-                                     emailVerifyController
-                                         .signUpIsAllowed
-                                         .value = true;
                                    }
+                                   } finally {
+                                     emailVerifyController
+                                         .signUpIsAllowed
+                                         .value = true;
+                                   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b063ecb and ee24210.

📒 Files selected for processing (3)
  • lib/controllers/authentication_controller.dart (1 hunks)
  • lib/views/screens/signup_screen.dart (3 hunks)
  • test/controllers/authentication_controller_test.dart (1 hunks)
🔇 Additional comments (3)
lib/controllers/authentication_controller.dart (1)

70-72: LGTM! Clean implementation of the processing lock.

The early guard correctly prevents concurrent API calls. Since Dart's event loop is single-threaded and the check-then-set happens synchronously before any await, there's no race condition risk for UI-triggered events.

test/controllers/authentication_controller_test.dart (1)

19-24: Good test setup with explicit mock assignment.

The direct assignment at line 23 ensures the mock is used, which is appropriate for unit testing. The combination of Get.put() and direct assignment provides belt-and-suspenders coverage.

lib/views/screens/signup_screen.dart (1)

70-85: LGTM! Form fields correctly disabled during loading.

The Obx wrapper ensures the email field reactively updates when isLoading changes, preventing user input during the API call. The same pattern is correctly applied to password fields at lines 89 and 132.

@M4dhav
Copy link
Contributor

M4dhav commented Dec 17, 2025

Closing as issue is not part of the unstoppable hackathon and already assigned to another contributor

@M4dhav M4dhav closed this Dec 17, 2025
@github-actions
Copy link
Contributor

PR Closed - Thank You, @pankajbaid567!

  • If this PR was merged: Congratulations! Your contribution is now part of the project. 🚀
  • If this PR was closed without merging: Don’t worry! You can always improve it and submit again. 💪

We appreciate your effort and look forward to more contributions from you! 🤝

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sign-Up button allows multiple taps causing duplicate API calls & "User already exists" error

3 participants