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
5 changes: 5 additions & 0 deletions docs/auth/sign-in.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Sign In

You can use one of the ways below to sign in a user:

- [`useSignIn` hook](../hooks/useSignIn.md)
113 changes: 113 additions & 0 deletions docs/hooks/useSignIn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
tags:
- hook
---

# `useSignIn` Hook

`useSignIn` hook is used to sign in a user. A very simple example would be:

```typescript
const { dispatch } = useSignIn({ auth });
await dispatch({
type: "classic",
email,
password,
});
```

!!! warning
`useSignIn` is lazy by default and will not do anything until you use `dispatch` function.

You can also get the state[^unauthorized] of sign-in process.

```typescript
const { state, dispatch } = useSignIn({ auth });
await dispatch({
type: "classic",
email,
password,
});
// `state` is "ready" | "loading" | "authenticated"
```

!!! warning
`useSignIn` automatically listens to authentication state and will be `"authenticated"` if the user is authenticated. In `"authenticated"` state, `dispatch` will simply do nothing even if it is invoked.

`dispatch` method will return an instance of [`UserCredential`][UserCredentialDocRef].

```typescript
const { state, dispatch } = useSignIn({ auth });
const credential = await dispatch({
type: "classic",
email,
password,
});
// do something with `credential`
```

## Input Parameters

Input parameters for `useSignIn` hook is as follows:

| Name | Type | Description | Required | Default Value |
|---|---|---|---|---|
| `auth` | [`firebase/auth/Auth`][AuthRefDoc] | Reference to the Firebase Auth service instance. | ✅ | - |

## Return Type

`useSignIn` hook returns an object with properties as below:

