diff --git a/KeychainExample/.detoxrc.js b/KeychainExample/.detoxrc.js index 9d6b24c1..7172e5dc 100644 --- a/KeychainExample/.detoxrc.js +++ b/KeychainExample/.detoxrc.js @@ -43,7 +43,7 @@ module.exports = { type: 'ios.simulator', headless: Boolean(process.env.CI), device: { - type: 'iPhone 15 Pro', + type: 'iPhone 16 Pro', }, }, attached: { diff --git a/KeychainExample/package.json b/KeychainExample/package.json index 2b6b3743..5ed49844 100644 --- a/KeychainExample/package.json +++ b/KeychainExample/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "android": "react-native run-android", - "ios": "react-native run-ios --simulator 'iPhone 15 Pro'", + "ios": "react-native run-ios", "build:android": "npm run mkdist && react-native bundle --entry-file index.js --platform android --dev false --bundle-output dist/main.android.jsbundle --assets-dest dist/res", "build:ios": "npm run mkdist && react-native bundle --entry-file index.js --platform ios --dev false --bundle-output dist/main.ios.jsbundle --assets-dest dist/assets", "mkdist": "node -e \"require('node:fs').mkdirSync('dist', { recursive: true, mode: 0o755 })\"", diff --git a/src/KeychainError.ts b/src/KeychainError.ts new file mode 100644 index 00000000..6a36271a --- /dev/null +++ b/src/KeychainError.ts @@ -0,0 +1,38 @@ +import { ERROR_CODE } from './enums'; + +const RETRYABLE_ERROR_CODES = [ + ERROR_CODE.BIOMETRIC_TIMEOUT, + ERROR_CODE.BIOMETRIC_LOCKOUT, + ERROR_CODE.BIOMETRIC_TEMPORARILY_UNAVAILABLE, +]; + +/** + * Custom error class for encapsulating the native error objects. + */ +export class KeychainError extends Error { + readonly code: ERROR_CODE; + readonly retryable: boolean; + readonly cause: Error | null; + + constructor(message: string, code: ERROR_CODE, cause?: Error) { + super(message); + + this.name = 'KeychainError'; + this.code = code; + this.retryable = RETRYABLE_ERROR_CODES.includes(code); + this.cause = cause ?? null; + } + + static parse(err: unknown) { + if (err instanceof Error) { + const code = + 'code' in err ? (err.code as ERROR_CODE) : ERROR_CODE.INTERNAL_ERROR; + + return new KeychainError(err.message, code, err); + } + + return err; + } +} + +export default KeychainError; diff --git a/src/index.ts b/src/index.ts index b2133c79..3391e8df 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ import type { AuthenticationTypeOption, AccessControlOption, } from './types'; +import KeychainError from './KeychainError'; import { normalizeAuthPrompt } from './normalizeOptions'; const { RNKeychainManager } = NativeModules; @@ -37,16 +38,20 @@ const { RNKeychainManager } = NativeModules; * await Keychain.setGenericPassword('username', 'password'); * ``` */ -export function setGenericPassword( +export async function setGenericPassword( username: string, password: string, options?: SetOptions ): Promise { - return RNKeychainManager.setGenericPasswordForOptions( - normalizeAuthPrompt(options), - username, - password - ); + try { + return await RNKeychainManager.setGenericPasswordForOptions( + normalizeAuthPrompt(options), + username, + password + ); + } catch (err) { + throw KeychainError.parse(err); + } } /** @@ -66,12 +71,16 @@ export function setGenericPassword( * } * ``` */ -export function getGenericPassword( +export async function getGenericPassword( options?: GetOptions ): Promise { - return RNKeychainManager.getGenericPasswordForOptions( - normalizeAuthPrompt(options) - ); + try { + return await RNKeychainManager.getGenericPasswordForOptions( + normalizeAuthPrompt(options) + ); + } catch (err) { + throw KeychainError.parse(err); + } } /** @@ -87,8 +96,14 @@ export function getGenericPassword( * console.log('Password exists:', hasPassword); * ``` */ -export function hasGenericPassword(options?: BaseOptions): Promise { - return RNKeychainManager.hasGenericPasswordForOptions(options); +export async function hasGenericPassword( + options?: BaseOptions +): Promise { + try { + return await RNKeychainManager.hasGenericPasswordForOptions(options); + } catch (err) { + throw KeychainError.parse(err); + } } /** @@ -104,8 +119,14 @@ export function hasGenericPassword(options?: BaseOptions): Promise { * console.log('Password reset successful:', success); * ``` */ -export function resetGenericPassword(options?: BaseOptions): Promise { - return RNKeychainManager.resetGenericPasswordForOptions(options); +export async function resetGenericPassword( + options?: BaseOptions +): Promise { + try { + return await RNKeychainManager.resetGenericPasswordForOptions(options); + } catch (err) { + throw KeychainError.parse(err); + } } /** @@ -119,10 +140,14 @@ export function resetGenericPassword(options?: BaseOptions): Promise { * console.log('Services:', services); * ``` */ -export function getAllGenericPasswordServices( +export async function getAllGenericPasswordServices( options?: GetAllOptions ): Promise { - return RNKeychainManager.getAllGenericPasswordServices(options); + try { + return await RNKeychainManager.getAllGenericPasswordServices(options); + } catch (err) { + throw KeychainError.parse(err); + } } /** @@ -138,10 +163,14 @@ export function getAllGenericPasswordServices( * console.log('Internet credentials exist:', hasCredentials); * ``` */ -export function hasInternetCredentials( +export async function hasInternetCredentials( options: string | BaseOptions ): Promise { - return RNKeychainManager.hasInternetCredentialsForOptions(options); + try { + return await RNKeychainManager.hasInternetCredentialsForOptions(options); + } catch (err) { + throw KeychainError.parse(err); + } } /** @@ -159,18 +188,22 @@ export function hasInternetCredentials( * await Keychain.setInternetCredentials('https://example.com', 'username', 'password'); * ``` */ -export function setInternetCredentials( +export async function setInternetCredentials( server: string, username: string, password: string, options?: SetOptions ): Promise { - return RNKeychainManager.setInternetCredentialsForServer( - server, - username, - password, - normalizeAuthPrompt(options) - ); + try { + return await RNKeychainManager.setInternetCredentialsForServer( + server, + username, + password, + normalizeAuthPrompt(options) + ); + } catch (err) { + throw KeychainError.parse(err); + } } /** @@ -191,14 +224,18 @@ export function setInternetCredentials( * } * ``` */ -export function getInternetCredentials( +export async function getInternetCredentials( server: string, options?: GetOptions ): Promise { - return RNKeychainManager.getInternetCredentialsForServer( - server, - normalizeAuthPrompt(options) - ); + try { + return await RNKeychainManager.getInternetCredentialsForServer( + server, + normalizeAuthPrompt(options) + ); + } catch (err) { + throw KeychainError.parse(err); + } } /** @@ -214,8 +251,14 @@ export function getInternetCredentials( * console.log('Credentials reset for server'); * ``` */ -export function resetInternetCredentials(options: BaseOptions): Promise { - return RNKeychainManager.resetInternetCredentialsForOptions(options); +export async function resetInternetCredentials( + options: BaseOptions +): Promise { + try { + return await RNKeychainManager.resetInternetCredentialsForOptions(options); + } catch (err) { + throw KeychainError.parse(err); + } } /** @@ -229,12 +272,16 @@ export function resetInternetCredentials(options: BaseOptions): Promise { * console.log('Supported Biometry Type:', biometryType); * ``` */ -export function getSupportedBiometryType(): Promise { +export async function getSupportedBiometryType(): Promise { if (!RNKeychainManager.getSupportedBiometryType) { - return Promise.resolve(null); + return null; } - return RNKeychainManager.getSupportedBiometryType(); + try { + return await RNKeychainManager.getSupportedBiometryType(); + } catch (err) { + throw KeychainError.parse(err); + } } /** @@ -254,17 +301,20 @@ export function getSupportedBiometryType(): Promise { * } * ``` */ -export function requestSharedWebCredentials(): Promise< +export async function requestSharedWebCredentials(): Promise< false | SharedWebCredentials > { if (Platform.OS !== 'ios') { - return Promise.reject( - new Error( - `requestSharedWebCredentials() is not supported on ${Platform.OS} yet` - ) + throw new Error( + `requestSharedWebCredentials() is not supported on ${Platform.OS} yet` ); } - return RNKeychainManager.requestSharedWebCredentials(); + + try { + return await RNKeychainManager.requestSharedWebCredentials(); + } catch (err) { + throw KeychainError.parse(err); + } } /** @@ -284,23 +334,26 @@ export function requestSharedWebCredentials(): Promise< * console.log('Shared web credentials set'); * ``` */ -export function setSharedWebCredentials( +export async function setSharedWebCredentials( server: string, username: string, password?: string ): Promise { if (Platform.OS !== 'ios') { - return Promise.reject( - new Error( - `setSharedWebCredentials() is not supported on ${Platform.OS} yet` - ) + throw new Error( + `setSharedWebCredentials() is not supported on ${Platform.OS} yet` + ); + } + + try { + return await RNKeychainManager.setSharedWebCredentialsForServer( + server, + username, + password ); + } catch (err) { + throw KeychainError.parse(err); } - return RNKeychainManager.setSharedWebCredentialsForServer( - server, - username, - password - ); } /** @@ -318,13 +371,18 @@ export function setSharedWebCredentials( * console.log('Can imply authentication:', canAuthenticate); * ``` */ -export function canImplyAuthentication( +export async function canImplyAuthentication( options?: AuthenticationTypeOption ): Promise { if (!RNKeychainManager.canCheckAuthentication) { - return Promise.resolve(false); + return false; + } + + try { + return await RNKeychainManager.canCheckAuthentication(options); + } catch (err) { + throw KeychainError.parse(err); } - return RNKeychainManager.canCheckAuthentication(options); } /** @@ -342,13 +400,18 @@ export function canImplyAuthentication( * console.log('Security Level:', securityLevel); * ``` */ -export function getSecurityLevel( +export async function getSecurityLevel( options?: AccessControlOption ): Promise { if (!RNKeychainManager.getSecurityLevel) { - return Promise.resolve(null); + return null; + } + + try { + return await RNKeychainManager.getSecurityLevel(options); + } catch (err) { + throw KeychainError.parse(err); } - return RNKeychainManager.getSecurityLevel(options); } /** @@ -362,15 +425,21 @@ export function getSecurityLevel( * console.log('Passcode authentication available:', isAvailable); * ``` */ -export function isPasscodeAuthAvailable(): Promise { +export async function isPasscodeAuthAvailable(): Promise { if (!RNKeychainManager.isPasscodeAuthAvailable) { - return Promise.resolve(false); + return false; + } + + try { + return await RNKeychainManager.isPasscodeAuthAvailable(); + } catch (err) { + throw KeychainError.parse(err); } - return RNKeychainManager.isPasscodeAuthAvailable(); } export * from './enums'; export * from './types'; +export * from './KeychainError'; /** @ignore */ export default { SECURITY_LEVEL, @@ -380,6 +449,7 @@ export default { BIOMETRY_TYPE, STORAGE_TYPE, ERROR_CODE, + KeychainError, getSecurityLevel, canImplyAuthentication, getSupportedBiometryType,