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
8 changes: 8 additions & 0 deletions .changeset/calm-clocks-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@clerk/clerk-js': patch
'@clerk/shared': patch
'@clerk/backend': patch
'@clerk/react': patch
---

Capture authentication timezones so Clerk emails can display timestamps in a stored user timezone.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
| <a id="primaryweb3wallet"></a> `primaryWeb3Wallet` | <code>null \| [Web3WalletResource](/docs/reference/types/web3-wallet)</code> | The user's primary Web3 wallet. |
| <a id="primaryweb3walletid"></a> `primaryWeb3WalletId` | <code>null \| string</code> | The ID of the user's primary Web3 wallet. |
| <a id="publicmetadata"></a> `publicMetadata` | [UserPublicMetadata](/docs/reference/types/metadata#userpublicmetadata) | Metadata that can be read from the Frontend API and Backend API and can be set only from the Backend API. |
| <a id="timezone"></a> `timezone` | <code>null \| string</code> | The user's timezone. |
| <a id="totpenabled"></a> `totpEnabled` | `boolean` | Indicates whether the user has enabled TOTP. |
| <a id="twofactorenabled"></a> `twoFactorEnabled` | `boolean` | Indicates whether the user has enabled two-factor authentication. |
| <a id="unsafemetadata"></a> `unsafeMetadata` | [UserUnsafeMetadata](/docs/reference/types/metadata#userunsafemetadata) | Metadata that can be read and set from the Frontend API. It's considered unsafe because it can be modified from the frontend. There is also an `unsafeMetadata` attribute in the [`SignUp`](/docs/reference/objects/sign-up-future) object. The value of that field will be automatically copied to the user's unsafe metadata once the sign-up is complete. |
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/api/endpoints/UserApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ export type CreateUserParams = {
lastName?: string;
/** The locale of the user in BCP-47 format (e.g., `'en-US'`, `'fr-FR'`). */
locale?: string;
/** The timezone of the user. */
timezone?: string;
/** When set to `true`, all password checks are skipped. It is recommended to use this method only when migrating plaintext passwords to Clerk. Upon migration the user base should be prompted to pick stronger password. */
skipPasswordChecks?: boolean;
/** When set to `true`, password is not required anymore when creating the user and can be omitted. This is useful when you are trying to create a user that doesn't have a password, in an instance that is using passwords. **You cannot use this flag if password is the only way for a user to sign into your instance.** */
Expand Down Expand Up @@ -324,6 +326,8 @@ export type UpdateUserParams = {
legalAcceptedAt?: Date;
/** The locale of the user in BCP-47 format (e.g., `'en-US'`). */
locale?: string;
/** The timezone of the user. */
timezone?: string;
/** If `true`, the user can delete themselves with the Frontend API. */
deleteSelfEnabled?: boolean;
/** If `true`, the user can create Organizations with the Frontend API. */
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,10 @@ export interface UserJSON extends ClerkResourceJSON {
* The locale of the user in BCP-47 format.
*/
locale: string | null;
/**
* The timezone of the user.
*/
timezone: string | null;
}

export interface VerificationJSON extends ClerkResourceJSON {
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/api/resources/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export class User {

/** The locale of the user in BCP-47 format. */
readonly locale: string | null,
/** The timezone of the user. */
readonly timezone: string | null = null,
) {}

static fromJSON(data: UserJSON): User {
Expand Down Expand Up @@ -120,6 +122,7 @@ export class User {
data.delete_self_enabled,
data.legal_accepted_at,
data.locale,
data.timezone,
);
res._raw = data;
return res;
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"files": [
{ "path": "./dist/clerk.js", "maxSize": "554KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "81KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "124KB" },
{ "path": "./dist/clerk.no-rhc.js", "maxSize": "322KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "124.5KB" },
{ "path": "./dist/clerk.no-rhc.js", "maxSize": "322.25KB" },
{ "path": "./dist/clerk.native.js", "maxSize": "80KB" },
{ "path": "./dist/vendors*.js", "maxSize": "7KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "36KB" },
Expand Down
19 changes: 19 additions & 0 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import {
import { _authenticateWithTransport } from '../../utils/authenticateWithTransport';
import { CaptchaChallenge } from '../../utils/captcha/CaptchaChallenge';
import { runAsyncResourceTask } from '../../utils/runAsyncResourceTask';
import { getBrowserTimezone } from '../../utils/timezone';
import { loadZxcvbn } from '../../utils/zxcvbn';
import {
clerkInvalidFAPIResponse,
Expand Down Expand Up @@ -127,6 +128,7 @@ export class SignIn extends BaseResource implements SignInResource {
userData: UserData = new UserData(null);
clientTrustState?: ClientTrustState;
protectCheck: ProtectCheckResource | null = null;
timezone: string | null = null;

/**
* The current status of the sign-in process.
Expand Down Expand Up @@ -200,6 +202,13 @@ export class SignIn extends BaseResource implements SignInResource {
body.locale = browserLocale;
}

if (body.timezone === undefined) {
const browserTimezone = getBrowserTimezone();
if (browserTimezone) {
body.timezone = browserTimezone;
}
}

if (
this.shouldRequireCaptcha(params) &&
!__BUILD_DISABLE_RHC__ &&
Expand Down Expand Up @@ -697,6 +706,7 @@ export class SignIn extends BaseResource implements SignInResource {
uiHints: data.protect_check.ui_hints,
}
: null;
this.timezone = data.timezone ?? null;
}

eventBus.emit('resource:update', { resource: this });
Expand Down Expand Up @@ -758,6 +768,7 @@ export class SignIn extends BaseResource implements SignInResource {
identifier: this.identifier,
created_session_id: this.createdSessionId,
user_data: this.userData.__internal_toSnapshot(),
timezone: this.timezone,
protect_check: this.protectCheck
? {
status: this.protectCheck.status,
Expand Down Expand Up @@ -851,6 +862,10 @@ class SignInFuture implements SignInFutureResource {
return this.#resource.identifier;
}

get timezone() {
return this.#resource.timezone;
}

get createdSessionId() {
return this.#resource.createdSessionId;
}
Expand Down Expand Up @@ -1076,13 +1091,15 @@ class SignInFuture implements SignInFutureResource {

private async _create(params: SignInFutureCreateParams): Promise<void> {
const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken(params);
const timezone = params.timezone ?? getBrowserTimezone();

const body: Record<string, unknown> = {
...params,
captchaToken,
captchaWidgetType,
captchaError,
locale: getBrowserLocale() || undefined,
...(timezone !== null ? { timezone } : {}),
Comment thread
Ephem marked this conversation as resolved.
};

await this.#resource.__internal_basePost({
Expand All @@ -1107,12 +1124,14 @@ class SignInFuture implements SignInFutureResource {
const identifier = params.identifier || params.emailAddress || params.phoneNumber;
const previousIdentifier = this.#resource.identifier;
const locale = getBrowserLocale();
const timezone = params.timezone ?? this.#resource.timezone ?? getBrowserTimezone();
await this.#resource.__internal_basePost({
path: this.#resource.pathRoot,
body: {
identifier: identifier || previousIdentifier,
password: params.password,
...(locale ? { locale } : {}),
...(timezone !== null ? { timezone } : {}),
},
});
});
Expand Down
33 changes: 29 additions & 4 deletions packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { _authenticateWithTransport } from '../../utils/authenticateWithTranspor
import { CaptchaChallenge } from '../../utils/captcha/CaptchaChallenge';
import { normalizeUnsafeMetadata } from '../../utils/resourceParams';
import { runAsyncResourceTask } from '../../utils/runAsyncResourceTask';
import { getBrowserTimezone } from '../../utils/timezone';
import { loadZxcvbn } from '../../utils/zxcvbn';
import {
clerkInvalidFAPIResponse,
Expand Down Expand Up @@ -101,6 +102,7 @@ export class SignUp extends BaseResource implements SignUpResource {
abandonAt: number | null = null;
legalAcceptedAt: number | null = null;
locale: string | null = null;
timezone: string | null = null;

/**
* The current status of the sign-up process.
Expand Down Expand Up @@ -168,6 +170,13 @@ export class SignUp extends BaseResource implements SignUpResource {
}
}

if (finalParams.timezone === undefined) {
const browserTimezone = getBrowserTimezone();
if (browserTimezone) {
finalParams.timezone = browserTimezone;
}
}

if (!__BUILD_DISABLE_RHC__ && !this.clientBypass() && !this.shouldBypassCaptchaForAttempt(params)) {
const captchaChallenge = new CaptchaChallenge(SignUp.clerk);
const captchaParams = await captchaChallenge.managedOrInvisible({ action: 'signup' });
Expand Down Expand Up @@ -550,6 +559,7 @@ export class SignUp extends BaseResource implements SignUpResource {
this.web3wallet = data.web3_wallet;
this.legalAcceptedAt = data.legal_accepted_at;
this.locale = data.locale;
this.timezone = data.timezone ?? null;
}

eventBus.emit('resource:update', { resource: this });
Expand Down Expand Up @@ -592,6 +602,7 @@ export class SignUp extends BaseResource implements SignUpResource {
web3_wallet: this.web3wallet,
legal_accepted_at: this.legalAcceptedAt,
locale: this.locale,
timezone: this.timezone,
external_account: this.externalAccount,
external_account_strategy: this.externalAccount?.strategy,
};
Expand Down Expand Up @@ -825,6 +836,10 @@ class SignUpFuture implements SignUpFutureResource {
return this.#resource.locale;
}

get timezone() {
return this.#resource.timezone;
}

get unverifiedFields() {
return this.#resource.unverifiedFields;
}
Expand Down Expand Up @@ -914,6 +929,7 @@ class SignUpFuture implements SignUpFutureResource {

private async _create(params: SignUpFutureCreateParams): Promise<void> {
const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken(params);
const timezone = params.timezone ?? getBrowserTimezone();

const body: Record<string, unknown> = {
transfer: params.transfer,
Expand All @@ -923,6 +939,7 @@ class SignUpFuture implements SignUpFutureResource {
...params,
unsafeMetadata: params.unsafeMetadata ? normalizeUnsafeMetadata(params.unsafeMetadata) : undefined,
locale: params.locale ?? getBrowserLocale(),
...(timezone !== null ? { timezone } : {}),
};

await this.#resource.__internal_basePost({ path: this.#resource.pathRoot, body });
Expand Down Expand Up @@ -961,9 +978,13 @@ class SignUpFuture implements SignUpFutureResource {
if (this.#resource.id) {
await this.#resource.__internal_basePatch({ body });
} else {
// Inject browser locale only when creating the sign-up, so an existing
// sign-up's locale is not overwritten on update.
// Inject browser locale and timezone only when creating the sign-up, so an existing
// sign-up's values are not overwritten on update.
body.locale = params.locale ?? getBrowserLocale();
const timezone = getBrowserTimezone();
if (timezone !== null) {
body.timezone = timezone;
}
await this.#resource.__internal_basePost({ path: this.#resource.pathRoot, body });
}
});
Expand Down Expand Up @@ -1104,9 +1125,13 @@ class SignUpFuture implements SignUpFutureResource {
if (this.#resource.id) {
return this.#resource.__internal_basePatch({ body });
}
// Inject browser locale only when creating the sign-up, so an existing
// sign-up's locale is not overwritten on update.
// Inject browser locale and timezone only when creating the sign-up, so an existing
// sign-up's values are not overwritten on update.
body.locale = locale ?? getBrowserLocale();
const browserTimezone = getBrowserTimezone();
if (browserTimezone !== null) {
body.timezone = browserTimezone;
}
return this.#resource.__internal_basePost({ path: this.#resource.pathRoot, body });
};

Expand Down
3 changes: 3 additions & 0 deletions packages/clerk-js/src/core/resources/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export class User extends BaseResource implements UserResource {
legalAcceptedAt: Date | null = null;
updatedAt: Date | null = null;
createdAt: Date | null = null;
timezone: string | null = null;

private cachedSessionsWithActivities: SessionWithActivities[] | null = null;

Expand Down Expand Up @@ -458,6 +459,7 @@ export class User extends BaseResource implements UserResource {
this.createOrganizationEnabled = data.create_organization_enabled || false;
this.createOrganizationsLimit = data.create_organizations_limit || null;
this.deleteSelfEnabled = data.delete_self_enabled || false;
this.timezone = data.timezone ?? null;

if (data.last_sign_in_at) {
this.lastSignInAt = unixEpochToDate(data.last_sign_in_at);
Expand Down Expand Up @@ -506,6 +508,7 @@ export class User extends BaseResource implements UserResource {
legal_accepted_at: this.legalAcceptedAt?.getTime() || null,
updated_at: this.updatedAt?.getTime() || null,
created_at: this.createdAt?.getTime() || null,
timezone: this.timezone,
};
}
}
Loading
Loading