Skip to content
Open
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
1 change: 1 addition & 0 deletions src/lib/src/features/clerk/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './use-auth-with-identifier';
export * from './use-add-identifier';
export * from './use-reset-password';
export * from './use-update-password';
export * from './use-update-identifier';
53 changes: 23 additions & 30 deletions src/lib/src/features/clerk/hooks/use-add-identifier.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,55 @@
import { isClerkAPIResponseError, useUser } from '@clerk/clerk-expo';
import { EmailAddressResource, PhoneNumberResource } from '@clerk/types';
import { useState } from 'react';
import { ClerkApiError } from '../enums';
import { UseAddIdentifierReturn } from '../types';
import { IdentifierType, UseAddIdentifierReturn } from '../types';

/**
* Hook that provides functionality to add new email or phone number identifiers to a user's account and verify them using verification codes.
*
* @param {IdentifierType} type - Specifies the type of identifier (e.g., 'phone', 'email')
*
* @returns {UseAddIdentifierReturn} Object containing:
* - `createIdentifier` - A function to add a new email or phone number identifier to the user's account and prepare it for verification
* - `verifyCode` - A function to verify a code sent to the identifier, completing the verification process
* - `isCreating` - A boolean indicating whether an identifier is currently being added
* - `isVerifying` - A boolean indicating whether a verification code is currently being processed
*/
export function useAddIdentifier(): UseAddIdentifierReturn {
export function useAddIdentifier(type: IdentifierType): UseAddIdentifierReturn {
const { user } = useUser();
const [identifierResource, setIdentifierResource] = useState<PhoneNumberResource | EmailAddressResource>();
const [isCreating, setIsCreating] = useState(false);
const [isVerifying, setIsVerifying] = useState(false);

const isEmail = type === 'email';

const createIdentifier: UseAddIdentifierReturn['createIdentifier'] = async ({ identifier }) => {
setIsCreating(true);
const isEmail = identifier?.includes('@');

try {
isEmail
? await user?.createEmailAddress({ email: identifier })
: await user?.createPhoneNumber({ phoneNumber: identifier });

await user?.reload();
let resource = isEmail
? user?.emailAddresses.find((a) => a.emailAddress === identifier)
: user?.phoneNumbers.find((a) => a.phoneNumber === identifier);

// If the resource already exists, re-creating it will cause an error,
// so skip the creation step and go to the send verification code flow.
if (!resource) {
resource = isEmail
? await user?.createEmailAddress({ email: identifier })
: await user?.createPhoneNumber({ phoneNumber: identifier });

await user?.reload();
}
await prepareVerification({ isEmail, identifier });

await prepareVerification({ identifier, isEmail });
setIdentifierResource(resource);

return { isSuccess: true, user };
} catch (e) {
if (isClerkAPIResponseError(e)) {
const error = e.errors[0];

if (error?.code === ClerkApiError.FORM_IDENTIFIER_EXIST && !getIdentifierVerified({ identifier, isEmail })) {
await prepareVerification({ identifier, isEmail });

await user?.reload();

return { isSuccess: true, user };
} else {
return { error: e, user };
}
return { error: e, user };
}

return { user, isSuccess: false };
return { isSuccess: false, user };
} finally {
setIsCreating(false);
}
Expand Down Expand Up @@ -82,13 +83,5 @@ export function useAddIdentifier(): UseAddIdentifierReturn {
setIdentifierResource(isEmail ? emailResource : phoneResource);
};

const getIdentifierVerified = ({ identifier, isEmail }: { identifier: string; isEmail: boolean }): boolean => {
const identifierResource = isEmail
? user?.emailAddresses?.find((a) => a.emailAddress === identifier)
: user?.phoneNumbers?.find((a) => a.phoneNumber === identifier);

return identifierResource?.verification?.status === 'verified';
};

return { createIdentifier, verifyCode, isCreating, isVerifying };
}
107 changes: 107 additions & 0 deletions src/lib/src/features/clerk/hooks/use-update-identifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useUser } from '@clerk/clerk-expo';
import { EmailAddressResource, PhoneNumberResource } from '@clerk/types';
import { useState } from 'react';
import { IdentifierType, UseUpdateIdentifierReturn } from '../types';
import { useAddIdentifier } from './use-add-identifier';

/**
* Hook that provides functionality to update an existing primary identifier
* (email address or phone number) for the current user.
*
* This hook is a higher-level abstraction built on top of `useAddIdentifier`.
* It reuses the identifier creation and verification flow, but extends it with
* additional logic that, upon successful verification, automatically:
*
* - Sets the newly verified identifier as the primary one
* - Removes the previously primary identifier (if different)
*
* The update process follows these stages:
* 1. `createIdentifier` — adds a new identifier and initiates its verification flow
* 2. `verifyCode` — verifies the received code and, if successful, updates the
* user's primary identifier
*
* All method signatures and return types are inherited from
* `useUpdateIdentifier`, ensuring full compatibility with `useAddIdentifier`.
*
* @param {IdentifierType} type - Specifies the type of identifier (e.g., 'phone', 'email')
*
* @returns {UseUpdateIdentifierReturn} Object containing:
* - `createIdentifier` — Adds a new email or phone identifier and prepares it for verification
* - `verifyCode` — Verifies the code sent to the identifier and, on success,
* updates the user's primary identifier
* - `isCreating` — Indicates whether an identifier is currently being added
* - `isVerifying` — Indicates whether a verification request is currently being processed
* - `isUpdating` — Indicates whether the primary identifier update is currently in progress
*/
export function useUpdateIdentifier(type: IdentifierType): UseUpdateIdentifierReturn {
const { user } = useUser();
const isEmail = type === 'email';

const { createIdentifier, verifyCode: verifyAddIdentifierCode, isCreating, isVerifying } = useAddIdentifier(type);

const [isUpdating, setIsUpdating] = useState(false);

const getIdentifierResource = (identifier: string): EmailAddressResource | PhoneNumberResource | undefined => {
return isEmail
? user?.emailAddresses?.find((a) => a.emailAddress === identifier)
: user?.phoneNumbers?.find((a) => a.phoneNumber === identifier);
};

const swapPrimaryIdentifier = async (identifier: string) => {
const newResource = getIdentifierResource(identifier);

if (!newResource || newResource.verification?.status !== 'verified') {
throw new Error('Identifier not found or not verified');
}

const currentPrimaryId = isEmail ? user?.primaryEmailAddressId : user?.primaryPhoneNumberId;

const resources = isEmail ? user?.emailAddresses : user?.phoneNumbers;
const oldPrimaryResource = resources?.find((r) => r.id === currentPrimaryId);

if (isEmail) {
await user?.update({ primaryEmailAddressId: newResource.id });
} else {
await user?.update({ primaryPhoneNumberId: newResource.id });
}

if (oldPrimaryResource && oldPrimaryResource.id !== newResource.id) {
await oldPrimaryResource.destroy();
}

await user?.reload();
};

const verifyCode: UseUpdateIdentifierReturn['verifyCode'] = async ({ code, identifier }) => {
setIsUpdating(true);

const result = await verifyAddIdentifierCode({ code });

// Important to reload user model after adding new fields
await user?.reload();

if (!result.isSuccess) {
setIsUpdating(false);

return result;
}

try {
await swapPrimaryIdentifier(identifier);

return { isSuccess: true, user };
} catch (error) {
return { isSuccess: false, error, user };
} finally {
setIsUpdating(false);
}
};

return {
createIdentifier,
verifyCode,
isCreating,
isVerifying,
isUpdating,
} satisfies UseUpdateIdentifierReturn;
}
33 changes: 33 additions & 0 deletions src/lib/src/features/clerk/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ export interface UseOtpVerificationReturn {

// #region --- IDENTIFIER MANAGEMENT ---

export type IdentifierType = 'email' | 'phone';

/**
* Return type for a hook that manages adding a new authentication identifier
* (such as an email address or phone number) to the currently signed-in user's account.
Expand Down Expand Up @@ -257,6 +259,37 @@ export interface UseAddIdentifierReturn {
isVerifying: boolean;
}

/**
* Return type for a hook that manages updating a new authentication identifier
* (such as an email address or phone number) to the currently signed-in user's account.
*/
export interface UseUpdateIdentifierReturn extends Omit<UseAddIdentifierReturn, 'verifyCode'> {
/**
* Verifies the code and select the provided identifier as primary.
*
* @param params - Parameters for verification.
* @param params.code - The one-time code sent to the identifier.
* @param params.identifier - The one-time code sent to the identifier.
*
* @returns A Promise resolving to a result object:
* - On success: `BaseSuccessReturn` with optional `user`.
* - On failure: `BaseFailureReturn` with optional `verifyAttempt` (email or phone resource) and `user`.
*/
verifyCode: (params: { code: string; identifier: string }) => Promise<
(
| BaseSuccessReturn
| (BaseFailureReturn & {
verifyAttempt?: PhoneNumberResource | EmailAddressResource;
})
) & {
user?: UserResource | null;
}
>;

/** Indicates whether a update identifier request is currently being processed. `true` or `false` */
isUpdating: boolean;
}

// #endregion

// #region --- AUTH WITH IDENTIFIER ---
Expand Down