Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 13 additions & 1 deletion projects/kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,7 @@ import {
kitSignOut,
kitResolveAuthStatus,
kitReauthWithRetry,
type User,
} from '@rdlabo/ionic-angular-kit/auth-firebase';
import { updatePassword } from 'firebase/auth'; // escape hatch for the reauth mutation

Expand All @@ -956,13 +957,24 @@ export class AuthService {
}).catch((e) => (this.presentError(e), false));
if (ok) this.overlay.alertClose({ header: 'Saved', message: '…' });
}

// A delayed denial must not sign out a newer Firebase session. Capture the User that owns the
// request and pass it as expectedUser; the kit checks object identity after `before` completes
// and immediately before invoking Firebase signOut.
signOutDeniedSession(expectedUser: User) {
return kitSignOut(
this.#auth,
{ before: () => this.clearDeniedSessionTransport() },
{ expectedUser },
);
}
}
```

Surface:

- **DI** — `KIT_FIREBASE_AUTH` (`InjectionToken<Auth>`), `provideKitFirebase({ firebaseConfig })`, `provideKitFirebaseAnalytics()`.
- **Flow functions** (uniform hooks + no-throw null/false) — `kitSignIn`, `kitSignUp` (create + send verification), `kitSignOut`, `kitSendPasswordReset`, `kitSendEmailVerification`, `kitUnlinkProvider`.
- **Flow functions** (uniform hooks + no-throw null/false) — `kitSignIn`, `kitSignUp` (create + send verification), `kitSignOut`, `kitSendPasswordReset`, `kitSendEmailVerification`, `kitUnlinkProvider`. `kitSignOut(..., { expectedUser })` prevents an old asynchronous denial from signing out a newer Firebase User object.
- **Mechanics** — `kitReauthWithRetry` (app injects `prompt` / `withLoading` / `mutate`; boolean result, non-wrong-password errors thrown), `kitResolveAuthStatus` (`'user' | 'confirm' | 'required'` from the user; social counts as verified; `allowWhen` bypass), `kitAuthState`, `kitGetIdToken`.
- **Error dictionary** — `KIT_DEFAULT_AUTH_TEXT` (importable canonical constant; the kit does not present it — the app renders its own alert).
- **Social** (`@rdlabo/ionic-angular-kit/auth-firebase/social`, separate nested entry to isolate the Capacitor plugins) — `kitFacebookLogin`, `kitAppleLogin`, `kitFacebookLogout`; options carry the same `{ before, success, error, finally }` hooks (`success` receives the identity payload for a backend call).
Expand Down
28 changes: 28 additions & 0 deletions projects/kit/auth-firebase/src/kit-firebase-auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,34 @@ describe('bundled email/password flows (uniform hooks + no-throw null/false)', (
expect(error).toHaveBeenCalledWith(boom);
});

it('kitSignOut skips a newer Firebase session that replaces the expected user during before', async () => {
const expectedUser = { uid: 'same-uid' };
const newerUser = { uid: 'same-uid' };
const auth = authWith(expectedUser);
const success = vi.fn();
const error = vi.fn();
const finallyHook = vi.fn();

const result = await kitSignOut(
auth,
{
before: async () => {
(auth as unknown as { currentUser: unknown }).currentUser = newerUser;
},
success,
error,
finally: finallyHook,
},
{ expectedUser: expectedUser as unknown as import('firebase/auth').User },
);

expect(result).toBe(false);
expect(signOut).not.toHaveBeenCalled();
expect(success).not.toHaveBeenCalled();
expect(error).not.toHaveBeenCalled();
expect(finallyHook).toHaveBeenCalledOnce();
});

it('kitSendEmailVerification is a no-op (still true) when signed out, sends otherwise', async () => {
expect(await kitSendEmailVerification(authWith(null))).toBe(true);
expect(sendEmailVerification).not.toHaveBeenCalled();
Expand Down
33 changes: 32 additions & 1 deletion projects/kit/auth-firebase/src/kit-firebase-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ export interface KitAuthHooks {
finally?: () => void | Promise<unknown>;
}

/** Optional identity guard evaluated immediately before Firebase sign-out is invoked. */
export interface KitSignOutOptions {
/**
* Only sign out when this exact Firebase User object is still current after `before` completes.
*
* @remarks
* A different object is treated as a newer authentication session even when it has the same UID.
* A mismatch resolves `false`, skips `success` and `error`, and still runs `finally`.
*/
expectedUser?: User;
}

/** Run a value-returning op through the {@link KitAuthHooks} lifecycle; resolve `null` on failure. */
const runAuthFlow = async <T>(op: () => Promise<T>, hooks?: KitAuthHooks): Promise<T | null> => {
await hooks?.before?.();
Expand Down Expand Up @@ -101,7 +113,26 @@ export const kitSignUp = (auth: Auth, email: string, password: string, hooks?: K
* App-specific cleanup (clearing stores, toasts, navigation, third-party logout) is the caller's,
* done via the hooks — the kit only owns the Firebase op. `true` on success, `false` on failure.
*/
export const kitSignOut = (auth: Auth, hooks?: KitAuthHooks): Promise<boolean> => runAuthFlowVoid(() => signOut(auth), hooks);
export const kitSignOut = async (
auth: Auth,
hooks?: KitAuthHooks,
options?: KitSignOutOptions,
): Promise<boolean> => {
await hooks?.before?.();
try {
if (options?.expectedUser !== undefined && auth.currentUser !== options.expectedUser) {
return false;
}
await signOut(auth);
await hooks?.success?.();
return true;
} catch (error) {
await hooks?.error?.(error);
return false;
} finally {
await hooks?.finally?.();
}
};

/** Send a password-reset email. `true` on success, `false` on failure. */
export const kitSendPasswordReset = (auth: Auth, email: string, hooks?: KitAuthHooks): Promise<boolean> =>
Expand Down
1 change: 1 addition & 0 deletions projects/kit/auth-firebase/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export {
} from './kit-firebase-auth';
export type {
KitAuthHooks,
KitSignOutOptions,
KitAuthStatus,
KitResolveAuthStatusOptions,
KitReauthWithRetryOptions,
Expand Down