import { Platform } from 'react-native'; import type { AuthenticationResponseJSON, CredentialReturn, PublicKeyCredentialCreationOptionsWithoutExtensions, PublicKeyCredentialRequestOptionsWithoutExtensions, PublicKeyCredentialWithAuthenticatorAssertionResponse, PublicKeyCredentialWithAuthenticatorAttestationResponse, RegistrationResponseJSON, SerializedPublicKeyCredentialCreationOptions, SerializedPublicKeyCredentialRequestOptions, } from './ClerkExpoPasskeys.types'; import ClerkExpoPasskeys from './ClerkExpoPasskeysModule'; import { arrayBufferToBase64Url, base64urlToArrayBuffer, ClerkWebAuthnError, encodeBase64Url, mapNativeErrorToClerkWebAuthnErrorCode, toArrayBuffer, } from './utils'; const makeSerializedCreateResponse = ( publicCredential: RegistrationResponseJSON, ): PublicKeyCredentialWithAuthenticatorAttestationResponse => ({ id: publicCredential.id, rawId: base64urlToArrayBuffer(publicCredential.rawId), response: { getTransports: () => publicCredential?.response?.transports as string[], attestationObject: base64urlToArrayBuffer(publicCredential.response.attestationObject), clientDataJSON: base64urlToArrayBuffer(publicCredential.response.clientDataJSON), }, type: publicCredential.type, authenticatorAttachment: publicCredential.authenticatorAttachment || null, toJSON: () => publicCredential, }); export async function create( publicKey: PublicKeyCredentialCreationOptionsWithoutExtensions, ): Promise> { if (!publicKey || !publicKey.rp.id) { throw new Error('Invalid public key or RpID'); } const createOptions: SerializedPublicKeyCredentialCreationOptions = { rp: { id: publicKey.rp.id, name: publicKey.rp.name }, user: { id: encodeBase64Url(toArrayBuffer(publicKey.user.id)), displayName: publicKey.user.displayName, name: publicKey.user.name, }, pubKeyCredParams: publicKey.pubKeyCredParams, challenge: encodeBase64Url(toArrayBuffer(publicKey.challenge)), authenticatorSelection: { authenticatorAttachment: 'platform', requireResidentKey: true, residentKey: 'required', userVerification: 'required', }, excludeCredentials: publicKey.excludeCredentials.map(c => ({ type: 'public-key', id: encodeBase64Url(toArrayBuffer(c.id)), })), }; const createPasskeyModule = Platform.select({ android: async () => ClerkExpoPasskeys.create(JSON.stringify(createOptions)), ios: async () => ClerkExpoPasskeys.create( createOptions.challenge, createOptions.rp.id, createOptions.user.id, createOptions.user.displayName, ), default: null, }); if (!createPasskeyModule) { throw new Error('Platform not supported'); } try { const response = await createPasskeyModule(); return { publicKeyCredential: makeSerializedCreateResponse(typeof response === 'string' ? JSON.parse(response) : response), error: null, }; } catch (error: any) { return { publicKeyCredential: null, error: mapNativeErrorToClerkWebAuthnErrorCode(error.code, error.message, 'create'), }; } } const makeSerializedGetResponse = ( publicKeyCredential: AuthenticationResponseJSON, ): PublicKeyCredentialWithAuthenticatorAssertionResponse => { return { type: publicKeyCredential.type, id: publicKeyCredential.id, rawId: base64urlToArrayBuffer(publicKeyCredential.rawId), authenticatorAttachment: publicKeyCredential?.authenticatorAttachment || null, response: { clientDataJSON: base64urlToArrayBuffer(publicKeyCredential.response.clientDataJSON), authenticatorData: base64urlToArrayBuffer(publicKeyCredential.response.authenticatorData), signature: base64urlToArrayBuffer(publicKeyCredential.response.signature), userHandle: publicKeyCredential?.response.userHandle ? base64urlToArrayBuffer(publicKeyCredential?.response.userHandle) : null, }, toJSON: () => publicKeyCredential, }; }; export async function get({ publicKeyOptions, }: { publicKeyOptions: PublicKeyCredentialRequestOptionsWithoutExtensions; }): Promise> { if (!publicKeyOptions) { throw new Error('publicKeyCredential has not been provided'); } const serializedPublicCredential: SerializedPublicKeyCredentialRequestOptions = { ...publicKeyOptions, // @ts-expect-error FIXME challenge: arrayBufferToBase64Url(publicKeyOptions.challenge), }; const getPasskeyModule = Platform.select({ android: async () => ClerkExpoPasskeys.get(JSON.stringify(serializedPublicCredential)), ios: async () => ClerkExpoPasskeys.get(serializedPublicCredential.challenge, serializedPublicCredential.rpId), default: null, }); if (!getPasskeyModule) { return { publicKeyCredential: null, error: new ClerkWebAuthnError('Platform is not supported', { code: 'passkey_not_supported' }), }; } try { const response = await getPasskeyModule(); return { publicKeyCredential: makeSerializedGetResponse(typeof response === 'string' ? JSON.parse(response) : response), error: null, }; } catch (error: any) { return { publicKeyCredential: null, error: mapNativeErrorToClerkWebAuthnErrorCode(error.code, error.message, 'get'), }; } } const ANDROID_9 = 28; const IOS_15 = 15; export function isSupported() { if (Platform.OS === 'android') { return Platform.Version >= ANDROID_9; } if (Platform.OS === 'ios') { return parseInt(Platform.Version, 10) > IOS_15; } return false; } // FIX:The autofill function has been implemented for iOS only, but the pop-up is not showing up. // This seems to be an issue with Expo that we haven't been able to resolve yet. // Further investigation and possibly reaching out to Expo support may be necessary. // async function autofill(): Promise { // if (Platform.OS === 'android') { // throw new Error('Not supported'); // } else if (Platform.OS === 'ios') { // throw new Error('Not supported'); // } else { // throw new Error('Not supported'); // } // } export const passkeys = { create, get, isSupported, isAutoFillSupported: () => { throw new Error('Not supported'); }, };