diff --git a/README.md b/README.md index af5ea08..5b06129 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ async function someBrokerizeActions() { createWebSocket: (url, protocol) => new WebSocket(url, protocol), // basePath: 'https://api-preview.brokerize.com', // this is the default value // basePathCryptoService: 'https://crypto-service-api.com' // the optional external crypto service + // acceptLanguage: 'de', // optional `Accept-Language` header for localized responses (e.g. legal terms). + // // may also be a function `() => 'de'` that is evaluated on every request, + // // so runtime language changes are picked up without recreating the client. }) /* create a guest user. the result contains the user's tokens and be stored, e.g. in a cookie or session storage */ diff --git a/src/apiCtx.ts b/src/apiCtx.ts index 457b0ae..603925e 100644 --- a/src/apiCtx.ts +++ b/src/apiCtx.ts @@ -22,6 +22,50 @@ export interface BrokerizeConfig { * The AWS cognito configuration, if the application is supposed to be used with brokerize accounts. */ cognito?: CognitoConfig; + /** + * Optional value for the `Accept-Language` header sent with API requests. Use this to request + * localized backend responses (e.g. legal terms) in the language selected in your application, + * overriding the language the browser would send by default. + * + * Accepts a static value (e.g. `"de"`, `"en"`, `"de-DE"`) or a function that returns the current + * language. The function is evaluated on every request, so runtime language changes are picked up + * without recreating the client. Return `undefined`/`null` (or provide no value) to omit the + * header and let the runtime/browser default apply. + */ + acceptLanguage?: + | string + | (() => string | null | undefined | Promise); +} + +/** + * Resolves the configured `Accept-Language` value (static or via a getter function) to a string, + * or `undefined` if none is configured / the getter yields an empty value. + */ +export async function resolveAcceptLanguage( + cfg: BrokerizeConfig, +): Promise { + const { acceptLanguage } = cfg; + const value = + typeof acceptLanguage === "function" + ? await acceptLanguage() + : acceptLanguage; + return value || undefined; +} + +/** + * Adds the configured `Accept-Language` header to the given headers object (mutating and returning + * it) if a language is configured. No-op otherwise. Used to apply the configured language uniformly + * to authorized and unauthenticated requests. + */ +export async function withAcceptLanguage( + cfg: BrokerizeConfig, + headers: Record, +): Promise> { + const acceptLanguage = await resolveAcceptLanguage(cfg); + if (acceptLanguage) { + headers["Accept-Language"] = acceptLanguage; + } + return headers; } export type AuthContextConfiguration = @@ -102,10 +146,10 @@ export function createAuth({ } const response = await fetch(cfg.basePath + "/user/token", { method: "POST", - headers: { + headers: await withAcceptLanguage(cfg, { "x-brkrz-client-id": cfg.clientId, "Content-Type": "application/x-www-form-urlencoded", - }, + }), // XXX some runtimes do not have URLSearchParams, so just produce the body in the old-fashioned way body: `grant_type=refresh_token&refresh_token=${encodeURIComponent( guestAuthCfg.tokens.response.refreshToken, diff --git a/src/authorizedApiContext.ts b/src/authorizedApiContext.ts index eb27e22..b9b49df 100644 --- a/src/authorizedApiContext.ts +++ b/src/authorizedApiContext.ts @@ -1,5 +1,10 @@ import { Subject } from "rxjs"; -import { Auth, BrokerizeConfig, createConfiguration } from "./apiCtx"; +import { + Auth, + BrokerizeConfig, + createConfiguration, + withAcceptLanguage, +} from "./apiCtx"; import { BrokerizeError } from "./errors"; import { createPollingSubscription } from "./pollingSubscription"; import * as openApiClient from "./swagger"; @@ -171,13 +176,14 @@ export class AuthorizedApiContext { throw new Error("AuthorizedApiContext is destroyed"); } const tok = await this._auth.getToken(); + const headers = await withAcceptLanguage(this._cfg, { + "x-brkrz-client-id": this._cfg.clientId, + Authorization: "Bearer " + tok.idToken, + "Content-Type": "application/json", + }); return { signal: this._abortController.signal, - headers: { - "x-brkrz-client-id": this._cfg.clientId, - Authorization: "Bearer " + tok.idToken, - "Content-Type": "application/json", - }, + headers, }; } async getBrokers() { diff --git a/src/index.ts b/src/index.ts index 29c35f2..00c61a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { GuestAuthContextConfiguration, RegisteredUserAuthContextConfiguration, TokenSet, + withAcceptLanguage, } from "./apiCtx"; import { AuthorizedApiContext, @@ -108,10 +109,10 @@ export class Brokerize { ): Promise { const response = await fetch(this._cfg.basePath + "/user/token", { method: "POST", - headers: { + headers: await withAcceptLanguage(this._cfg, { "x-brkrz-client-id": this._cfg.clientId, "Content-Type": "application/x-www-form-urlencoded", - }, + }), // XXX some runtimes do not have URLSearchParams, so just produce the body in the old-fashioned way body: `grant_type=refresh_token&refresh_token=${encodeURIComponent( refreshToken, @@ -159,10 +160,10 @@ export class Brokerize { async createGuestUser(): Promise { const updatedAt = Date.now(); const user = await this._userApi.createGuestUser({ - headers: { + headers: await withAcceptLanguage(this._cfg, { "x-brkrz-client-id": this._cfg.clientId, "Content-Type": "application/json", - }, + }), }); return { type: "guest", @@ -218,16 +219,16 @@ export class Brokerize { }); } - checkRecoveryPhrase(recoveryPhrase: string) { + async checkRecoveryPhrase(recoveryPhrase: string) { return this._userApi.checkRecoveryPhrase( { obtainTokenByRecoveryPhraseParams: { recoveryPhrase }, }, { - headers: { + headers: await withAcceptLanguage(this._cfg, { "x-brkrz-client-id": this._cfg.clientId, "Content-Type": "application/json", - }, + }), }, ); } @@ -240,10 +241,10 @@ export class Brokerize { obtainTokenByRecoveryPhraseParams: { recoveryPhrase }, }, { - headers: { + headers: await withAcceptLanguage(this._cfg, { "x-brkrz-client-id": this._cfg.clientId, "Content-Type": "application/json", - }, + }), }, );