{"id":26869393,"url":"https://github.com/iosazee/expo-passkey","last_synced_at":"2025-03-31T06:01:27.086Z","repository":{"id":284086569,"uuid":"953777750","full_name":"iosazee/expo-passkey","owner":"iosazee","description":null,"archived":false,"fork":false,"pushed_at":"2025-03-24T04:45:34.000Z","size":0,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-24T05:01:41.656Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/iosazee.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-03-24T04:05:41.000Z","updated_at":"2025-03-24T04:45:38.000Z","dependencies_parsed_at":"2025-03-24T05:01:54.974Z","dependency_job_id":"37ba3bac-75ef-47f6-aa8c-62700c444d3c","html_url":"https://github.com/iosazee/expo-passkey","commit_stats":null,"previous_names":["iosazee/expo-passkey"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iosazee%2Fexpo-passkey","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iosazee%2Fexpo-passkey/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iosazee%2Fexpo-passkey/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iosazee%2Fexpo-passkey/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/iosazee","download_url":"https://codeload.github.com/iosazee/expo-passkey/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246423731,"owners_count":20774820,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-31T06:01:26.009Z","updated_at":"2025-03-31T06:01:27.055Z","avatar_url":"https://github.com/iosazee.png","language":"TypeScript","funding_links":[],"categories":["Plugins","TypeScript"],"sub_categories":[],"readme":"# Expo Passkey\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Platform-iOS%20%7C%20Android-blue\" alt=\"Platform iOS | Android\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/License-MIT-green\" alt=\"MIT License\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/TypeScript-Ready-blue\" alt=\"TypeScript Ready\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Status-Beta-yellow\" alt=\"Beta Status\" /\u003e\n\u003c/p\u003e\n\nA Better Auth plugin enabling secure, passwordless authentication in Expo applications through native biometric authentication.\n\n## 📋 Table of Contents\n\n- [Overview](#overview)\n- [Key Features](#key-features)\n- [Platform Requirements](#platform-requirements)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Detailed Setup](#detailed-setup)\n- [Usage Examples](#usage-examples)\n- [Complete API Reference](#complete-api-reference)\n- [Database Schema](#database-schema)\n- [UI Components](#ui-components)\n- [Integration With Better Auth](#integration-with-better-auth)\n- [Hooks and Patterns](#hooks-and-patterns)\n- [Troubleshooting](#troubleshooting)\n- [Security Considerations](#security-considerations)\n- [Error Handling](#error-handling)\n- [Bugs and Known Issues](#bugs-and-known-issues)\n- [License](#license)\n\n## Overview\n\nExpo Passkey bridges the gap between Better Auth's backend capabilities and native biometric authentication on mobile devices. It allows your users to authenticate securely using Face ID, Touch ID, or fingerprint recognition without passwords, providing a modern, frictionless authentication experience.\n\nThis plugin implements FIDO2-inspired passkey authentication by connecting Better Auth's backend infrastructure with Expo's client-side biometric capabilities, offering a complete end-to-end solution that you can integrate with minimal configuration.\n\n## Key Features\n\n- ✅ **Seamless Integration**: Works directly with Better Auth server and Expo client\n- ✅ **Native Biometrics**: Leverages Face ID, Touch ID, and fingerprint authentication\n- ✅ **Cross-Platform**: Full support for iOS (16+) and Android (10+)\n- ✅ **Complete Lifecycle Management**: Registration, authentication, and revocation flows\n- ✅ **Type-Safe API**: Comprehensive TypeScript definitions and autocomplete\n- ✅ **Secure Device Binding**: Ensures keys are bound to specific devices\n- ✅ **Database Integration**: Automatically creates a MobilePasskey model in your database\n- ✅ **Automatic Cleanup**: Optional automatic revocation of unused passkeys\n- ✅ **Rich Metadata**: Store and retrieve device-specific context with each passkey\n- ✅ **Custom UI Hooks**: Simplifies integration in your React Native UI\n\n## Platform Requirements\n\n| Platform | Minimum Version | Biometric Requirements |\n|----------|----------------|------------------------|\n| iOS      | iOS 16+        | Face ID or Touch ID configured |\n| Android  | Android 10+ (API level 29+) | Fingerprint or Face Recognition configured |\n\n## Installation\n\n### Client Installation\nIn your expo app:\n```bash\n# Install the package\nnpm i expo-passkey\n\n# Install peer dependencies (if not already installed)\nnpx expo install expo-application expo-local-authentication expo-secure-store expo-crypto expo-device\n```\n\n### Server Installation\nIn your auth server:\n```bash\n# Install the package\nnpm i expo-passkey\n\n# Install peer dependencies (if not already installed)\nnpm install better-auth zod better-fetch\n```\n\n## Quick Start\n\n1. **Add to Server**:\n```typescript\nimport { betterAuth } from \"better-auth\";\nimport { expoPasskey } from \"expo-passkey/server\";\n\nexport const auth = betterAuth({\n  plugins: [\n    expoPasskey({\n      rpId: \"example.com\",\n      rpName: \"Your App Name\"\n    })\n  ]\n});\n```\n\n2. **Add to Client**:\n```typescript\nimport { createAuthClient } from \"better-auth/client\";\nimport { expoPasskeyClient } from \"expo-passkey\";\n\nexport const { \n  registerPasskey, \n  authenticateWithPasskey,\n  isPasskeySupported\n} = createAuthClient({\n  plugins: [expoPasskeyClient()]\n});\n```\n\n3. **Implement Authentication**:\n```tsx\nfunction PasskeyButton() {\n  const handleAuth = async () =\u003e {\n    const supported = await isPasskeySupported();\n    if (!supported) {\n      Alert.alert(\"Your device doesn't support passkeys\");\n      return;\n    }\n    \n    try {\n      const result = await authenticateWithPasskey();\n      if (result.data) {\n        // Authentication successful\n        console.log(\"Authenticated user:\", result.data.user);\n      }\n    } catch (error) {\n      console.error(\"Authentication failed:\", error);\n    }\n  };\n\n  return \u003cButton title=\"Sign in with Face ID\" onPress={handleAuth} /\u003e;\n}\n```\n\n## Detailed Setup\n\n### Server Configuration\n\n```typescript\nimport { betterAuth } from \"better-auth\";\nimport { expoPasskey } from \"expo-passkey/server\";\n\nexport const auth = betterAuth({\n  // Other auth config...\n  plugins: [\n    // Other plugins...\n    expoPasskey({\n      rpId: \"example.com\",       // Required: Domain identifier\n      rpName: \"Your App Name\",   // Required: Human-readable app name\n      \n      // Optional settings\n      logger: {\n        enabled: true,           // Enable detailed logging (default: true in dev)\n        level: \"debug\",          // Log level: \"debug\", \"info\", \"warn\", \"error\"\n      },\n      rateLimit: {\n        registerWindow: 300,     // Time window in seconds for rate limiting\n        registerMax: 3,          // Max registration attempts in window\n        authenticateWindow: 60,  // Time window for auth attempts\n        authenticateMax: 5,      // Max auth attempts in window\n      },\n      cleanup: {\n        inactiveDays: 30,        // Auto-revoke passkeys after 30 days of inactivity\n        disableInterval: false,  // Set to true in serverless environments\n      },\n    }),\n  ],\n});\n```\n\n### Client Configuration\n\n```typescript\nimport { createAuthClient } from \"better-auth/client\";\nimport { expoPasskeyClient } from \"expo-passkey\";\n\nexport const authClient = createAuthClient({\n  baseURL: \"https://api.example.com\", // Your API base URL\n  plugins: [\n    expoPasskeyClient({\n      storagePrefix: \"your-app\", // Optional storage key prefix (default: \"_better-auth\")\n    }),\n  ],\n});\n\n// Export actions for use throughout your app\nexport const {\n  registerPasskey,\n  authenticateWithPasskey,\n  listPasskeys,\n  revokePasskey,\n  getBiometricInfo,\n  isPasskeySupported,\n  checkPasskeyRegistration,\n  getStorageKeys,\n} = authClient;\n```\n\n## Usage Examples\n\n### Checking Device Compatibility\n\n```typescript\nimport { isPasskeySupported, getBiometricInfo } from \"./auth-client\";\n\n// Check if device supports passkeys\nconst supported = await isPasskeySupported();\n\nif (supported) {\n  // Get detailed information about biometric capabilities\n  const deviceInfo = await getBiometricInfo();\n  console.log(`Device supports ${deviceInfo.biometricSupport.authenticationType}`);\n}\n```\n\n### Registering a New Passkey\n\n```typescript\nimport { registerPasskey } from \"./auth-client\";\n\n// Register a passkey for the user\n// Will prompt for biometric verification\nconst result = await registerPasskey({\n  userId: \"user-123\",\n  metadata: {\n    deviceName: \"My iPhone\",\n    lastLocation: \"registration-screen\",\n  },\n});\n\nif (result.error) {\n  console.error(\"Registration failed:\", result.error.message);\n} else {\n  console.log(\"Passkey registered successfully\");\n}\n```\n\n### Authenticating with a Passkey\n\n```typescript\nimport { authenticateWithPasskey } from \"./auth-client\";\n\ntry {\n  // Will prompt for biometric verification\n  const result = await authenticateWithPasskey({\n    metadata: {\n      lastLocation: \"login-screen\",\n    }\n  });\n\n  if (result.error) throw result.error;\n  \n  // Authentication successful\n  const { user, token } = result.data;\n  console.log(\"Authenticated user:\", user);\n  \n  // Use token for authenticated API requests\n  // ...\n} catch (error) {\n  console.error(\"Authentication failed:\", error.message);\n}\n```\n\n### Custom Passkey Status Hook\n\n```typescript\nimport { useState, useEffect } from \"react\";\nimport { isPasskeySupported, checkPasskeyRegistration } from \"./auth-client\";\n\nexport function usePasskeyStatus(userId) {\n  const [isSupported, setIsSupported] = useState(false);\n  const [hasPasskey, setHasPasskey] = useState(false);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() =\u003e {\n    async function checkStatus() {\n      try {\n        setLoading(true);\n        \n        // Check device support\n        const supported = await isPasskeySupported();\n        setIsSupported(supported);\n        \n        if (supported \u0026\u0026 userId) {\n          // Check if device has registered passkey\n          const result = await checkPasskeyRegistration(userId);\n          setHasPasskey(result.isRegistered);\n        }\n      } catch (error) {\n        console.error(\"Error checking passkey status:\", error);\n      } finally {\n        setLoading(false);\n      }\n    }\n    \n    checkStatus();\n  }, [userId]);\n\n  return { isSupported, hasPasskey, loading };\n}\n```\n\n### Comprehensive Passkeys Hook\n\n```typescript\nimport { useState } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { listPasskeys, getStorageKeys } from \"./auth-client\";\nimport * as SecureStore from \"expo-secure-store\";\nimport type { MobilePasskey } from \"expo-passkey\";\n\nexport function usePasskeys(userId) {\n  const [currentDeviceId, setCurrentDeviceId] = useState(null);\n\n  // Fetch current device ID\n  const fetchDeviceId = async () =\u003e {\n    try {\n      const STORAGE_KEYS = getStorageKeys();\n      const deviceId = await SecureStore.getItemAsync(STORAGE_KEYS.DEVICE_ID);\n      setCurrentDeviceId(deviceId);\n      return deviceId;\n    } catch (error) {\n      console.error(\"Error fetching device ID:\", error);\n      return null;\n    }\n  };\n\n  // Main query to fetch passkeys\n  const {\n    data: result,\n    isLoading,\n    error,\n    refetch,\n  } = useQuery({\n    queryKey: [\"passkeys\", userId],\n    queryFn: async () =\u003e {\n      // Make sure we have the current device ID\n      await fetchDeviceId();\n\n      // Call the listPasskeys function\n      const result = await listPasskeys({ userId });\n      if (result.error) throw result.error;\n      return result;\n    },\n    enabled: !!userId,\n  });\n\n  // Process the result\n  const passkeys = result?.data?.passkeys || [];\n  const hasRegisteredPasskey = passkeys.length \u003e 0;\n  const currentDeviceHasPasskey = passkeys.some(\n    (pk) =\u003e pk.deviceId === currentDeviceId\n  );\n\n  return {\n    passkeys,\n    hasRegisteredPasskey,\n    currentDeviceHasPasskey,\n    currentDeviceId,\n    isLoading,\n    refetch,\n    error: error instanceof Error ? error : null,\n  };\n}\n```\n\n## Complete API Reference\n\n### Client API\n\n#### `registerPasskey(options): Promise\u003cRegisterPasskeyResult\u003e`\n\nRegisters a new passkey for a user. This will prompt for biometric authentication.\n\n```typescript\ninterface RegisterOptions {\n  userId: string;              // Required: User ID to associate with the passkey\n  deviceId?: string;           // Optional: Override automatic device ID \n  metadata?: {                 // Optional: Additional metadata to store\n    deviceName?: string;       // Device name (e.g. \"John's iPhone\")\n    deviceModel?: string;      // Device model (e.g. \"iPhone 14 Pro\")\n    appVersion?: string;       // App version\n    lastLocation?: string;     // Context where registered (e.g. \"settings-screen\")\n    manufacturer?: string;     // Device manufacturer\n    brand?: string;            // Device brand\n    biometricType?: string;    // Type of biometric used\n    [key: string]: any;        // Any other custom metadata\n  };\n}\n\n// Return type\ninterface RegisterPasskeyResult {\n  data: { \n    success: boolean; \n    rpName: string;            // Relying party name from server config \n    rpId: string;              // Relying party ID from server config\n  } | null;\n  error: Error | null;\n}\n```\n\n#### `authenticateWithPasskey(options?): Promise\u003cAuthenticatePasskeyResult\u003e`\n\nAuthenticates a user with a registered passkey. This will prompt for biometric authentication.\n\n```typescript\ninterface AuthenticateOptions {\n  deviceId?: string;           // Optional: Override automatic device ID\n  metadata?: {                 // Optional: Additional metadata to update\n    lastLocation?: string;     // Context where authentication occurred\n    appVersion?: string;       // App version\n    [key: string]: any;        // Any other custom metadata\n  };\n}\n\n// Return type\ninterface AuthenticatePasskeyResult {\n  data: { \n    token: string;             // Session token for authentication\n    user: {                    // User object\n      id: string;              // User ID\n      email: string;           // User email\n      [key: string]: any;      // Any other user properties\n    };\n  } | null;\n  error: Error | null;\n}\n```\n\n#### `listPasskeys(options): Promise\u003cListPasskeysResult\u003e`\n\nLists all passkeys registered for a user. Useful for managing devices.\n\n```typescript\ninterface ListOptions {\n  userId: string;              // Required: User ID\n  limit?: number;              // Optional: Pagination limit (default: 10)\n  offset?: number;             // Optional: Pagination offset (default: 0)\n}\n\n// Return type\ninterface ListPasskeysResult {\n  data: { \n    passkeys: Array\u003cMobilePasskey\u003e; // Array of passkey objects\n    nextOffset?: number;       // Pagination offset for next page\n  } | null;\n  error: Error | null;\n}\n\n// MobilePasskey type\ninterface MobilePasskey {\n  id: string;                  // Passkey ID\n  userId: string;              // User ID\n  deviceId: string;            // Device ID  \n  platform: string;            // Platform (ios/android)\n  lastUsed: string;            // ISO timestamp\n  status: \"active\" | \"revoked\";\n  createdAt: string;           // ISO timestamp\n  updatedAt: string;           // ISO timestamp\n  revokedAt?: string;          // ISO timestamp (if revoked)\n  revokedReason?: string;      // Reason for revocation\n  metadata: string | Record\u003cstring, any\u003e; // Parsed metadata or JSON string\n}\n```\n\n#### `revokePasskey(options): Promise\u003cRevokePasskeyResult\u003e`\n\nRevokes a passkey, preventing it from being used for authentication.\n\n```typescript\ninterface RevokeOptions {\n  userId: string;              // Required: User ID\n  deviceId?: string;           // Optional: Override automatic device ID\n  reason?: string;             // Optional: Reason for revocation\n}\n\n// Return type\ninterface RevokePasskeyResult {\n  data: { success: boolean } | null;\n  error: Error | null;\n}\n```\n\n#### `checkPasskeyRegistration(userId: string): Promise\u003cPasskeyRegistrationCheckResult\u003e`\n\nChecks if the current device has a registered passkey for the given user.\n\n```typescript\n// Return type\ninterface PasskeyRegistrationCheckResult {\n  isRegistered: boolean;       // Whether device has a registered passkey\n  deviceId: string | null;     // Device ID\n  biometricSupport: BiometricSupportInfo | null; // Biometric support info\n  error: Error | null;         // Error if any\n}\n```\n\n#### `getBiometricInfo(): Promise\u003cDeviceInfo\u003e`\n\nGets information about the device's biometric capabilities, platform, and configuration.\n\n```typescript\n// Return type\ninterface DeviceInfo {\n  deviceId: string;            // Unique device identifier\n  platform: \"ios\" | \"android\"; // Device platform\n  model: string | null;        // Device model (e.g. \"iPhone 14\")\n  manufacturer: string | null; // Device manufacturer (e.g. \"Apple\")\n  osVersion: string;           // OS version (e.g. \"16.0\")\n  appVersion: string;          // App version\n  biometricSupport: {\n    isSupported: boolean;      // Whether biometrics are supported\n    isEnrolled: boolean;       // Whether biometrics are set up\n    availableTypes: number[];  // Available authentication types\n    authenticationType: string; // Human-readable type (e.g. \"Face ID\")\n    error: string | null;      // Error message if any\n    platformDetails: {         // Platform-specific details\n      platform: string;\n      version: string | number;\n      apiLevel?: number | null; // Android API level\n      manufacturer?: string | null;\n      brand?: string | null;\n    }\n  };\n}\n```\n\n#### `isPasskeySupported(): Promise\u003cboolean\u003e`\n\nChecks if passkeys are supported on the current device based on platform, OS version, and biometric capabilities.\n\n```typescript\n// Returns: boolean\n// true if the device supports passkeys, false otherwise\n```\n\n#### `getStorageKeys(): StorageKeys`\n\nGets the storage keys used by the plugin for secure storage.\n\n```typescript\n// Return type\ninterface StorageKeys {\n  DEVICE_ID: string;           // Key for device ID in SecureStore\n  STATE: string;               // Key for state in SecureStore\n  USER_ID: string;             // Key for user ID in SecureStore\n}\n```\n\n### Server API\n\n#### `expoPasskey(options): BetterAuthPlugin`\n\nCreates a server-side plugin for handling passkey operations.\n\n```typescript\ninterface ExpoPasskeyOptions {\n  rpId: string;                // Required: Relying Party ID (domain)\n  rpName: string;              // Required: Human-readable app name\n  \n  // Optional settings\n  logger?: {\n    enabled?: boolean;         // Enable logging (default: true in dev)\n    level?: \"debug\" | \"info\" | \"warn\" | \"error\"; // Log level\n  };\n  \n  rateLimit?: {\n    registerWindow?: number;   // Time window for rate limiting (seconds)\n    registerMax?: number;      // Max registration attempts in window\n    authenticateWindow?: number; // Time window for auth attempts\n    authenticateMax?: number;  // Max auth attempts in window\n  };\n  \n  cleanup?: {\n    inactiveDays?: number;     // Days after which to revoke inactive passkeys\n    disableInterval?: boolean; // Disable automatic cleanup (for serverless)\n  };\n}\n```\n\n## Database Schema\n\nThe plugin automatically creates a `mobilePasskey` table in your database with the following schema:\n\n```typescript\n{\n  id: string;                  // Unique identifier\n  userId: string;              // User ID (references user.id)\n  deviceId: string;            // Device identifier\n  platform: string;            // \"ios\" or \"android\"\n  lastUsed: string;            // ISO timestamp \n  status: \"active\" | \"revoked\"; // Status\n  createdAt: Date;             // Creation timestamp\n  updatedAt: Date;             // Last update timestamp\n  revokedAt?: string;          // Revocation timestamp (if applicable)\n  revokedReason?: string;      // Reason for revocation (if applicable)\n  metadata: string;            // JSON string of device metadata\n}\n```\n\n## UI Components\n\nHere are examples of useful UI components you can create to work with this package:\n\n### PasskeyRegistrationButton Component\n\n```tsx\nimport React, { useState, useEffect } from \"react\";\nimport { View, Pressable, ActivityIndicator, Platform, Alert, Linking } from \"react-native\";\nimport { Text } from \"./ui/text\";\nimport { registerPasskey, getBiometricInfo } from \"../lib/auth-client\";\nimport * as Application from \"expo-application\";\n\nexport const PasskeyRegistration = ({\n  userId,\n  onComplete,\n}: {\n  userId: string;\n  onComplete?: () =\u003e void;\n}) =\u003e {\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState(\"\");\n  const [biometricInfo, setBiometricInfo] = useState(null);\n\n  useEffect(() =\u003e {\n    checkBiometricSupport();\n  }, []);\n\n  const checkBiometricSupport = async () =\u003e {\n    try {\n      const deviceInfo = await getBiometricInfo();\n      setBiometricInfo(deviceInfo.biometricSupport);\n    } catch (err) {\n      setError(err instanceof Error ? err.message : \"Failed to check biometric status\");\n    }\n  };\n\n  const handleRegister = async () =\u003e {\n    try {\n      setLoading(true);\n      setError(\"\");\n\n      const metadata = {\n        lastLocation: \"security-settings\",\n        appVersion: Application.nativeApplicationVersion || \"1.0.0\",\n      };\n\n      // Use the package's registerPasskey function\n      const result = await registerPasskey({\n        userId,\n        metadata,\n      });\n\n      if (result.error) {\n        throw result.error;\n      }\n\n      Alert.alert(\n        \"Success\",\n        `${biometricInfo.authenticationType} has been successfully registered for quick sign-in`,\n        [{ text: \"OK\", onPress: () =\u003e onComplete?.() }]\n      );\n    } catch (err) {\n      setError(err instanceof Error ? err.message : \"Failed to register passkey\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  return (\n    \u003cView\u003e\n      \u003cText className=\"text-lg font-semibold\"\u003e\n        Enable {biometricInfo?.authenticationType || \"Biometric\"} Sign-in\n      \u003c/Text\u003e\n      \u003cText className=\"text-muted-foreground\"\u003e\n        Use {biometricInfo?.authenticationType || \"biometrics\"} for quick and secure sign-in\n      \u003c/Text\u003e\n\n      {error ? (\n        \u003cView className=\"bg-destructive/10 p-4 rounded-md mt-4\"\u003e\n          \u003cText className=\"text-destructive\"\u003e{error}\u003c/Text\u003e\n        \u003c/View\u003e\n      ) : null}\n\n      \u003cPressable\n        className=\"h-12 items-center justify-center rounded-md bg-primary mt-4\"\n        onPress={handleRegister}\n        disabled={loading}\n      \u003e\n        {loading ? (\n          \u003cActivityIndicator color=\"white\" /\u003e\n        ) : (\n          \u003cText className=\"text-white font-semibold\"\u003e\n            Register {biometricInfo?.authenticationType || \"Passkey\"}\n          \u003c/Text\u003e\n        )}\n      \u003c/Pressable\u003e\n    \u003c/View\u003e\n  );\n};\n```\n\n### PasskeyLoginButton Component\n\n```tsx\nimport React, { useState, useEffect } from \"react\";\nimport { Pressable, View, ActivityIndicator } from \"react-native\";\nimport { Text } from \"./ui/text\";\nimport { authenticateWithPasskey, getBiometricInfo, getStorageKeys } from \"../lib/auth-client\";\nimport * as SecureStore from \"expo-secure-store\";\nimport { router } from \"expo-router\";\nimport { Key } from \"lucide-react-native\";\n\nexport function PasskeyLoginButton({\n  onSuccess,\n  onError,\n}) {\n  const [loading, setLoading] = useState(false);\n  const [isAvailable, setIsAvailable] = useState(false);\n\n  useEffect(() =\u003e {\n    checkPasskeyAvailability();\n  }, []);\n\n  const checkPasskeyAvailability = async () =\u003e {\n    try {\n      // Get biometric info from the package\n      const deviceInfo = await getBiometricInfo();\n      const biometricSupport = deviceInfo.biometricSupport;\n\n      // Get the STORAGE_KEYS from the package\n      const STORAGE_KEYS = getStorageKeys();\n      const storedDeviceId = await SecureStore.getItemAsync(STORAGE_KEYS.DEVICE_ID);\n\n      // Platform-specific checks\n      let platformSupported = true;\n      if (Platform.OS === \"ios\") {\n        const version = parseInt(Platform.Version, 10);\n        if (version \u003c 16) platformSupported = false;\n      } else if (Platform.OS === \"android\") {\n        const apiLevel = biometricSupport.platformDetails.apiLevel;\n        if (!apiLevel || apiLevel \u003c 29) platformSupported = false;\n      }\n\n      // Only show if everything is supported and we have a registered passkey\n      setIsAvailable(\n        platformSupported \u0026\u0026 \n        biometricSupport.isSupported \u0026\u0026 \n        biometricSupport.isEnrolled \u0026\u0026 \n        !!storedDeviceId\n      );\n    } catch (error) {\n      console.error(\"Error checking passkey availability:\", error);\n      setIsAvailable(false);\n    }\n  };\n\n  const handlePasskeyAuth = async () =\u003e {\n    try {\n      setLoading(true);\n      const result = await authenticateWithPasskey();\n\n      if (result.error) {\n        throw result.error;\n      }\n\n      if (onSuccess) onSuccess();\n      \n      // Navigate after authentication\n      router.replace(\"/dashboard\");\n    } catch (error) {\n      if (onError \u0026\u0026 error instanceof Error) {\n        onError(error);\n      }\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  // Don't render if not available\n  if (!isAvailable) return null;\n\n  return (\n    \u003cPressable\n      className=\"h-12 w-full items-center justify-center rounded-md border border-border\"\n      onPress={handlePasskeyAuth}\n      disabled={loading}\n    \u003e\n      {loading ? (\n        \u003cActivityIndicator size=\"small\" color=\"#007AFF\" /\u003e\n      ) : (\n        \u003cView className=\"flex-row items-center\"\u003e\n          \u003cKey size={20} color=\"#000\" /\u003e\n          \u003cText className=\"font-semibold ml-2\"\u003eSign in with Passkey\u003c/Text\u003e\n        \u003c/View\u003e\n      )}\n    \u003c/Pressable\u003e\n  );\n}\n```\n\n## Integration With Better Auth\n\n### Configuring in a Next.js Backend\n\nHere's how to integrate with Better Auth:\n\n```typescript\nimport { betterAuth } from \"better-auth\";\nimport { passkey } from \"better-auth/plugins/passkey\";\nimport { emailOTP, admin } from \"better-auth/plugins\";\nimport { nextCookies } from \"better-auth/next-js\";\nimport { prismaAdapter } from \"better-auth/adapters/prisma\";\nimport { expoPasskey } from \"expo-passkey/server\";\nimport { db } from \"./lib/db\";\n\nconst isDevEnvironment = process.env.NODE_ENV === \"development\";\nconst domain = isDevEnvironment ? \"localhost\" : \"yourdomain.com\";\n\nexport const auth = betterAuth({\n  appName: \"Your App\",\n  database: prismaAdapter(db),\n  baseURL: process.env.NEXT_PUBLIC_APP_URL,\n  trustedOrigins: [\n    \"https://yourdomain.com\",\n    \"yourdomain://\", // Deep linking scheme\n    \"exp+yourdomain://\", // Expo linking scheme\n    ...(isDevEnvironment ? [\"http://localhost:3000\"] : []),\n  ],\n  plugins: [\n    // Web passkey support\n    passkey({\n      rpID: domain,\n      rpName: \"Your App\",\n      origin: isDevEnvironment ? \"http://localhost:3000\" : \"https://yourdomain.com\",\n    }),\n    // Expo passkey support (biometric auth for mobile)\n    expoPasskey({\n      rpId: domain,\n      rpName: \"Your App\",\n      logger: {\n        enabled: true,\n        level: \"debug\",\n      },\n    }),\n    // Other auth plugins\n    emailOTP({\n      // Email OTP configuration\n    }),\n    admin(),\n    nextCookies(),\n  ],\n});\n```\nfor more information see https://www.better-auth.com/docs/integrations/next\n\n### Setting Up Client Instance in your expo app\n\n```typescript\n// lib/auth-client.ts\nimport { createAuthClient } from \"better-auth/client\";\nimport { expoPasskeyClient } from \"expo-passkey\";\n\nexport const authClient = createAuthClient({\n  baseURL: process.env.NEXT_PUBLIC_API_URL,\n  plugins: [expoPasskeyClient()],\n});\n\nexport const { \n  registerPasskey, \n  authenticateWithPasskey, \n  listPasskeys,\n  revokePasskey,\n  getBiometricInfo,\n  isPasskeySupported,\n  checkPasskeyRegistration,\n  getStorageKeys,\n  // Other auth functions from Better Auth\n  signIn,\n  signOut,\n  signUp,\n  // ...\n} = authClient;\n```\nfor more information see https://www.better-auth.com/docs/integrations/expo\n\n## Hooks and Patterns\n\n### PasskeyManager Component\n\nA complete example of a PasskeyManager component for displaying and managing passkeys:\n\n```tsx\nimport React, { useState } from \"react\";\nimport { View, FlatList, Alert, ActivityIndicator } from \"react-native\";\nimport { Text, Card, Button } from \"../ui/components\";\nimport { usePasskeys } from \"../hooks/use-passkeys\";\nimport { revokePasskey, getStorageKeys } from \"../lib/auth-client\";\nimport * as SecureStore from \"expo-secure-store\";\nimport { queryClient } from \"../lib/query-client\";\nimport { PasskeyRegistration } from \"./passkey-registration\";\n\nexport function PasskeyManager({ userId }) {\n  const [revoking, setRevoking] = useState(null);\n  \n  const {\n    passkeys,\n    hasRegisteredPasskey,\n    currentDeviceHasPasskey,\n    currentDeviceId,\n    isLoading,\n    refetch,\n  } = usePasskeys(userId);\n\n  const handleRevokePasskey = async (deviceId) =\u003e {\n    Alert.alert(\n      \"Remove Passkey\",\n      \"Are you sure you want to remove this passkey?\",\n      [\n        { text: \"Cancel\" },\n        {\n          text: \"Remove\",\n          style: \"destructive\",\n          onPress: async () =\u003e {\n            try {\n              setRevoking(deviceId);\n              const result = await revokePasskey({\n                userId,\n                deviceId,\n                reason: \"user_requested\",\n              });\n\n              if (result.error) throw result.error;\n\n              // If this is the current device, clear the device ID\n              if (currentDeviceId === deviceId) {\n                const STORAGE_KEYS = getStorageKeys();\n                await SecureStore.deleteItemAsync(STORAGE_KEYS.DEVICE_ID);\n              }\n\n              // Update UI\n              queryClient.invalidateQueries({ queryKey: [\"passkeys\", userId] });\n              Alert.alert(\"Success\", \"Passkey has been removed successfully\");\n            } catch (error) {\n              Alert.alert(\"Error\", \"Failed to remove passkey. Please try again.\");\n            } finally {\n              setRevoking(null);\n            }\n          },\n        },\n      ]\n    );\n  };\n\n  if (isLoading) {\n    return \u003cActivityIndicator size=\"large\" /\u003e;\n  }\n\n  return (\n    \u003cView\u003e\n      \u003cText className=\"text-xl font-bold mb-4\"\u003ePasskey Authentication\u003c/Text\u003e\n      \n      {!currentDeviceHasPasskey \u0026\u0026 (\n        \u003cCard className=\"mb-4 p-4\"\u003e\n          \u003cText className=\"font-semibold mb-2\"\u003eEnable Passkey Authentication\u003c/Text\u003e\n          \u003cText className=\"text-muted-foreground mb-4\"\u003e\n            Set up biometric authentication for faster sign-in on this device.\n          \u003c/Text\u003e\n          \u003cPasskeyRegistration \n            userId={userId} \n            onComplete={refetch} \n          /\u003e\n        \u003c/Card\u003e\n      )}\n\n      {passkeys.length \u003e 0 ? (\n        \u003c\u003e\n          \u003cText className=\"font-semibold mb-2\"\u003eYour Registered Devices\u003c/Text\u003e\n          \u003cFlatList\n            data={passkeys}\n            keyExtractor={(item) =\u003e item.id}\n            renderItem={({ item }) =\u003e {\n              // Parse metadata\n              let metadata = {};\n              if (typeof item.metadata === \"string\") {\n                try {\n                  metadata = JSON.parse(item.metadata);\n                } catch (error) {\n                  console.error(\"Error parsing metadata:\", error);\n                }\n              } else if (item.metadata \u0026\u0026 typeof item.metadata === \"object\") {\n                metadata = item.metadata;\n              }\n\n              const isCurrentDevice = item.deviceId === currentDeviceId;\n              \n              return (\n                \u003cCard className=\"mb-2 p-4\"\u003e\n                  \u003cView className=\"flex-row justify-between items-center mb-2\"\u003e\n                    \u003cText className=\"font-semibold\"\u003e\n                      {metadata.deviceName || metadata.deviceModel || \n                        (item.platform === \"ios\" ? \"iOS Device\" : \"Android Device\")}\n                    \u003c/Text\u003e\n                    {isCurrentDevice \u0026\u0026 (\n                      \u003cView className=\"bg-primary/10 px-2 py-1 rounded\"\u003e\n                        \u003cText className=\"text-xs text-primary\"\u003eCurrent Device\u003c/Text\u003e\n                      \u003c/View\u003e\n                    )}\n                  \u003c/View\u003e\n                  \n                  \u003cView className=\"space-y-1 mb-3\"\u003e\n                    \u003cText className=\"text-sm\"\u003e\n                      Platform: {item.platform === \"ios\" ? \"iOS\" : \"Android\"}\n                    \u003c/Text\u003e\n                    \u003cText className=\"text-sm\"\u003e\n                      Authentication: {metadata.biometricType || \"Biometric\"}\n                    \u003c/Text\u003e\n                    \u003cText className=\"text-sm\"\u003e\n                      Last used: {new Date(item.lastUsed).toLocaleDateString()}\n                    \u003c/Text\u003e\n                  \u003c/View\u003e\n                  \n                  {isCurrentDevice \u0026\u0026 (\n                    \u003cButton \n                      variant=\"destructive\"\n                      onPress={() =\u003e handleRevokePasskey(item.deviceId)}\n                      disabled={revoking === item.deviceId}\n                    \u003e\n                      {revoking === item.deviceId ? (\n                        \u003cActivityIndicator size=\"small\" color=\"white\" /\u003e\n                      ) : (\n                        \"Remove This Passkey\"\n                      )}\n                    \u003c/Button\u003e\n                  )}\n                \u003c/Card\u003e\n              );\n            }}\n          /\u003e\n        \u003c/\u003e\n      ) : (\n        \u003cView className=\"items-center justify-center p-8 bg-muted/20 rounded-lg\"\u003e\n          \u003cText className=\"text-center text-muted-foreground\"\u003e\n            No passkeys registered. Register a passkey to enable biometric sign-in.\n          \u003c/Text\u003e\n        \u003c/View\u003e\n      )}\n    \u003c/View\u003e\n  );\n}\n```\n\n## Troubleshooting\n\n### iOS Issues\n\n- **iOS Version Requirements**: Must be running iOS 16+ for passkey support\n- **Biometric Setup**: Ensure Face ID/Touch ID is configured in device settings\n- **Simulator Limitations**: Biometric authentication in simulators requires additional setup:\n  - In the simulator, go to Features → Face ID/Touch ID → Enrolled\n  - When prompted, select \"Matching Face/Fingerprint\" for success testing\n- **Device ID Generation**: iOS uses vendor ID from `expo-application`\n- **Device Changes**: If a user resets Face ID/Touch ID, passkeys need re-registration\n\n### Android Issues\n\n- **API Level**: Must be running Android 10+ (API level 29+)\n- **Biometric Hardware**: Device must have fingerprint or facial recognition hardware\n- **Configuration**: Biometric authentication must be set up in device settings\n- **Emulator Testing**: Configure fingerprint in emulator settings (AVD Manager):\n  - In AVD settings, enable fingerprint\n  - Use \"adb -e emu finger touch 1\" command to simulate fingerprint\n- **Fragmentation**: Behavior may vary across manufacturers\n\n### Common Issues\n\n1. **\"Device ID not found\" error**:\n   - The device doesn't have a registered passkey\n   - Solution: Register a passkey for the device first\n\n2. **\"Biometric authentication failed\" error**:\n   - User canceled biometric prompt or failed authentication\n   - Solution: Retry authentication or offer alternative login method\n\n3. **\"Invalid credential\" error**:\n   - The passkey has been revoked or doesn't exist\n   - Solution: Re-register passkey\n\n4. **\"Registration failed\" error**:\n   - Check if the user exists in your database\n   - Ensure rpId matches your domain\n   - Check server logs for specific errors\n\n### Diagnostic Tools\n\n1. **Device Compatibility Check**:\n   ```javascript\n   const info = await getBiometricInfo();\n   console.log(JSON.stringify(info, null, 2));\n   ```\n\n2. **Storage Check**:\n   ```javascript\n   import * as SecureStore from 'expo-secure-store';\n   const keys = getStorageKeys();\n   const deviceId = await SecureStore.getItemAsync(keys.DEVICE_ID);\n   console.log(\"Current Device ID:\", deviceId);\n   ```\n\n3. **Clear Device ID** (for testing):\n   ```javascript\n   import * as SecureStore from 'expo-secure-store';\n   const keys = getStorageKeys();\n   await SecureStore.deleteItemAsync(keys.DEVICE_ID);\n   ```\n\n4. **Server Logs**: Enable debug logging on the server:\n   ```javascript\n   expoPasskey({\n     // ...other options\n     logger: { enabled: true, level: \"debug\" }\n   })\n   ```\n\n\n## Security Considerations\n\n- **Device Binding**: Passkeys are bound to specific devices for security\n- **Biometric Data**: Biometric data never leaves the device\n- **Token Security**: Use HTTPS for all API communications\n- **Rate Limiting**: Configure appropriate rate limits to prevent brute force attacks\n- **Automatic Cleanup**: Enable cleanup to revoke unused passkeys periodically\n- **Multiple Devices**: Allow users to register multiple devices for convenience\n- **Fallback Authentication**: Always provide alternate authentication methods\n- **Revocation**: Users should be able to revoke passkeys from all devices\n- **Metadata Handling**: Be careful with what you store in metadata to avoid privacy concerns\n\n## Error Handling\n\nThe package provides specific error codes for different scenarios:\n\n```typescript\n// Environment errors\nERROR_CODES.ENVIRONMENT.NOT_SUPPORTED     // Device/platform not supported\nERROR_CODES.ENVIRONMENT.MODULE_NOT_FOUND  // Required Expo module missing\n\n// Biometric errors\nERROR_CODES.BIOMETRIC.NOT_SUPPORTED       // Device lacks biometric hardware\nERROR_CODES.BIOMETRIC.NOT_ENROLLED        // Biometrics not set up on device\nERROR_CODES.BIOMETRIC.AUTHENTICATION_FAILED // User failed/cancelled verification\n\n// Device errors\nERROR_CODES.DEVICE.ID_GENERATION_FAILED   // Could not generate device ID\n\n// Server errors\nERROR_CODES.SERVER.CREDENTIAL_EXISTS      // Passkey already registered\nERROR_CODES.SERVER.INVALID_CREDENTIAL     // Passkey not found\nERROR_CODES.SERVER.CREDENTIAL_NOT_FOUND   // Passkey doesn't exist\nERROR_CODES.SERVER.AUTHENTICATION_FAILED  // Authentication failed\nERROR_CODES.SERVER.USER_NOT_FOUND         // User not found\n```\n\nExample error handling pattern:\n\n```typescript\ntry {\n  const result = await authenticateWithPasskey();\n  if (result.error) {\n    // Handle specific error types\n    if (result.error.code === ERROR_CODES.BIOMETRIC.AUTHENTICATION_FAILED) {\n      showAuthFailedMessage();\n    } else if (result.error.code === ERROR_CODES.SERVER.INVALID_CREDENTIAL) {\n      promptReregistration();\n    } else {\n      // Generic error handling\n      showErrorMessage(result.error.message);\n    }\n    return;\n  }\n  \n  // Handle success\n  handleSuccessfulAuthentication(result.data);\n} catch (error) {\n  // Catch unexpected errors\n  console.error(\"Unexpected error:\", error);\n  showGenericErrorMessage();\n}\n```\n\n## Bugs and Known Issues\n\nAs the package is currently in beta, there may be unexpected bugs or incomplete features. Please report any issues you encounter on our [Github issues page](https://github.com/iosazee/expo-passkey/issues).\n\nKnown issues include:\n\n- **Expo Go Limitations**: Due to how Expo Go manages native modules, passkey functionality requires a development build or production build\n- **Android Compatibility**: Some Android devices may not support passkeys despite meeting the API level requirements\n- **iOS Simulator**: Biometric authentication in iOS simulators may not work consistently\n- **Error Messages**: Some error messages may not be descriptive enough\n- **Storage Persistence**: On some devices, SecureStore may be cleared when app is uninstalled\n\nWe appreciate your feedback and contributions to improve stability and functionality.\n\n## License\n\nMIT\n\n---\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Related\n\n- [Better Auth Documentation](https://www.better-auth.com/docs/integrations/expo)\n- [Expo Local Authentication](https://docs.expo.dev/versions/latest/sdk/local-authentication/)\n- [FIDO2 WebAuthn](https://webauthn.guide/)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiosazee%2Fexpo-passkey","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fiosazee%2Fexpo-passkey","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiosazee%2Fexpo-passkey/lists"}