-
Notifications
You must be signed in to change notification settings - Fork 419
fix(clerk-js,shared,types): Support calling password without identifier #6534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
37aced5
343da4b
41ebb72
8437b3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@clerk/clerk-js': minor | ||
| '@clerk/shared': minor | ||
| '@clerk/types': minor | ||
| --- | ||
|
|
||
| [Experimental] Signals: Add support for calling `signIn.password()` without an identifier. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -503,12 +503,18 @@ class SignInFuture implements SignInFutureResource { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| submitPassword: this.submitResetPassword.bind(this), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchStatus: 'idle' | 'fetching' = 'idle'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor(readonly resource: SignIn) {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| get status() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.resource.status; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| get availableStrategies() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.resource.supportedFirstFactors ?? []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async sendResetPasswordEmailCode(): Promise<{ error: unknown }> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| eventBus.emit('resource:error', { resource: this.resource, error: null }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -593,12 +599,13 @@ class SignInFuture implements SignInFutureResource { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async password({ identifier, password }: { identifier: string; password: string }): Promise<{ error: unknown }> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async password({ identifier, password }: { identifier?: string; password: string }): Promise<{ error: unknown }> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| eventBus.emit('resource:error', { resource: this.resource, error: null }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const previousIdentifier = this.resource.identifier; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await this.resource.__internal_basePost({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path: this.resource.pathRoot, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: { identifier, password }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: { identifier: identifier || previousIdentifier, password }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+602
to
609
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainBug: password() misses strategy and wrong endpoint; add identifier guard and use attempt_first_factor when applicable. Posting to /client/sign_ins without Apply this diff: - async password({ identifier, password }: { identifier?: string; password: string }): Promise<{ error: unknown }> {
- eventBus.emit('resource:error', { resource: this.resource, error: null });
- const previousIdentifier = this.resource.identifier;
- try {
- await this.resource.__internal_basePost({
- path: this.resource.pathRoot,
- body: { identifier: identifier || previousIdentifier, password },
- });
- } catch (err: unknown) {
- eventBus.emit('resource:error', { resource: this.resource, error: err });
- return { error: err };
- }
-
- return { error: null };
- }
+ async password({ identifier, password }: { identifier?: string; password: string }): Promise<{ error: unknown }> {
+ eventBus.emit('resource:error', { resource: this.resource, error: null });
+ this.fetchStatus = 'fetching';
+ const previousIdentifier = this.resource.identifier;
+ const idToUse = identifier ?? previousIdentifier ?? undefined;
+ try {
+ if (!this.resource.id) {
+ if (!idToUse) {
+ throw new Error('Cannot attempt password without an identifier. Call create() first or provide one.');
+ }
+ // Create + password in one call
+ await this.resource.__internal_basePost({
+ path: this.resource.pathRoot,
+ body: { strategy: 'password', identifier: idToUse, password },
+ });
+ } else {
+ // Attempt first factor on existing sign-in
+ await this.resource.__internal_basePost({
+ body: { strategy: 'password', password },
+ action: 'attempt_first_factor',
+ });
+ }
+ } catch (err: unknown) {
+ eventBus.emit('resource:error', { resource: this.resource, error: err });
+ return { error: err };
+ } finally {
+ this.fetchStatus = 'idle';
+ }
+
+ return { error: null };
+ }Additionally, consider a unit/integration test to cover:
🏁 Script executed: #!/bin/bash
set -e
# Show the current password() implementation with surrounding context
rg -n 'async password' -C10 packages/clerk-js/src/core/resources/SignIn.tsLength of output: 952 🏁 Script executed: #!/bin/bash
set -e
# Search for other uses of `strategy` in SignIn.ts
echo "=== strategy occurrences ==="
rg -n 'strategy' -C3 packages/clerk-js/src/core/resources/SignIn.ts || true
# Search for `action` parameter usages in SignIn.ts
echo "=== action occurrences ==="
rg -n 'action:' -C3 packages/clerk-js/src/core/resources/SignIn.ts || trueLength of output: 9787 🏁 Script executed: #!/bin/bash
set -e
# Find definition of __internal_basePost across the repo
rg -n 'function __internal_basePost' -C3
rg -n '__internal_basePost' -C3Length of output: 51 🏁 Script executed: #!/bin/bash
set -e
# Search for fetchStatus in SignIn.ts
rg -n 'fetchStatus' -C3 packages/clerk-js/src/core/resources/SignIn.ts || true
# Search for fetchStatus in the entire repo
rg -n 'fetchStatus' -C3 .Length of output: 23302 🏁 Script executed: #!/bin/bash
set -e
# Show first 50 lines of SignIn.ts to inspect class context
sed -n '1,50p' packages/clerk-js/src/core/resources/SignIn.tsLength of output: 1430 Implement proper strategy, action, identifier guard, and fetchStatus in password()
Diff: --- a/packages/clerk-js/src/core/resources/SignIn.ts
+++ b/packages/clerk-js/src/core/resources/SignIn.ts
@@ -602,14 +602,38 @@
async password({ identifier, password }: { identifier?: string; password: string }): Promise<{ error: unknown }> {
eventBus.emit('resource:error', { resource: this.resource, error: null });
+ this.fetchStatus = 'fetching';
const previousIdentifier = this.resource.identifier;
- try {
- await this.resource.__internal_basePost({
- path: this.resource.pathRoot,
- body: { identifier: identifier || previousIdentifier, password },
- });
- } catch (err: unknown) {
- eventBus.emit('resource:error', { resource: this.resource, error: err });
- return { error: err };
- }
-
- return { error: null };
+ const idToUse = identifier ?? previousIdentifier;
+ try {
+ if (!this.resource.id) {
+ if (!idToUse) {
+ throw new Error('Cannot attempt password without an identifier. Call create() first or provide one.');
+ }
+ // Create + password in one call
+ await this.resource.__internal_basePost({
+ path: this.resource.pathRoot,
+ body: { strategy: 'password', identifier: idToUse, password },
+ });
+ } else {
+ // Attempt first factor on existing sign-in
+ await this.resource.__internal_basePost({
+ body: { strategy: 'password', password },
+ action: 'attempt_first_factor',
+ });
+ }
+ } catch (err: unknown) {
+ eventBus.emit('resource:error', { resource: this.resource, error: err });
+ return { error: err };
+ } finally {
+ this.fetchStatus = 'idle';
+ }
+
+ return { error: null };
+ }Add corresponding unit/integration tests to cover all scenarios. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err: unknown) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| eventBus.emit('resource:error', { resource: this.resource, error: err }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -688,4 +695,20 @@ class SignInFuture implements SignInFutureResource { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { error: null }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async finalize(): Promise<{ error: unknown }> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| eventBus.emit('resource:error', { resource: this.resource, error: null }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!this.resource.createdSessionId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Cannot finalize sign-in without a created session.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await SignIn.clerk.setActive({ session: this.resource.createdSessionId }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err: unknown) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| eventBus.emit('resource:error', { resource: this.resource, error: err }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { error: err }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { error: null }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -82,7 +82,7 @@ export function isKnownError(error: any): error is ClerkAPIResponseError | Clerk | |
| * @internal | ||
| */ | ||
| export function isClerkAPIResponseError(err: any): err is ClerkAPIResponseError { | ||
| return 'clerkError' in err; | ||
| return err && 'clerkError' in err; | ||
| } | ||
|
Comment on lines
84
to
86
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard all type-narrowing helpers against falsy inputs to prevent runtime TypeErrors. Good fix on isClerkAPIResponseError. However, isKnownError can still throw via isMetamaskError / isClerkRuntimeError when passed null/undefined, because the Apply this diff: export function isKnownError(error: any): error is ClerkAPIResponseError | ClerkRuntimeError | MetamaskError {
- return isClerkAPIResponseError(error) || isMetamaskError(error) || isClerkRuntimeError(error);
+ // Guard against falsy input to avoid TypeError from `in` operator in downstream checks
+ if (!error) {
+ return false;
+ }
+ return isClerkAPIResponseError(error) || isMetamaskError(error) || isClerkRuntimeError(error);
}
export function isClerkAPIResponseError(err: any): err is ClerkAPIResponseError {
- return err && 'clerkError' in err;
+ return !!err && typeof err === 'object' && 'clerkError' in err;
}
export function isClerkRuntimeError(err: any): err is ClerkRuntimeError {
- return 'clerkRuntimeError' in err;
+ return !!err && typeof err === 'object' && 'clerkRuntimeError' in err;
}
export function isMetamaskError(err: any): err is MetamaskError {
- return 'code' in err && [4001, 32602, 32603].includes(err.code) && 'message' in err;
+ return (
+ !!err &&
+ typeof err === 'object' &&
+ 'code' in err &&
+ [4001, 32602, 32603].includes((err as any).code) &&
+ 'message' in err
+ );
}Also applies to: 105-106, 123-124, 75-77 🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -126,9 +126,16 @@ export interface SignInResource extends ClerkResource { | |||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface SignInFutureResource { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchStatus: 'idle' | 'fetching'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| availableStrategies: SignInFirstFactor[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| status: SignInStatus | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+129
to
131
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add JSDoc for new public APIs (fetchStatus, availableStrategies, password identifier fallback, finalize). Ensure discoverability and clarity for consumers. Apply this diff to document the additions: export interface SignInFutureResource {
- fetchStatus: 'idle' | 'fetching';
- availableStrategies: SignInFirstFactor[];
+ /**
+ * Reflects ongoing network activity for the future resource.
+ * 'fetching' while an API call is in flight, otherwise 'idle'.
+ * @experimental
+ */
+ fetchStatus: 'idle' | 'fetching';
+ /**
+ * Convenience mirror of SignIn.supportedFirstFactors. Always an array.
+ * @experimental
+ */
+ availableStrategies: SignInFirstFactor[];
status: SignInStatus | null;
- create: (params: { identifier: string }) => Promise<{ error: unknown }>;
- password: (params: { identifier?: string; password: string }) => Promise<{ error: unknown }>;
+ /**
+ * Attempts password auth. When identifier is omitted, falls back to the identifier
+ * set during a previous create() call.
+ * @experimental
+ */
+ password: (params: { identifier?: string; password: string }) => Promise<{ error: unknown }>;
...
- finalize: () => Promise<{ error: unknown }>;
+ /**
+ * Activates the created session (SignIn.createdSessionId) via Clerk.setActive.
+ * Returns `{ error: null }` on success or `{ error }` on failure.
+ * @experimental
+ */
+ finalize: () => Promise<{ error: unknown }>;
}Also applies to: 149-150, 133-134 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| create: (params: { identifier: string }) => Promise<{ error: unknown }>; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| password: (params: { identifier: string; password: string }) => Promise<{ error: unknown }>; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| create: (params: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| identifier?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| strategy?: OAuthStrategy | 'saml' | 'enterprise_sso'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| redirectUrl?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| actionCompleteRedirectUrl?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) => Promise<{ error: unknown }>; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+132
to
+137
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainDocument the expanded create() params and verify redirect param naming consistency. The widened signature looks correct; please add JSDoc describing each param and its behavior. Additionally, the naming differs from sso() which uses redirectUrlComplete while create() uses actionCompleteRedirectUrl. If this is intentional, document the distinction; otherwise consider aligning or supporting both (runtime-dependent). Apply this diff to document the params: status: SignInStatus | null;
- create: (params: {
+ /**
+ * Starts a sign-in with optional identifier and/or federated SSO strategy.
+ * When `strategy` is provided (OAuth/SAML/enterprise SSO), `redirectUrl` and
+ * optionally `actionCompleteRedirectUrl` control the return locations.
+ * `identifier` is optional here to support flows that defer it to a later step.
+ * @experimental
+ */
+ create: (params: {
identifier?: string;
strategy?: OAuthStrategy | 'saml' | 'enterprise_sso';
redirectUrl?: string;
actionCompleteRedirectUrl?: string;
}) => Promise<{ error: unknown }>;Run this to check for naming consistency across the repo: 🏁 Script executed: #!/bin/bash
# Find occurrences of these redirect params to confirm intent and consistency.
rg -n -S 'redirectUrlComplete|actionCompleteRedirectUrl' -A2 -B2Length of output: 50926 Document create() params and unify redirect param naming Please add JSDoc to the new • File: packages/types/src/signIn.ts (lines 132–137) Proposed diff for JSDoc and aliasing: status: SignInStatus | null;
+ /**
+ * Starts a sign-in flow, optionally with an identifier or federated SSO strategy.
+ * @param params.identifier Optional user identifier for credentialed sign-in.
+ * @param params.strategy OAuth/SAML/enterprise SSO strategy.
+ * @param params.redirectUrl URL to return to after initiating the flow.
+ * @param params.redirectUrlComplete
+ * Full URL or path to navigate to once sign-in completes. Alias for
+ * `actionCompleteRedirectUrl` to align with other APIs.
+ * @experimental
+ */
create: (params: {
identifier?: string;
strategy?: OAuthStrategy | 'saml' | 'enterprise_sso';
redirectUrl?: string;
- actionCompleteRedirectUrl?: string;
+ actionCompleteRedirectUrl?: string;
+ redirectUrlComplete?: string;
}) => Promise<{ error: unknown }>;Additionally, review and update the same pattern in:
📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| password: (params: { identifier?: string; password: string }) => Promise<{ error: unknown }>; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| emailCode: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| sendCode: (params: { email: string }) => Promise<{ error: unknown }>; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| verifyCode: (params: { code: string }) => Promise<{ error: unknown }>; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -144,6 +151,7 @@ export interface SignInFutureResource { | |||||||||||||||||||||||||||||||||||||||||||||||||
| redirectUrl: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| redirectUrlComplete: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) => Promise<{ error: unknown }>; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| finalize: () => Promise<{ error: unknown }>; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export type SignInStatus = | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
fetchStatus is never updated; wire it to network calls.
The property is added but never transitions to 'fetching'/'idle', limiting its usefulness as an observable.
As a pattern, set to 'fetching' before a request and back to 'idle' in finally blocks. See the password/finalize suggestions below for concrete diffs; apply similarly across other methods in SignInFuture.
🤖 Prompt for AI Agents