Skip to content

feat: Add optional Accept-Language header configuration #1148

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

Closed
wants to merge 3 commits into from
Closed
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
55 changes: 55 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Examples using react-native-auth0

- [SDK Configuration](#sdk-configuration)
- [Setting the Accept-Language Header](#setting-the-accept-language-header)
- [Authentication API](#authentication-api)
- [Login with Password Realm Grant](#login-with-password-realm-grant)
- [Get user information using user's access_token](#get-user-information-using-users-access_token)
Expand All @@ -21,6 +23,59 @@
- [iOS](#ios)
- [Expo](#expo)

## SDK Configuration

This section covers examples related to configuring the behavior of the Auth0 SDK itself.

### Setting the Accept-Language Header

You can configure the SDK to automatically send the `Accept-Language` HTTP header with all outgoing requests. This is useful if you want to signal your preferred language(s) to Auth0, which might influence the language used in error messages or other localized content returned by Auth0 APIs.


Provide the `acceptLanguage` option during initialization. The value should be a string conforming to the standard `Accept-Language` header format (e.g., `en-US`, `fr-CA,fr;q=0.9`).

**Using the `Auth0` class:**

```js
import Auth0 from 'react-native-auth0';

const auth0 = new Auth0({
domain: 'YOUR_AUTH0_DOMAIN',
clientId: 'YOUR_AUTH0_CLIENT_ID',
// Set preferred language(s)
acceptLanguage: 'es-ES,es;q=0.9'
});

// All subsequent API calls made using this auth0 instance
// (e.g., auth0.auth.passwordlessWithSMS())
// will include the 'Accept-Language: es-ES,es;q=0.9' header.
```

**Using the Auth0Provider hook:**

```js
import { Auth0Provider } from 'react-native-auth0';

const App = () => {
return (
<Auth0Provider
domain="YOUR_AUTH0_DOMAIN"
clientId="YOUR_AUTH0_CLIENT_ID"
// Set preferred language(s)
acceptLanguage="de-DE"
>
{/* YOUR APP */}
</Auth0Provider>
);
};

export default App;

// All subsequent API calls made via the useAuth0() hook derived
// from this provider will include the 'Accept-Language: de-DE' header.

```

## Authentication API

Unlike web authentication, we do not provide a hook for integrating with the Authentication API.
Expand Down
17 changes: 10 additions & 7 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ function convertTimestampInCredentials(
return { ...credentials, expiresAt };
}

interface AuthOptions {
baseUrl: string;
clientId: string;
telemetry?: Telemetry;
token?: string;
timeout?: number;
acceptLanguage?: string; // Added acceptLanguage property
}

/**
* Class for interfacing with the Auth0 Authentication API endpoints.
*
Expand All @@ -76,13 +85,7 @@ class Auth {
/**
* @ignore
*/
constructor(options: {
baseUrl: string;
clientId: string;
telemetry?: Telemetry;
token?: string;
timeout?: number;
}) {
constructor(options: AuthOptions) {
this.client = new Client(options);
this.domain = this.client.domain;

Expand Down
27 changes: 16 additions & 11 deletions src/auth0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,22 @@ import addDefaultLocalAuthOptions from './utils/addDefaultLocalAuthOptions';
/**
* Auth0 for React Native client
*/

interface Auth0ConstructorOptions {
domain: string;
clientId: string;
telemetry?: Telemetry;
token?: string;
timeout?: number;
localAuthenticationOptions?: LocalAuthenticationOptions;
acceptLanguage?: string; // <-- Changed: Accept language option
}

class Auth0 {
public auth: Auth;
public webAuth: WebAuth;
public credentialsManager: CredentialsManager;
private options;
private options: Auth0ConstructorOptions;

/**
* Creates an instance of Auth0.
Expand All @@ -24,20 +35,14 @@ class Auth0 {
* @param {String} options.token Token to be used for Management APIs
* @param {String} options.timeout Timeout to be set for requests.
* @param {LocalAuthenticationOptions} options.localAuthenticationOptions The options for configuring the display of local authentication prompt, authentication level (Android only) and evaluation policy (iOS only).
* @param {string} [options.acceptLanguage] Optional language tag (e.g., "en-US", "fr") to be sent in the Accept-Language header for all requests.
*/
constructor(options: {
domain: string;
clientId: string;
telemetry?: Telemetry;
token?: string;
timeout?: number;
localAuthenticationOptions?: LocalAuthenticationOptions;
}) {
const { domain, clientId, ...extras } = options;
constructor(options: Auth0ConstructorOptions) {
const { domain, clientId, acceptLanguage, ...extras } = options;
const localAuthenticationOptions = options.localAuthenticationOptions
? addDefaultLocalAuthOptions(options.localAuthenticationOptions)
: undefined;
this.auth = new Auth({ baseUrl: domain, clientId, ...extras });
this.auth = new Auth({ baseUrl: domain, clientId, acceptLanguage, ...extras });
this.webAuth = new WebAuth(this.auth, localAuthenticationOptions);
this.credentialsManager = new CredentialsManager(
domain,
Expand Down
24 changes: 16 additions & 8 deletions src/hooks/auth0-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,21 @@ const finalizeScopeParam = (inputScopes?: string) => {
return Array.from(scopeSet).join(' ');
};

interface Auth0ProviderProps {
domain: string;
clientId: string;
localAuthenticationOptions?: LocalAuthenticationOptions;
timeout?: number;
acceptLanguage?: string;
}

/**
* Provides the Auth0Context to its child components.
* @param {String} domain Your Auth0 domain
* @param {String} clientId Your Auth0 client ID
* @param {LocalAuthenticationOptions} localAuthenticationOptions The local auth options
* @param {number} timeout - Optional timeout in milliseconds for authentication requests.
* @param {string} [acceptLanguage] Optional language tag (e.g., "en-US", "fr") to be sent in the Accept-Language header for all requests.
* @param {React.ReactNode} children - The child components to render within the provider.
*
* @example
Expand All @@ -76,16 +85,12 @@ const Auth0Provider = ({
clientId,
localAuthenticationOptions,
timeout,
acceptLanguage,
children,
}: PropsWithChildren<{
domain: string;
clientId: string;
localAuthenticationOptions?: LocalAuthenticationOptions;
timeout?: number;
}>) => {
}: PropsWithChildren<Auth0ProviderProps>) => {
const client = useMemo(
() => new Auth0({ domain, clientId, localAuthenticationOptions, timeout }),
[domain, clientId, localAuthenticationOptions, timeout]
() => new Auth0({ domain, clientId, localAuthenticationOptions, timeout, acceptLanguage }),
[domain, clientId, localAuthenticationOptions, timeout, acceptLanguage]
);
const [state, dispatch] = useReducer(reducer, initialState);

Expand Down Expand Up @@ -446,6 +451,9 @@ Auth0Provider.propTypes = {
domain: PropTypes.string.isRequired,
clientId: PropTypes.string.isRequired,
children: PropTypes.element.isRequired,
acceptLanguage: PropTypes.string,
localAuthenticationOptions: PropTypes.object,
timeout: PropTypes.number
};

export default Auth0Provider;
15 changes: 9 additions & 6 deletions src/management/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ const attributes = [
'family_name',
];

interface UsersClientOptions {
baseUrl: string;
telemetry?: Telemetry;
token?: string;
timeout?: number;
acceptLanguage?: string;
}

/**
* Auth0 Management API User endpoints
*
Expand All @@ -42,12 +50,7 @@ class Users {
/**
* @ignore
*/
constructor(options: {
baseUrl: string;
telemetry?: Telemetry;
token?: string;
timeout?: number;
}) {
constructor(options: UsersClientOptions) {
this.client = new Client(options);
if (!options.token) {
throw new Error('Missing token in parameters');
Expand Down
74 changes: 74 additions & 0 deletions src/networking/__tests__/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ describe('client', () => {
it('should fail with no domain', () => {
expect(() => new Client()).toThrowErrorMatchingSnapshot();
});

it('should store acceptLanguage option if provided', () => {
const lang = 'fr-CA';
const client = new Client({ baseUrl, acceptLanguage: lang });
expect(client.acceptLanguage).toEqual(lang);
});
});

describe('requests', () => {
Expand Down Expand Up @@ -219,6 +225,74 @@ describe('client', () => {
});
});

describe('Accept-Language Header', () => {
const path = '/test-endpoint';
const mockResponse = { status: 200, body: { success: true } };
const defaultClient = new Client({
baseUrl,
telemetry: {name: 'react-native-auth0', version: '1.0.0'},
token: 'a.bearer.token',
});
beforeEach(fetchMock.restore);
it('should NOT set Accept-Language header if option is not provided', async () => {
fetchMock.getOnce(`https://${domain}${path}`, mockResponse);
await defaultClient.get(path);

const requestOptions = fetchMock.lastOptions();
const headers = requestOptions.headers;
expect(headers.get('Accept-Language')).toBeNull();
});

it('should set Accept-Language header if option IS provided', async () => {
const lang = 'es-ES,es;q=0.9';
const clientWithLang = new Client({
baseUrl,
telemetry: { name: 'react-native-auth0', version: '1.0.0' },
token: 'a.bearer.token',
acceptLanguage: lang, // Provide the option
});

fetchMock.getOnce(`https://${domain}${path}`, mockResponse);
await clientWithLang.get(path);

const requestOptions = fetchMock.lastOptions();
const headers = requestOptions.headers;
expect(headers.get('Accept-Language')).toEqual(lang);
});

it('should set Accept-Language header for POST requests if option is provided', async () => {
const lang = 'fr';
const clientWithLang = new Client({
baseUrl,
acceptLanguage: lang,
});
const body = { data: 'test' };

fetchMock.postOnce(`https://${domain}${path}`, mockResponse);
await clientWithLang.post(path, body);

const requestOptions = fetchMock.lastOptions();
const headers = requestOptions.headers;
expect(headers.get('Accept-Language')).toEqual(lang);
});

it('should set Accept-Language header for PATCH requests if option is provided', async () => {
const lang = 'de-DE';
const clientWithLang = new Client({
baseUrl,
acceptLanguage: lang,
});
const body = { data: 'update' };

fetchMock.patchOnce(`https://${domain}${path}`, mockResponse);
await clientWithLang.patch(path, body);

const requestOptions = fetchMock.lastOptions();
const headers = requestOptions.headers;
expect(headers.get('Accept-Language')).toEqual(lang);
});
});

describe('url', () => {
const client = new Client({
baseUrl,
Expand Down
9 changes: 9 additions & 0 deletions src/networking/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,27 @@ class Client {
public domain: string;
private bearer?: string;
private timeout: number;
private acceptLanguage?: string;

constructor(options: {
baseUrl: string;
telemetry?: Telemetry;
token?: string;
timeout?: number;
acceptLanguage?: string;
}) {
const {
baseUrl,
telemetry = {},
token,
timeout = 10000,
acceptLanguage,
}: {
baseUrl: string;
telemetry?: Telemetry;
token?: string;
timeout?: number;
acceptLanguage?: string;
} = options;
if (!baseUrl) {
throw new Error('Missing Auth0 domain');
Expand All @@ -51,6 +55,7 @@ class Client {
}

this.timeout = timeout;
this.acceptLanguage = acceptLanguage;
}

post<TData = unknown, TBody = unknown>(path: string, body: TBody) {
Expand Down Expand Up @@ -89,6 +94,10 @@ class Client {
headers.set('Content-Type', 'application/json');
headers.set('Auth0-Client', this._encodedTelemetry());

if (this.acceptLanguage) {
headers.set('Accept-Language', this.acceptLanguage);
}

const options: RequestInit = {
method,
headers,
Expand Down