Skip to content
Open
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 apps/docs/content/docs/advanced/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ function LoginButton() {
const handleLogin = async () => {
const response = await login({ scope: 'email' });

if (response.status !== 'connected') return;

// Narrow to scope flow — authResponse contains accessToken, not code
if (!('accessToken' in response.authResponse)) return;

// Send the token to your server for validation and exchange
await fetch('/api/auth/facebook', {
method: 'POST',
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/components/error-boundary.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function App() {
}}
>
<FacebookProvider appId="YOUR_APP_ID">
<Login onSuccess={handleSuccess}>Login with Facebook</Login>
<Login onSuccess={handleSuccess} >Login with Facebook</Login>
</FacebookProvider>
</FacebookErrorBoundary>
);
Expand Down
91 changes: 75 additions & 16 deletions apps/docs/content/docs/components/login.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,25 @@ import { Login } from 'react-facebook';

## Props

| Prop | Type | Default | Description |
| ------------------ | ---------------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------- |
| `children` | `ReactNode \| ((props: LoginRenderProps) => ReactElement)` | `undefined` | Button content, or a render function receiving `{ onClick, loading, isDisabled }`. |
| `onSuccess` | `(response: LoginResponse) => void` | `undefined` | Called after a successful login with the login response containing `authResponse`. |
| `onError` | `(error: Error) => void` | `undefined` | Called when the login fails or the user cancels. |
| `onProfileSuccess` | `(profile: Record<string, unknown>) => void` | `undefined` | Called with the user profile when `fields` are provided and the profile is fetched. |
| `scope` | `string \| string[]` | `['public_profile', 'email']` | Permissions to request. Accepts a comma-separated string or an array. |
| `fields` | `string[]` | `[]` | Profile fields to fetch after login (e.g. `['name', 'email', 'picture']`). |
| `as` | `ElementType \| ComponentType` | `'button'` | The HTML element or React component to render. |
| `disabled` | `boolean` | `false` | Disables the login button. |
| `returnScopes` | `boolean` | `undefined` | When `true`, the response includes the scopes the user actually granted. |
| `authType` | `string[]` | `undefined` | Auth type array (e.g. `['rerequest']`). |
| `rerequest` | `boolean` | `undefined` | Adds `'rerequest'` to `authType`, prompting for previously declined permissions. |
| `reauthorize` | `boolean` | `undefined` | Adds `'reauthenticate'` to `authType`, forcing re-authentication. |
| `className` | `string` | `undefined` | CSS class name applied to the rendered element. |
| `style` | `CSSProperties` | `undefined` | Inline styles applied to the rendered element. |
`LoginProps` is a **discriminated union** — you must provide either `configId` or `scope`, not both. Passing both is a TypeScript error.

| Prop | Type | Default | Description |
| ------------------ | ---------------------------------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `children` | `ReactNode \| ((props: LoginRenderProps) => ReactElement)` | `undefined` | Button content, or a render function receiving `{ onClick, loading, isDisabled }`. |
| `onSuccess` | `(response: LoginResponse) => void` | `undefined` | Called after a successful login with the login response containing `authResponse`. |
| `onError` | `(error: Error) => void` | `undefined` | Called when the login fails or the user cancels. |
| `onProfileSuccess` | `(profile: Record<string, unknown>) => void` | `undefined` | Called with the user profile when `fields` are provided and the profile is fetched. |
| `configId` | `string` | `undefined` | Facebook Business Login configuration ID. When set, triggers the BISU code flow (`response_type: 'code'`).<br/><br/> <b>Note:</b> Mutually exclusive with `scope`. |
| `scope` | `string \| string[]` | `['public_profile', 'email']` | Permissions to request. Accepts a comma-separated string or an array.<br/><br/> <b>Note:</b> Mutually exclusive with `configId`. |
| `fields` | `string[]` | `[]` | Profile fields to fetch after login (e.g. `['name', 'email', 'picture']`). |
| `as` | `ElementType \| ComponentType` | `'button'` | The HTML element or React component to render. |
| `disabled` | `boolean` | `false` | Disables the login button. |
| `returnScopes` | `boolean` | `undefined` | When `true`, the response includes the scopes the user actually granted. Applies to the `scope` flow only. |
| `authType` | `string[]` | `undefined` | Auth type array (e.g. `['rerequest']`). |
| `rerequest` | `boolean` | `undefined` | Adds `'rerequest'` to `authType`, prompting for previously declined permissions. |
| `reauthorize` | `boolean` | `undefined` | Adds `'reauthenticate'` to `authType`, forcing re-authentication. |
| `className` | `string` | `undefined` | CSS class name applied to the rendered element. |
| `style` | `CSSProperties` | `undefined` | Inline styles applied to the rendered element. |

Any additional props are spread onto the rendered element.

Expand Down Expand Up @@ -117,3 +120,59 @@ When you provide `fields`, the component automatically fetches the user profile
Sign in with Facebook
</Login>
```

### Facebook Login for Business (configId)

<Callout type="info">
Facebook Login for Business uses a configuration ID created in the [Facebook App Dashboard](https://developers.facebook.com/docs/facebook-login/facebook-login-for-business).
Instead of a client-side access token the SDK returns a short-lived authorization **code** that must be exchanged
server-side for a BISU token. See the [Facebook Login for Business docs](https://developers.facebook.com/docs/facebook-login/facebook-login-for-business)
for the full server-side exchange flow.
</Callout>

Pass `configId` instead of `scope`. The `onSuccess` callback receives `authResponse.code` — there is no `accessToken` in this flow.

**With a default button:**

```tsx
<Login
configId="YOUR_CONFIG_ID"
onSuccess={(response) => {
if (response.status === 'connected' && 'code' in response.authResponse) {
// Exchange this code on your server
fetch('/api/auth/facebook/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: response.authResponse.code }),
});
}
}}
onError={(error) => console.error('Login failed:', error)}
>
Continue with Facebook
</Login>
```

**With the render props pattern:**

```tsx
<Login
configId="YOUR_CONFIG_ID"
onSuccess={(response) => {
if (response.status === 'connected' && 'code' in response.authResponse) {
fetch('/api/auth/facebook/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: response.authResponse.code }),
});
}
}}
onError={(error) => console.error('Login failed:', error)}
>
{({ onClick, loading, isDisabled }) => (
<button onClick={onClick} disabled={isDisabled}>
{loading ? 'Connecting...' : 'Sign in with Facebook'}
</button>
)}
</Login>
```
104 changes: 88 additions & 16 deletions apps/docs/content/docs/hooks/use-login.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,50 @@ The hook returns an object with the following properties:

### LoginOptions

The `login` function accepts the following options:
`LoginOptions` is a **discriminated union** — pass either `configId` or `scope`, not both.

| Property | Type | Default | Description |
| -------------- | ---------- | ----------- | ------------------------------------------------------------------------------- |
| `scope` | `string` | `undefined` | Comma-separated list of permissions to request (e.g. `'email,public_profile'`). |
| `returnScopes` | `boolean` | `undefined` | When `true`, the response includes the scopes that were granted. |
| `authType` | `string[]` | `undefined` | Array of auth types to include in the request. |
| `rerequest` | `boolean` | `undefined` | When `true`, asks the user again for previously declined permissions. |
| `reauthorize` | `boolean` | `undefined` | When `true`, forces re-authentication of the user. |
**Shared options** (apply to both flows):

| Property | Type | Default | Description |
| ------------- | ---------- | ----------- | --------------------------------------------------------------------- |
| `returnScopes` | `boolean` | `undefined` | When `true`, the response includes the scopes that were granted. Applies to the `scope` flow only. |
| `authType` | `string[]` | `undefined` | Array of auth types to include in the request. |
| `rerequest` | `boolean` | `undefined` | When `true`, asks the user again for previously declined permissions. |
| `reauthorize` | `boolean` | `undefined` | When `true`, forces re-authentication of the user. |

**Scope flow** — pass `scope` to request permissions directly:

| Property | Type | Default | Description |
| -------- | -------- | ----------- | -------------------------------------------------------------------------------- |
| `scope` | `string` | `undefined` | Comma-separated list of permissions to request (e.g. `'email,public_profile'`).<br/><br/> <b>Note:</b> Mutually exclusive with `configId`. |

**Business Login flow** — pass `configId` to use a server-defined configuration:

| Property | Type | Default | Description |
| ---------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------- |
| `configId` | `string` | `undefined` | Facebook Business Login configuration ID. Triggers the BISU code flow (`response_type: 'code'`).<br/><br/> <b>Note:</b> Mutually exclusive with `scope`. |

### LoginResponse

When the status is `'connected'`, the response includes an `authResponse` object with:
When `status` is `'connected'`, the response includes an `authResponse` whose shape depends on the login flow used.

**Scope flow** (`authResponse` when `scope` was passed):

| Property | Type | Description |
| ------------- | -------- | ------------------------------- |
| `userID` | `string` | The Facebook user ID. |
| `accessToken` | `string` | The access token for API calls. |
| Property | Type | Description |
| ------------- | -------- | ------------------------------------ |
| `userID` | `string` | The Facebook user ID. |
| `accessToken` | `string` | The access token for API calls. |
| `expiresIn` | `number` | Seconds until the token expires. |

**Business Login flow** (`authResponse` when `configId` was passed):

| Property | Type | Description |
| ----------- | -------- | ----------------------------------------------------------------------------------------------- |
| `code` | `string` | Short-lived authorization code to exchange server-side for a BISU token. |
| `userID` | `null` | Always `null` — no user-scoped ID is returned in the `configId` flow. |
| `expiresIn` | `number` | `NaN` — expiration is defined by the BISU configuration, not the SDK response. |

Narrow `authResponse` with `'accessToken' in response.authResponse` before accessing flow-specific fields.

## Usage

Expand Down Expand Up @@ -99,7 +125,9 @@ function LoginWithErrorHandling() {

try {
const response = await login({ scope: 'email,public_profile' });
console.log('Logged in as:', response.authResponse?.userID);
if (response.status === 'connected' && 'accessToken' in response.authResponse) {
console.log('Logged in as:', response.authResponse.userID);
}
} catch (err) {
const message = err instanceof Error ? err.message : 'An unexpected error occurred';
setLoginError(message);
Expand Down Expand Up @@ -178,7 +206,7 @@ function LoginAndProfile() {

## Forward Token to Server

After login, send the `accessToken` to your backend API for server-side verification or session creation.
After a scope-based login, send the `accessToken` to your backend for server-side verification or session creation. Narrow `authResponse` with `'accessToken' in response.authResponse` to confirm this is the scope flow before accessing `accessToken` and `userID`.

```tsx
import { useLogin } from 'react-facebook';
Expand All @@ -191,10 +219,15 @@ function LoginWithBackend() {
try {
const response = await login({ scope: 'email,public_profile' });

if (response.status !== 'connected' || !response.authResponse) {
if (response.status !== 'connected') {
throw new Error('Login did not complete');
}

// Narrow to scope flow — authResponse contains accessToken, not code
if (!('accessToken' in response.authResponse)) {
throw new Error('Unexpected response type');
}

const { accessToken, userID } = response.authResponse;

// Send the access token to your backend for verification
Expand Down Expand Up @@ -262,3 +295,42 @@ function ConditionalLogout() {
);
}
```

## Facebook Login for Business (configId)

Pass `configId` instead of `scope` to use the [Facebook Login for Business](https://developers.facebook.com/docs/facebook-login/facebook-login-for-business) flow. The SDK returns a short-lived authorization **code** in `authResponse.code` — there is no `accessToken`. Exchange this code on your server for a BISU token.

**Basic usage:**

```tsx
import { useLogin } from 'react-facebook';

function BusinessLoginButton() {
const { login, loading } = useLogin();

const handleLogin = async () => {
try {
const response = await login({ configId: 'YOUR_CONFIG_ID' });

if (response.status !== 'connected' || !('code' in response.authResponse)) {
throw new Error('Login did not complete');
}

// Exchange this code server-side for a BISU token
await fetch('/api/auth/facebook/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: response.authResponse.code }),
});
} catch (err) {
console.error('Login failed:', err);
}
};

return (
<button onClick={handleLogin} disabled={loading}>
{loading ? 'Connecting...' : 'Continue with Facebook'}
</button>
);
}
```
22 changes: 18 additions & 4 deletions apps/docs/content/docs/migration/facebook-login-setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ function App() {
scope={['public_profile', 'email']}
fields={['name', 'email', 'picture']}
onSuccess={(response) => {
console.log('Auth token:', response.authResponse.accessToken);
if (response.status === 'connected' && 'accessToken' in response.authResponse) {
console.log('Auth token:', response.authResponse.accessToken);
}
}}
onProfileSuccess={(profile) => {
console.log('User:', profile.name, profile.email);
Expand Down Expand Up @@ -58,6 +60,8 @@ function LoginButton() {
const handleLogin = async () => {
try {
const response = await login({ scope: 'email,public_profile' });
// Narrow to the scope flow before accessing accessToken / userID
if (response.status !== 'connected' || !('accessToken' in response.authResponse)) return;
// Send token to your backend
await fetch('/api/auth/facebook', {
method: 'POST',
Expand Down Expand Up @@ -189,11 +193,21 @@ import { FacebookProvider, FacebookErrorBoundary, Login } from 'react-facebook';
Every component and hook is fully typed. No separate `@types/` package needed.

```tsx
import type { LoginResponse, AuthResponse } from 'react-facebook';
import type { LoginResponse } from 'react-facebook';

function handleSuccess(response: LoginResponse) {
const token: string = response.authResponse.accessToken;
const userId: string = response.authResponse.userID;
if (response.status !== 'connected') return;

// Scope flow: authResponse contains accessToken and userID
if ('accessToken' in response.authResponse) {
const token: string = response.authResponse.accessToken;
const userId: string = response.authResponse.userID;
}

// Business Login (configId) flow: authResponse contains code instead
if ('code' in response.authResponse) {
const code: string = response.authResponse.code;
}
}
```

Expand Down
Loading