| Name | Type | Description |
|---|---|---|
| `state` | `"ready" | "loading" | "authenticated"`[^unauthorized] | The state of sign-up process. |
| `dispatch` | [`UseSignInDispatcher`](#sign-in-methods) | A callback to start sign-up process. |

## Sign In Methods

There are many sign-in methods available. The available ones are:

| Method | `type` | `provider` |
|---|---|---|
| Email and password | `classic` | ❌ |
| Google | `google` | [`GoogleAuthProvider`][GoogleAuthProviderRefDoc] |
| Facebook | `facebook` | [`FacebookAuthProvider`][FacebookAuthProviderRefDoc] |
| Apple | `apple` | [`OAuthProvider`][OAuthProviderRefDoc] |
| Microsoft | `microsoft` | [`OAuthProvider`][OAuthProviderRefDoc] |
| Yahoo | `yahoo` | [`OAuthProvider`][OAuthProviderRefDoc] |
| Twitter | `twitter` | [`TwitterAuthProvider`][TwitterAuthProviderRefDoc] |
| Github | `github` | [`GithubAuthProvider`][GithubAuthProviderRefDoc] |

`dispatch` method will require an object as parameter. This object will always have property of `type: string`. `type` will correspond to what kind of method you will prefer while signing in a visitor.

If `type` is `"classic"` (email-password authentication), it's pretty simple:

```typescript
await dispatch({
type: "classic",
email,
password,
});
```

If `type` is something else, you need to provide initialized `*AuthProvider` instance. An example for Google sign-in looks as such:

```typescript
const { state, dispatch } = useSignIn({ auth });
const provider = new GoogleAuthProvider();
await dispatch({
type: "google",
provider,
});
// state is "loading" until user signs in
```

[^unauthorized]: You can consider `"authenticated"` state as logically *unauthorized*. Your website visitors are not authorized to sign in again if they are already authenticated (signed-in).

[AuthRefDoc]: https://firebase.google.com/docs/reference/node/firebase.auth.Auth
[UserCredentialDocRef]: https://firebase.google.com/docs/reference/js/auth.usercredential
[OAuthProviderRefDoc]: https://firebase.google.com/docs/reference/node/firebase.auth.OAuthProvider
[GoogleAuthProviderRefDoc]: https://firebase.google.com/docs/reference/node/firebase.auth.GoogleAuthProvider
[FacebookAuthProviderRefDoc]: https://firebase.google.com/docs/reference/node/firebase.auth.FacebookAuthProvider
[TwitterAuthProviderRefDoc]: https://firebase.google.com/docs/reference/node/firebase.auth.TwitterAuthProvider
[GithubAuthProviderRefDoc]: https://firebase.google.com/docs/reference/node/firebase.auth.GithubAuthProvider
2 changes: 1 addition & 1 deletion docs/hooks/useSignUp.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ await dispatch(email, password);
!!! warning
`useSignUp` is lazy by default and will not do anything until you use `dispatch` function.

You can also get the state[^unauthorized] of sign-out process.
You can also get the state[^unauthorized] of sign-up process.

```typescript
const { state, dispatch } = useSignUp({ auth });
Expand Down
3 changes: 3 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ nav:
- auth/conditionally-render-by-authentication.md
- auth/get-user.md
- auth/sign-up.md
- auth/sign-in.md
- auth/sign-out.md
- auth/delete-user.md
- Hooks:
Expand All @@ -62,6 +63,8 @@ nav:
- hooks/useDeleteDocument.md
- Auth:
- hooks/useUser.md
- hooks/useSignUp.md
- hooks/useSignIn.md
- hooks/useSignOut.md
- hooks/useDeleteUser.md
- Components:
Expand Down
2 changes: 2 additions & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export * from "./useSignUp";
export * from "./useDeleteUser";

export * from "./AuthorizationZone";

export * from "./useSignIn";
83 changes: 83 additions & 0 deletions src/auth/useSignIn.hook.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) 2024 Eray Erdin
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

import {
UserCredential,
createUserWithEmailAndPassword,
deleteUser,
signInAnonymously,
signInWithEmailAndPassword,
signOut,
} from "@firebase/auth";
import { renderHook } from "@testing-library/react";
import { useSignIn } from ".";
import { auth } from "../firebase";

const generateEmail = (id: string) => `usesignin_${id}@hook.com`;
const password = "111111" as const;

describe("when authed, useSignIn hook", () => {
let credential: UserCredential;
const index: number = 0;

beforeEach(async () => {
const email = generateEmail(index.toString());
await createUserWithEmailAndPassword(auth, email, password);
credential = await signInWithEmailAndPassword(auth, email, password);
});

afterEach(async () => {
await signOut(auth);
await deleteUser(credential.user);
});

it("should have authenticated state", async () => {
const { result } = renderHook(() => useSignIn({ auth }));
const { state } = result.current;
expect(state).toBe("authenticated");
});
});

describe("when anon, useSignIn hook", () => {
it("should have ready state if real anon", async () => {
// setup
await signOut(auth);

// test
const { result } = renderHook(() => useSignIn({ auth }));
const { state } = result.current;
expect(state).toBe("ready");
});

it("should have ready state if firebase anon", async () => {
// setup
const credential = await signInAnonymously(auth);

// test
const { result } = renderHook(() => useSignIn({ auth }));
const { state } = result.current;
expect(state).toBe("ready");

// teardown
await signOut(auth);
await deleteUser(credential.user);
});

it("should dispatch sign-in with email and password", async () => {
// setup
const email = generateEmail("dispatchemail");
await createUserWithEmailAndPassword(auth, email, password);

const { result } = renderHook(() => useSignIn({ auth }));
const { dispatch } = result.current;

const credential = await dispatch({ type: "classic", email, password });
expect(credential.user.email).toBe(email);

// teardown
await signOut(auth);
await deleteUser(credential.user);
});
});
130 changes: 130 additions & 0 deletions src/auth/useSignIn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright (c) 2024 Eray Erdin
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

import {
Auth,
FacebookAuthProvider,
GithubAuthProvider,
GoogleAuthProvider,
OAuthProvider,
TwitterAuthProvider,
UserCredential,
signInWithEmailAndPassword,
signInWithPopup,
} from "@firebase/auth";
import { useEffect, useState } from "react";
import { useUser } from ".";

type UseSignInParams = {
auth: Auth;
};

type UseSignInState = "ready" | "loading" | "authenticated";
type UseSignInDispatcher = (
params:
| {
type: "classic";
email: string;
password: string;
}
| {
type: "google";
provider: GoogleAuthProvider;
}
| {
type: "facebook";
provider: FacebookAuthProvider;
}
| {
type: "apple" | "microsoft" | "yahoo";
provider: OAuthProvider;
}
| {
type: "twitter";
provider: TwitterAuthProvider;
}
| {
type: "github";
provider: GithubAuthProvider;
},
) => Promise<UserCredential>;
type UseSignIn = {
state: UseSignInState;
dispatch: UseSignInDispatcher;
};

export const useSignIn = ({ auth }: UseSignInParams): UseSignIn => {
const user = useUser({ auth });
const [state, setState] = useState<UseSignInState>("ready");

useEffect(() => {
setState(user ? (user.isAnonymous ? "ready" : "authenticated") : "ready");
return () => setState("ready");
}, [user]);

const dispatch: UseSignInDispatcher = async (params) => {
setState("loading");
const { type } = params;

switch (type) {
case "classic": {
const { email, password } = params;
const credential = await signInWithEmailAndPassword(
auth,
email,
password,
);
setState("authenticated");
return credential;
}
case "google": {
const { provider } = params;
const credential = await signInWithPopup(auth, provider);
setState("authenticated");
setState("authenticated");
return credential;
}
case "facebook": {
const { provider } = params;
const credential = await signInWithPopup(auth, provider);
setState("authenticated");
setState("authenticated");
return credential;
}
case "apple": {
const { provider } = params;
const credential = await signInWithPopup(auth, provider);
setState("authenticated");
return credential;
}
case "twitter": {
const { provider } = params;
const credential = await signInWithPopup(auth, provider);
setState("authenticated");
return credential;
}
case "github": {
const { provider } = params;
const credential = await signInWithPopup(auth, provider);
setState("authenticated");
return credential;
}
case "microsoft": {
const { provider } = params;
const credential = await signInWithPopup(auth, provider);
setState("authenticated");
return credential;
}
case "yahoo": {
const { provider } = params;
const credential = await signInWithPopup(auth, provider);
setState("authenticated");
return credential;
}
}
};

return { state, dispatch };
};