diff --git a/.changeset/user-update-metadata.md b/.changeset/user-update-metadata.md new file mode 100644 index 00000000000..7b183fb5d29 --- /dev/null +++ b/.changeset/user-update-metadata.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': minor +'@clerk/shared': minor +--- + +Add `user.updateMetadata()` for updating current user's metadata attributes by merging existing values with the provided parameters. diff --git a/packages/clerk-js/src/core/resources/User.ts b/packages/clerk-js/src/core/resources/User.ts index af80f6704bb..3b96e83f342 100644 --- a/packages/clerk-js/src/core/resources/User.ts +++ b/packages/clerk-js/src/core/resources/User.ts @@ -35,6 +35,7 @@ import type { TOTPJSON, TOTPResource, UpdateMeEnterpriseConnectionParams, + UpdateUserMetadataParams, UpdateUserParams, UpdateUserPasswordParams, UserJSON, @@ -241,6 +242,13 @@ export class User extends BaseResource implements UserResource { }); }; + updateMetadata = (params: UpdateUserMetadataParams): Promise => { + return this._basePatch({ + path: `${this.path()}/metadata`, + body: normalizeUnsafeMetadata(params), + }); + }; + updatePassword = (params: UpdateUserPasswordParams): Promise => { return this._basePost({ body: params, diff --git a/packages/clerk-js/src/core/resources/__tests__/User.test.ts b/packages/clerk-js/src/core/resources/__tests__/User.test.ts index 0dad85bc27e..4e56eaf1b5d 100644 --- a/packages/clerk-js/src/core/resources/__tests__/User.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/User.test.ts @@ -727,4 +727,38 @@ describe('User', () => { body: params, }); }); + + it('.updateMetadata triggers a PATCH to /me/metadata with stringified unsafe_metadata', async () => { + // @ts-ignore + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: {} })); + + const user = new User({} as unknown as UserJSON); + await user.updateMetadata({ unsafeMetadata: { theme: 'dark', nested: { level: 1 } } }); + + // @ts-ignore + expect(BaseResource._fetch).toHaveBeenCalledWith({ + method: 'PATCH', + path: '/me/metadata', + body: { + unsafeMetadata: JSON.stringify({ theme: 'dark', nested: { level: 1 } }), + }, + }); + }); + + it('.updateMetadata sends an explicit null patch when a key is being removed', async () => { + // @ts-ignore + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: {} })); + + const user = new User({} as unknown as UserJSON); + await user.updateMetadata({ unsafeMetadata: { theme: null as unknown as undefined } }); + + // @ts-ignore + expect(BaseResource._fetch).toHaveBeenCalledWith({ + method: 'PATCH', + path: '/me/metadata', + body: { + unsafeMetadata: JSON.stringify({ theme: null }), + }, + }); + }); }); diff --git a/packages/shared/src/types/user.ts b/packages/shared/src/types/user.ts index 43e5aa0a492..5f6c392cc93 100644 --- a/packages/shared/src/types/user.ts +++ b/packages/shared/src/types/user.ts @@ -108,6 +108,7 @@ export interface UserResource extends ClerkResource, BillingPayerMethods { createdAt: Date | null; update: (params: UpdateUserParams) => Promise; + updateMetadata: (params: UpdateUserMetadataParams) => Promise; delete: () => Promise; updatePassword: (params: UpdateUserPasswordParams) => Promise; removePassword: (params: RemoveUserPasswordParams) => Promise; @@ -187,6 +188,16 @@ type UpdateUserJSON = Pick< export type UpdateUserParams = Partial>; +/** + * Parameters for {@link UserResource.updateMetadata}. Only `unsafeMetadata` + * is end-user-writable on the Frontend API and the field is required: the + * submitted value is deep-merged with the existing `unsafeMetadata`, and keys + * at any level whose value is `null` are removed. + */ +export type UpdateUserMetadataParams = { + unsafeMetadata: UserUnsafeMetadata; +}; + export type UpdateUserPasswordParams = { newPassword: string; currentPassword?: string;