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

Capture authentication timezones so Clerk emails can display timestamps in a stored user timezone.
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 @@ -706,6 +706,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
20 changes: 19 additions & 1 deletion packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ import {

import { debugLogger } from '@/utils/debug';

import { getBrowserLocale, web3 } from '../../utils';
import { getBrowserLocale, getBrowserTimezone, web3 } from '../../utils';
import {
_authenticateWithPopup,
_futureAuthenticateWithPopup,
Expand Down Expand Up @@ -125,6 +125,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 @@ -198,6 +199,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 @@ -653,6 +661,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 @@ -713,6 +722,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 @@ -806,6 +816,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 @@ -1027,13 +1041,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 } : {}),
};

await this.#resource.__internal_basePost({
Expand All @@ -1058,12 +1074,14 @@ class SignInFuture implements SignInFutureResource {
const identifier = params.identifier || params.emailAddress || params.phoneNumber;
const previousIdentifier = this.#resource.identifier;
const locale = getBrowserLocale();
const timezone = this.#resource.id ? null : (params.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
52 changes: 41 additions & 11 deletions packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import type {

import { debugLogger } from '@/utils/debug';

import { getBrowserLocale, getClerkQueryParam, web3 } from '../../utils';
import { getBrowserLocale, getBrowserTimezone, getClerkQueryParam, web3 } from '../../utils';
import {
_authenticateWithPopup,
_futureAuthenticateWithPopup,
Expand All @@ -76,6 +76,12 @@ declare global {
}
}

const withoutTimezone = <T extends object>(params: T): Omit<T, 'timezone'> => {
const body = { ...params } as T & { timezone?: unknown };
delete body.timezone;
return body;
};

export class SignUp extends BaseResource implements SignUpResource {
pathRoot = '/client/sign_ups';

Expand All @@ -101,6 +107,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 +175,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 All @@ -186,7 +200,7 @@ export class SignUp extends BaseResource implements SignUpResource {
prepareVerification = (params: PrepareVerificationParams): Promise<this> => {
debugLogger.debug('SignUp.prepareVerification', { id: this.id, strategy: params.strategy });
return this._basePost({
body: params,
body: withoutTimezone(params),
action: 'prepare_verification',
coalesce: true,
});
Expand All @@ -195,7 +209,7 @@ export class SignUp extends BaseResource implements SignUpResource {
attemptVerification = (params: AttemptVerificationParams): Promise<SignUpResource> => {
debugLogger.debug('SignUp.attemptVerification', { id: this.id, strategy: params.strategy });
return this._basePost({
body: params,
body: withoutTimezone(params),
action: 'attempt_verification',
});
};
Expand Down Expand Up @@ -502,7 +516,7 @@ export class SignUp extends BaseResource implements SignUpResource {

update = (params: SignUpUpdateParams): Promise<SignUpResource> => {
return this._basePatch({
body: normalizeUnsafeMetadata(params),
body: normalizeUnsafeMetadata(withoutTimezone(params)),
});
};

Expand Down Expand Up @@ -550,6 +564,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 +607,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 +841,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 +934,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 +944,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 All @@ -937,7 +959,7 @@ class SignUpFuture implements SignUpFutureResource {
async update(params: SignUpFutureUpdateParams): Promise<{ error: ClerkError | null }> {
return runAsyncResourceTask(this.#resource, async () => {
const body: Record<string, unknown> = {
...params,
...withoutTimezone(params),
unsafeMetadata: params.unsafeMetadata ? normalizeUnsafeMetadata(params.unsafeMetadata) : undefined,
};

Expand All @@ -954,16 +976,20 @@ class SignUpFuture implements SignUpFutureResource {
captchaToken,
captchaWidgetType,
captchaError,
...params,
...withoutTimezone(params),
unsafeMetadata: params.unsafeMetadata ? normalizeUnsafeMetadata(params.unsafeMetadata) : undefined,
};

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 +1130,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 Expand Up @@ -1218,7 +1248,7 @@ class SignUpFuture implements SignUpFutureResource {

async ticket(params?: SignUpFutureTicketParams): Promise<{ error: ClerkError | null }> {
const ticket = params?.ticket ?? getClerkQueryParam('__clerk_ticket');
return this.create({ ...params, strategy: 'ticket', ticket: ticket ?? undefined });
return this.create({ ...withoutTimezone(params ?? {}), strategy: 'ticket', ticket: ticket ?? undefined });
}

async finalize(params?: SignUpFutureFinalizeParams): Promise<{ error: ClerkError | null }> {
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 @@ -456,6 +457,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 @@ -504,6 +506,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