diff --git a/README.md b/README.md
index 90f40b1..aa396cd 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ React Native Credentials Manager

-A React Native library that implements the [Credential Manager](https://developer.android.com/identity/sign-in/credential-manager) API for Android. This library allows you to manage passwords, passkeys and google signin in your React Native applications.
+A React Native library that implements the [Credential Manager](https://developer.android.com/identity/sign-in/credential-manager) API for Android and [AuthenticationServices](https://developer.apple.com/documentation/authenticationservices) for iOS. This library allows you to manage passwords, passkeys and platform-specific sign-in (Google Sign-In on Android, Apple Sign In on iOS) in your React Native applications.
@@ -18,5 +18,196 @@ A React Native library that implements the [Credential Manager](https://develope
+## Platform Support
+
+- ✅ **Android**: Full implementation with Credential Manager API (Android 14+ / API 34+)
+- ✅ **iOS**: Full implementation with AuthenticationServices (iOS 16.0+)
+
+### Platform-Specific Features
+
+| Feature | Android | iOS |
+| ------------------------- | ------------------------- | --------------------------------- |
+| Passkeys | ✅ Credential Manager API | ✅ AuthenticationServices |
+| AutoFill Password Support | ✅ Credential Manager API | ✅ AuthenticationServices |
+| Manual Password Storage | ✅ Credential Manager API | ❌ Not supported (iOS limitation) |
+| Third-party Sign In | ✅ Google Sign In | ✅ Apple Sign In |
+
+> [!NOTE] > **iOS Implementation**: This library strictly follows Apple's Authentication Services framework. Manual password storage is not supported on iOS as it's not part of Apple's official Authentication Services APIs. Use AutoFill passwords instead.
+
> [!IMPORTANT]
> 📚 **Documentation has moved!** The complete documentation is now available at [https://docs.benjamineruvieru.com/docs/react-native-credentials-manager/](https://docs.benjamineruvieru.com/docs/react-native-credentials-manager/)
+
+## Platform-Specific Parameters
+
+When using this library, be aware that some parameters are platform-specific:
+
+| Function | Parameter | Platform Support | Notes |
+| ---------------------- | --------------------------------------- | ---------------- | ----------------------------------------------------------------- |
+| `signUpWithPasskeys()` | `preferImmediatelyAvailableCredentials` | Android only | This parameter is ignored on iOS |
+| `signUpWithPassword()` | All parameters | Android only | This function is not supported on iOS and will throw an error |
+| `signIn()` | `googleSignIn` | Android only | This parameter is ignored on iOS |
+| `signIn()` | `appleSignIn` | iOS only | This parameter is ignored on Android |
+| `signUpWithGoogle()` | All parameters | Cross-platform | Uses Google Sign-In on Android, Apple Sign-In on iOS |
+| `signUpWithApple()` | All parameters | iOS only | This function is not supported on Android and will throw an error |
+
+### Handling Platform Differences
+
+To handle these platform differences, you can use conditional code:
+
+```typescript
+// For passkey registration
+await signUpWithPasskeys(
+ requestJson,
+ Platform.OS === 'android' ? true : false // preferImmediatelyAvailableCredentials
+);
+
+// For sign-in
+await signIn(
+ [
+ 'passkeys',
+ 'password',
+ Platform.OS === 'android' ? 'google-signin' : 'apple-signin',
+ ],
+ {
+ passkeys: passkeyParams,
+ ...(Platform.OS === 'android'
+ ? { googleSignIn: { serverClientId: 'your-client-id' } }
+ : { appleSignIn: { requestedScopes: ['fullName', 'email'] } }),
+ }
+);
+```
+
+## iOS Setup Requirements
+
+### 1. Associated Domains
+
+Add the Associated Domains capability to your iOS app:
+
+1. In Xcode, select your project
+2. Go to Signing & Capabilities
+3. Add "Associated Domains" capability
+4. Add your domain with the `webcredentials` service: `webcredentials:yourdomain.com`
+
+### 2. Apple App Site Association (AASA)
+
+Ensure your domain has a proper AASA file at `https://yourdomain.com/.well-known/apple-app-site-association`:
+
+```json
+{
+ "webcredentials": {
+ "apps": ["TEAMID.com.yourcompany.yourapp"]
+ }
+}
+```
+
+### 3. Apple Sign In Setup (Optional)
+
+If using Apple Sign In, configure it in your Apple Developer account:
+
+1. Enable "Sign In with Apple" capability in Xcode
+2. Configure Sign In with Apple in your Apple Developer account
+3. Add your app's bundle identifier to the Sign In with Apple configuration
+
+## Quick Example
+
+```typescript
+import {
+ signUpWithPasskeys,
+ signUpWithPassword, // Android only - throws error on iOS
+ signUpWithGoogle, // Cross-platform: Google on Android, Apple on iOS
+ signUpWithApple, // iOS-specific function
+ signIn,
+ signOut,
+ type Credential,
+ type AppleCredential,
+ type GoogleCredential,
+ type AppleSignInParams,
+ type GoogleSignInParams,
+} from 'react-native-credentials-manager';
+
+// Sign up with passkey (works on both platforms)
+const passkeyResult = await signUpWithPasskeys({
+ challenge: 'base64-challenge',
+ rp: { name: 'Your App', id: 'yourdomain.com' },
+ user: { id: 'user-id', name: 'username', displayName: 'User Name' },
+ // ... other WebAuthn options
+});
+
+// Password handling - platform differences
+try {
+ // This will work on Android but throw an error on iOS
+ await signUpWithPassword({ username: 'user', password: 'pass' });
+} catch (error) {
+ if (Platform.OS === 'ios') {
+ console.log(
+ 'Manual password storage not supported on iOS. Use AutoFill instead.'
+ );
+ }
+}
+
+// Unified sign in (supports passkeys, AutoFill passwords, and platform sign-in)
+const credential = await signIn(
+ ['passkeys', 'password', 'google-signin'], // 'google-signin' becomes 'apple-signin' on iOS
+ {
+ passkeys: { challenge: 'base64-challenge', rpId: 'yourdomain.com' },
+ googleSignIn: { serverClientId: 'your-client-id' },
+ }
+);
+
+// Platform-specific sign-up methods
+if (Platform.OS === 'ios') {
+ // Direct Apple Sign In (iOS only) - uses AuthenticationServices
+ const appleCredential = await signUpWithApple({
+ requestedScopes: ['fullName', 'email'],
+ });
+} else {
+ // Google Sign In (Android only)
+ const googleCredential = await signUpWithGoogle({
+ serverClientId: 'your-client-id',
+ });
+}
+
+// Cross-platform sign-up (automatically uses the appropriate method)
+const credential = await signUpWithGoogle({
+ serverClientId: 'your-client-id', // Used on Android, ignored on iOS
+});
+// Returns GoogleCredential on Android, AppleCredential on iOS
+
+// Handle different credential types
+if (credential.type === 'passkey') {
+ console.log('Passkey authentication:', credential.authenticationResponseJson);
+} else if (credential.type === 'password') {
+ console.log('AutoFill Password:', credential.username, credential.password);
+} else if (credential.type === 'google-signin') {
+ console.log('Google Sign In:', credential.idToken);
+} else if (credential.type === 'apple-signin') {
+ console.log('Apple Sign In:', credential.idToken, credential.email);
+}
+```
+
+## Cross-Platform Compatibility
+
+The library provides excellent cross-platform compatibility while respecting platform limitations:
+
+- **`signUpWithGoogle()`**: Cross-platform function
+ - Android: Uses Google Sign In
+ - iOS: Automatically uses Apple Sign In (AuthenticationServices)
+- **`signUpWithApple()`**: iOS-specific function
+ - iOS: Uses Apple Sign In (AuthenticationServices)
+ - Android: Rejects with clear error message
+- **`signUpWithPassword()`**: Platform-specific behavior
+ - Android: Uses Credential Manager API
+ - iOS: Rejects (manual storage not supported by AuthenticationServices)
+- **AutoFill Passwords**: Available on both platforms through `signIn` method
+- **Error handling**: Platform-specific errors are handled gracefully
+
+## iOS AuthenticationServices Integration
+
+The iOS implementation strictly follows Apple's Authentication Services framework:
+
+- **Passkeys**: `ASAuthorizationPlatformPublicKeyCredentialProvider`
+- **AutoFill Passwords**: `ASAuthorizationPasswordProvider`
+- **Apple Sign In**: `ASAuthorizationAppleIDProvider`
+- **No Custom Keychain**: Manual credential storage is handled by the system
+
+This ensures compliance with Apple's security guidelines and provides the best user experience on iOS.
diff --git a/android/build.gradle b/android/build.gradle
index ba723fb..d489e47 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -100,11 +100,12 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
dependencies {
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
- implementation "androidx.credentials:credentials:1.5.0-rc01"
- implementation "androidx.credentials:credentials-play-services-auth:1.5.0-rc01"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
- implementation "com.google.android.libraries.identity.googleid:googleid:1.1.1"
+ implementation("androidx.credentials:credentials:1.6.0-alpha02")
+
+ implementation("androidx.credentials:credentials-play-services-auth:1.6.0-alpha02")
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
+ implementation "com.google.android.libraries.identity.googleid:googleid:1.1.1"
}
if (isNewArchitectureEnabled()) {
diff --git a/android/src/main/java/com/credentialsmanager/handlers/CredentialHandler.kt b/android/src/main/java/com/credentialsmanager/handlers/CredentialHandler.kt
index 7340f6d..ba2f815 100644
--- a/android/src/main/java/com/credentialsmanager/handlers/CredentialHandler.kt
+++ b/android/src/main/java/com/credentialsmanager/handlers/CredentialHandler.kt
@@ -36,26 +36,33 @@ class CredentialHandler(
jsonString: String,
preferImmediatelyAvailableCredentials: Boolean,
): ReadableMap? {
- val request =
- CreatePublicKeyCredentialRequest(
- requestJson = jsonString,
- preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
- )
-
- val response =
- credentialManager.createCredential(
- context,
- request,
- ) as CreatePublicKeyCredentialResponse
-
- return response.data.getString("androidx.credentials.BUNDLE_KEY_REGISTRATION_RESPONSE_JSON")?.let { json ->
- val jsonObject = Arguments.createMap()
- val parsedObject = JSONObject(json)
-
- parsedObject.keys().forEach { key ->
- jsonObject.putString(key, parsedObject.getString(key))
+ Log.d("CredentialManager", "Creating passkey with request: $jsonString")
+
+ try {
+ val request =
+ CreatePublicKeyCredentialRequest(
+ requestJson = jsonString,
+ preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
+ )
+
+ val response =
+ credentialManager.createCredential(
+ context,
+ request,
+ ) as CreatePublicKeyCredentialResponse
+
+ return response.data.getString("androidx.credentials.BUNDLE_KEY_REGISTRATION_RESPONSE_JSON")?.let { json ->
+ val jsonObject = Arguments.createMap()
+ val parsedObject = JSONObject(json)
+
+ parsedObject.keys().forEach { key ->
+ jsonObject.putString(key, parsedObject.getString(key))
+ }
+ jsonObject
}
- jsonObject
+ } catch (e: Exception) {
+ Log.e("CredentialManager", "Error creating passkey", e)
+ throw e
}
}
@@ -63,8 +70,15 @@ class CredentialHandler(
username: String,
password: String,
) {
- val createPasswordRequest = CreatePasswordRequest(id = username, password = password)
- credentialManager.createCredential(context, createPasswordRequest)
+ Log.d("CredentialManager", "Creating password credential for username: $username")
+
+ try {
+ val createPasswordRequest = CreatePasswordRequest(id = username, password = password)
+ credentialManager.createCredential(context, createPasswordRequest)
+ } catch (e: Exception) {
+ Log.e("CredentialManager", "Error creating password credential", e)
+ throw e
+ }
}
suspend fun signIn(
diff --git a/android/src/newarch/java/com/credentialsmanager/CredentialsManagerModule.kt b/android/src/newarch/java/com/credentialsmanager/CredentialsManagerModule.kt
index 2f133e1..7ea1bf7 100644
--- a/android/src/newarch/java/com/credentialsmanager/CredentialsManagerModule.kt
+++ b/android/src/newarch/java/com/credentialsmanager/CredentialsManagerModule.kt
@@ -49,14 +49,34 @@ class CredentialsManagerModule(
}
}
- override fun signUpWithPassword(credObject: ReadableMap) {
+ override fun signUpWithPassword(credObject: ReadableMap, promise: Promise) {
val username = credObject.getString("username") ?: ""
val password = credObject.getString("password") ?: ""
+
+ if (username.isEmpty()) {
+ promise.reject("INVALID_USERNAME", "Username cannot be empty")
+ return
+ }
+
+ if (password.isEmpty()) {
+ promise.reject("INVALID_PASSWORD", "Password cannot be empty")
+ return
+ }
+
coroutineScope.launch {
try {
credentialHandler.createPassword(username, password)
+
+ // Create success response
+ val result = mapOf(
+ "type" to "password",
+ "username" to username,
+ "success" to true
+ )
+ promise.resolve(result)
} catch (e: CreateCredentialException) {
ErrorHandler.handleCredentialError(e)
+ promise.reject("CREDENTIAL_ERROR", e.message.toString())
}
}
}
@@ -140,4 +160,11 @@ class CredentialsManagerModule(
}
}
}
+
+ override fun signUpWithApple(params: ReadableMap, promise: Promise) {
+ promise.reject(
+ "PLATFORM_NOT_SUPPORTED",
+ "Sign up with Apple is only supported on iOS devices"
+ )
+ }
}
diff --git a/android/src/oldarch/java/com/credentialsmanager/CredentialsManagerModule.kt b/android/src/oldarch/java/com/credentialsmanager/CredentialsManagerModule.kt
index 2ad676f..a7ac8f5 100644
--- a/android/src/oldarch/java/com/credentialsmanager/CredentialsManagerModule.kt
+++ b/android/src/oldarch/java/com/credentialsmanager/CredentialsManagerModule.kt
@@ -55,14 +55,34 @@ class CredentialsManagerModule(
}
@ReactMethod
- fun signUpWithPassword(credObject: ReadableMap) {
+ fun signUpWithPassword(credObject: ReadableMap, promise: Promise) {
val username = credObject.getString("username") ?: ""
val password = credObject.getString("password") ?: ""
+
+ if (username.isEmpty()) {
+ promise.reject("INVALID_USERNAME", "Username cannot be empty")
+ return
+ }
+
+ if (password.isEmpty()) {
+ promise.reject("INVALID_PASSWORD", "Password cannot be empty")
+ return
+ }
+
coroutineScope.launch {
try {
credentialHandler.createPassword(username, password)
+
+ // Create success response
+ val result = mapOf(
+ "type" to "password",
+ "username" to username,
+ "success" to true
+ )
+ promise.resolve(result)
} catch (e: CreateCredentialException) {
ErrorHandler.handleCredentialError(e)
+ promise.reject("CREDENTIAL_ERROR", e.message.toString())
}
}
}
@@ -161,4 +181,13 @@ class CredentialsManagerModule(
}
}
}
+
+ @ReactMethod
+ fun signUpWithApple(params: ReadableMap, promise: Promise) {
+ // Since this is an iOS-specific function, we just reject with an appropriate message on Android
+ promise.reject(
+ "PLATFORM_NOT_SUPPORTED",
+ "Sign up with Apple is only supported on iOS devices"
+ )
+ }
}
diff --git a/example/ios/.xcode.env.local b/example/ios/.xcode.env.local
index 9726762..cc1ba63 100644
--- a/example/ios/.xcode.env.local
+++ b/example/ios/.xcode.env.local
@@ -1 +1 @@
-export NODE_BINARY=/Users/mac/.nvm/versions/node/v20.14.0/bin/node
+export NODE_BINARY=/opt/homebrew/Cellar/node/23.11.0/bin/node
diff --git a/example/ios/CredentialsManagerExample.xcodeproj/project.pbxproj b/example/ios/CredentialsManagerExample.xcodeproj/project.pbxproj
index 84e87b2..429eb0a 100644
--- a/example/ios/CredentialsManagerExample.xcodeproj/project.pbxproj
+++ b/example/ios/CredentialsManagerExample.xcodeproj/project.pbxproj
@@ -24,6 +24,7 @@
5DCACB8F33CDC322A6C60F78 /* libPods-CredentialsManagerExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-CredentialsManagerExample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = CredentialsManagerExample/AppDelegate.swift; sourceTree = ""; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = CredentialsManagerExample/LaunchScreen.storyboard; sourceTree = ""; };
+ B546FE0F2DE77843007A8E3F /* CredentialsManagerExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = CredentialsManagerExample.entitlements; path = CredentialsManagerExample/CredentialsManagerExample.entitlements; sourceTree = ""; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
@@ -42,6 +43,7 @@
13B07FAE1A68108700A75B9A /* CredentialsManagerExample */ = {
isa = PBXGroup;
children = (
+ B546FE0F2DE77843007A8E3F /* CredentialsManagerExample.entitlements */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
761780EC2CA45674006654EE /* AppDelegate.swift */,
13B07FB61A68108700A75B9A /* Info.plist */,
@@ -259,7 +261,9 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = CredentialsManagerExample/CredentialsManagerExample.entitlements;
CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = UDVM22XGUG;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = CredentialsManagerExample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
@@ -273,7 +277,7 @@
"-ObjC",
"-lc++",
);
- PRODUCT_BUNDLE_IDENTIFIER = credentialsmanager.example;
+ PRODUCT_BUNDLE_IDENTIFIER = com.jobpro.id;
PRODUCT_NAME = CredentialsManagerExample;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -287,7 +291,9 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = CredentialsManagerExample/CredentialsManagerExample.entitlements;
CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = UDVM22XGUG;
INFOPLIST_FILE = CredentialsManagerExample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
@@ -300,7 +306,7 @@
"-ObjC",
"-lc++",
);
- PRODUCT_BUNDLE_IDENTIFIER = credentialsmanager.example;
+ PRODUCT_BUNDLE_IDENTIFIER = com.jobpro.id;
PRODUCT_NAME = CredentialsManagerExample;
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -376,10 +382,7 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
- OTHER_LDFLAGS = (
- "$(inherited)",
- " ",
- );
+ OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -448,10 +451,7 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
- OTHER_LDFLAGS = (
- "$(inherited)",
- " ",
- );
+ OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
diff --git a/example/ios/CredentialsManagerExample/CredentialsManagerExample.entitlements b/example/ios/CredentialsManagerExample/CredentialsManagerExample.entitlements
new file mode 100644
index 0000000..b0cfae8
--- /dev/null
+++ b/example/ios/CredentialsManagerExample/CredentialsManagerExample.entitlements
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.developer.applesignin
+
+ Default
+
+ com.apple.developer.associated-domains
+
+ webcredentials:www.benjamineruvieru.com
+
+
+
diff --git a/example/ios/CredentialsManagerExample/Info.plist b/example/ios/CredentialsManagerExample/Info.plist
index a16852a..ae2ceda 100644
--- a/example/ios/CredentialsManagerExample/Info.plist
+++ b/example/ios/CredentialsManagerExample/Info.plist
@@ -26,7 +26,6 @@
NSAppTransportSecurity
-
NSAllowsArbitraryLoads
NSAllowsLocalNetworking
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 9d0588e..31a80bd 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1210,6 +1210,27 @@ PODS:
- React-jsiexecutor
- React-RCTFBReactNativeSpec
- ReactCommon/turbomodule/core
+ - react-native-credentials-manager (0.5.3):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
- React-nativeconfig (0.77.0)
- React-NativeModulesApple (0.77.0):
- glog
@@ -1541,6 +1562,7 @@ DEPENDENCIES:
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
+ - react-native-credentials-manager (from `../..`)
- React-nativeconfig (from `../node_modules/react-native/ReactCommon`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
@@ -1650,6 +1672,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon"
React-microtasksnativemodule:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
+ react-native-credentials-manager:
+ :path: "../.."
React-nativeconfig:
:path: "../node_modules/react-native/ReactCommon"
React-NativeModulesApple:
@@ -1719,7 +1743,7 @@ SPEC CHECKSUMS:
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8
hermes-engine: 1f783c3d53940aed0d2c84586f0b7a85ab7827ef
- RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
+ RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809
RCTDeprecation: f5c19ebdb8804b53ed029123eb69914356192fc8
RCTRequired: 6ae6cebe470486e0e0ce89c1c0eabb998e7c51f4
RCTTypeSafety: 50d6ec72a3d13cf77e041ff43a0617050fb98e3f
@@ -1748,6 +1772,7 @@ SPEC CHECKSUMS:
React-logger: 9a0c4e1e41cd640ac49d69aacadab783f7e0096b
React-Mapbuffer: 6993c785c22a170c02489bc78ed207814cbd700f
React-microtasksnativemodule: 19230cd0933df6f6dc1336c9a9edc382d62638ae
+ react-native-credentials-manager: df7d44c3d21d83d5a67bddaf071119361da333f7
React-nativeconfig: cd0fbb40987a9658c24dab5812c14e5522a64929
React-NativeModulesApple: 45187d13c68d47250a7416b18ff082c7cc07bff7
React-perflogger: 15a7bcb6c46eae8a981f7add8c9f4172e2372324
@@ -1778,8 +1803,8 @@ SPEC CHECKSUMS:
ReactCodegen: 1baa534318b19e95fb0f02db0a1ae1e3c271944d
ReactCommon: 6014af4276bb2debc350e2620ef1bd856b4d981c
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
- Yoga: c0d8564af14a858f962607cd7306539cb2ace926
+ Yoga: 78d74e245ed67bb94275a1316cdc170b9b7fe884
PODFILE CHECKSUM: 04ab7ab32404c56cc432a0131cd18e84b514cd04
-COCOAPODS: 1.15.2
+COCOAPODS: 1.16.2
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 0dfcda0..8a4d298 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -1,51 +1,18 @@
-import { View, StyleSheet, Button } from 'react-native';
+import { View, StyleSheet, Button, Platform } from 'react-native';
import {
signUpWithPasskeys,
signUpWithPassword,
signUpWithGoogle,
+ signUpWithApple,
signOut,
signIn,
} from 'react-native-credentials-manager';
-const WEB_CLIENT_ID = process.env.WEB_CLIENT_ID || '';
-
-const requestJson = {
- challenge: 'c29tZS1yYW5kb20tY2hhbGxlbmdl',
- rp: {
- name: 'CredentialsManagerExample',
- id: 'www.benjamineruvieru.com',
- },
- user: {
- id: 'dXNlcl9pZF8xMjM0NTY=',
- name: 'johndoe',
- displayName: 'John Doe',
- },
- pubKeyCredParams: [
- {
- type: 'public-key',
- alg: -7,
- },
- {
- type: 'public-key',
- alg: -257,
- },
- ],
- timeout: 1800000,
- attestation: 'none',
- excludeCredentials: [],
- authenticatorSelection: {
- authenticatorAttachment: 'platform',
- requireResidentKey: true,
- residentKey: 'required',
- userVerification: 'required',
- },
-};
+import {
+ generateTestRegistrationRequest,
+ generateTestAuthenticationRequest,
+} from './helpers/passkeyTestHelper';
-const signinPasskeysRequestJson = {
- challenge: 'HjBbH__fbLuzy95AGR31yEARA0EMtKlY0NrV5oy3NQw',
- timeout: 1800000,
- userVerification: 'required',
- rpId: 'www.benjamineruvieru.com',
-};
+const WEB_CLIENT_ID = process.env.WEB_CLIENT_ID || '';
export default function App() {
return (
@@ -54,7 +21,9 @@ export default function App() {
title="Signup With Passkey"
onPress={async () => {
try {
- const res = await signUpWithPasskeys(requestJson);
+ // Use the helper to generate a valid registration request
+ const validRequest = generateTestRegistrationRequest();
+ const res = await signUpWithPasskeys(validRequest);
console.log(JSON.stringify(res));
console.log(res);
} catch (e) {
@@ -62,24 +31,40 @@ export default function App() {
}
}}
/>
-
- signUpWithPassword({ username: 'User1', password: 'Password123!' })
- }
- />
+ {Platform.OS === 'android' && (
+ {
+ try {
+ const result = await signUpWithPassword({
+ username: 'User1',
+ password: 'Password123!',
+ });
+ console.log('Password registration result:', result);
+ } catch (e) {
+ console.error('Password registration error:', e);
+ }
+ }}
+ />
+ )}
{
try {
+ // Use the helper to generate a valid authentication request
+ const validAuthRequest = generateTestAuthenticationRequest();
+
const credential = await signIn(
- ['passkeys', 'password', 'google-signin'],
+ ['passkeys', 'password', 'apple-signin'],
{
- passkeys: signinPasskeysRequestJson,
+ passkeys: validAuthRequest,
googleSignIn: {
serverClientId: WEB_CLIENT_ID,
autoSelectEnabled: true,
},
+ appleSignIn: {
+ requestedScopes: ['fullName', 'email'],
+ },
}
);
@@ -90,8 +75,7 @@ export default function App() {
username: credential.username,
password: credential.password,
});
- }
- if (credential.type === 'google-signin') {
+ } else if (credential.type === 'google-signin') {
console.log('Google credentials:', {
id: credential.id,
idToken: credential.idToken,
@@ -101,6 +85,15 @@ export default function App() {
profilePicture: credential.profilePicture,
phoneNumber: credential.phoneNumber,
});
+ } else if (credential.type === 'apple-signin') {
+ console.log('Apple credentials:', {
+ id: credential.id,
+ idToken: credential.idToken,
+ displayName: credential.displayName,
+ familyName: credential.familyName,
+ givenName: credential.givenName,
+ email: credential.email,
+ });
}
} catch (e) {
console.error(e);
@@ -108,40 +101,68 @@ export default function App() {
}}
/>
- {
- try {
- const credential = await signUpWithGoogle({
- serverClientId: WEB_CLIENT_ID,
- autoSelectEnabled: true,
- });
- if (credential.type === 'google-signin') {
- console.log('Google credentials:', {
+ {Platform.OS === 'android' && (
+ {
+ try {
+ const credential = await signUpWithGoogle({
+ serverClientId: WEB_CLIENT_ID,
+ autoSelectEnabled: true,
+ });
+ if (credential.type === 'google-signin') {
+ console.log('Google credentials:', {
+ id: credential.id,
+ idToken: credential.idToken,
+ displayName: credential.displayName,
+ familyName: credential.familyName,
+ givenName: credential.givenName,
+ profilePicture: credential.profilePicture,
+ phoneNumber: credential.phoneNumber,
+ });
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }}
+ />
+ )}
+
+ {Platform.OS === 'ios' && (
+ {
+ try {
+ const credential = await signUpWithApple({
+ requestedScopes: ['fullName', 'email'],
+ });
+ console.log('Apple credentials:', {
id: credential.id,
idToken: credential.idToken,
displayName: credential.displayName,
familyName: credential.familyName,
givenName: credential.givenName,
- profilePicture: credential.profilePicture,
- phoneNumber: credential.phoneNumber,
+ email: credential.email,
});
+ } catch (e) {
+ console.error(e);
}
- } catch (e) {
- console.error(e);
- }
- }}
- />
- {
- try {
- await signOut();
- } catch (e) {
- console.error(e);
- }
- }}
- />
+ }}
+ />
+ )}
+
+ {Platform.OS === 'android' && (
+ {
+ try {
+ await signOut();
+ } catch (e) {
+ console.error(e);
+ }
+ }}
+ />
+ )}
);
}
diff --git a/example/src/helpers/passkeyTestHelper.ts b/example/src/helpers/passkeyTestHelper.ts
new file mode 100644
index 0000000..15c1142
--- /dev/null
+++ b/example/src/helpers/passkeyTestHelper.ts
@@ -0,0 +1,89 @@
+/**
+ * Generates a proper base64 challenge string
+ * @returns A valid base64-encoded challenge string
+ */
+export function generateValidChallenge(): string {
+ // Generate random bytes
+ const randomBytes = new Uint8Array(32);
+ for (let i = 0; i < randomBytes.length; i++) {
+ randomBytes[i] = Math.floor(Math.random() * 256);
+ }
+
+ // Convert to base64
+ return bytesToBase64(randomBytes);
+}
+
+/**
+ * Convert Uint8Array to base64 string
+ */
+function bytesToBase64(bytes: Uint8Array): string {
+ const binString = Array.from(bytes)
+ .map((byte) => String.fromCharCode(byte))
+ .join('');
+
+ return btoa(binString);
+}
+
+/**
+ * Generate a valid RP ID for testing
+ */
+export function getTestRpId(): string {
+ return 'www.benjamineruvieru.com';
+}
+
+/**
+ * Generate a valid registration request for testing
+ */
+export function generateTestRegistrationRequest(
+ username: string = 'testuser'
+): any {
+ const userId = bytesToBase64(
+ new TextEncoder().encode(
+ `user_id_${Math.random().toString(36).substring(2, 15)}`
+ )
+ );
+
+ return {
+ challenge: generateValidChallenge(),
+ rp: {
+ name: 'Test App',
+ id: getTestRpId(),
+ },
+ user: {
+ id: userId,
+ name: username,
+ displayName: username,
+ },
+ pubKeyCredParams: [
+ {
+ type: 'public-key',
+ alg: -7, // ES256
+ },
+ {
+ type: 'public-key',
+ alg: -257, // RS256
+ },
+ ],
+ timeout: 60000, // 1 minute is usually enough for testing
+ attestation: 'none',
+ excludeCredentials: [],
+ authenticatorSelection: {
+ authenticatorAttachment: 'platform',
+ requireResidentKey: true,
+ residentKey: 'required',
+ userVerification: 'required',
+ },
+ };
+}
+
+/**
+ * Generate a valid authentication request for testing
+ */
+export function generateTestAuthenticationRequest(): any {
+ return {
+ challenge: generateValidChallenge(),
+ timeout: 60000,
+ userVerification: 'required',
+ rpId: getTestRpId(),
+ };
+}
diff --git a/ios/CredentialsManager.h b/ios/CredentialsManager.h
index dc94f52..f9bb590 100644
--- a/ios/CredentialsManager.h
+++ b/ios/CredentialsManager.h
@@ -1,6 +1,12 @@
-
+#import
+#import
#import "generated/RNCredentialsManagerSpec/RNCredentialsManagerSpec.h"
-@interface CredentialsManager : NSObject
+@interface CredentialsManager : NSObject
+
+@property (nonatomic, strong) ASPresentationAnchor authenticationAnchor;
+@property (nonatomic, copy) RCTPromiseResolveBlock currentResolve;
+@property (nonatomic, copy) RCTPromiseRejectBlock currentReject;
+@property (nonatomic, strong) NSString *relyingPartyIdentifier;
@end
diff --git a/ios/CredentialsManager.mm b/ios/CredentialsManager.mm
index fbbfbca..d6223b8 100644
--- a/ios/CredentialsManager.mm
+++ b/ios/CredentialsManager.mm
@@ -1,18 +1,410 @@
#import "CredentialsManager.h"
+#import
+#import
+#import
@implementation CredentialsManager
-RCT_EXPORT_MODULE()
-- (NSNumber *)multiply:(double)a b:(double)b {
- NSNumber *result = @(a * b);
+RCT_EXPORT_MODULE()
- return result;
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ // Default relying party identifier - should be configurable
+ self.relyingPartyIdentifier = @"www.benjamineruvieru.com";
+
+ // Get the main window as the authentication anchor
+ dispatch_async(dispatch_get_main_queue(), ^{
+ UIWindow *keyWindow = nil;
+ for (UIWindowScene *windowScene in [UIApplication sharedApplication].connectedScenes) {
+ if (windowScene.activationState == UISceneActivationStateForegroundActive) {
+ for (UIWindow *window in windowScene.windows) {
+ if (window.isKeyWindow) {
+ keyWindow = window;
+ break;
+ }
+ }
+ }
+ }
+ self.authenticationAnchor = keyWindow;
+ });
+ }
+ return self;
}
+#pragma mark - TurboModule
+
- (std::shared_ptr)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared(params);
}
+#pragma mark - NativeCredentialsManagerSpec
+
+- (void)signUpWithPasskeys:(NSDictionary *)requestJson
+preferImmediatelyAvailableCredentials:(BOOL)preferImmediatelyAvailableCredentials
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject {
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ self.currentResolve = resolve;
+ self.currentReject = reject;
+
+ // Extract challenge and user info from requestJson
+ NSString *challengeString = requestJson[@"challenge"];
+ NSDictionary *userInfo = requestJson[@"user"];
+ NSDictionary *rpInfo = requestJson[@"rp"];
+
+ if (!challengeString || !userInfo || !rpInfo) {
+ reject(@"INVALID_REQUEST", @"Missing required fields in request JSON", nil);
+ return;
+ }
+
+ // Decode base64 challenge
+ NSData *challenge = [[NSData alloc] initWithBase64EncodedString:challengeString options:0];
+ if (!challenge) {
+ reject(@"INVALID_CHALLENGE", @"Invalid base64 challenge", nil);
+ return;
+ }
+
+ // Extract user information
+ NSString *userName = userInfo[@"name"];
+ NSString *userIdString = userInfo[@"id"];
+
+ // Decode user ID
+ NSData *userId = [[NSData alloc] initWithBase64EncodedString:userIdString options:0];
+ if (!userId) {
+ // If not base64, use the string directly
+ userId = [userIdString dataUsingEncoding:NSUTF8StringEncoding];
+ }
+
+ // Update relying party identifier
+ NSString *rpId = rpInfo[@"id"];
+ if (rpId) {
+ self.relyingPartyIdentifier = rpId;
+ }
+
+ // Create the passkey registration request using Apple's Authentication Services
+ ASAuthorizationPlatformPublicKeyCredentialProvider *provider =
+ [[ASAuthorizationPlatformPublicKeyCredentialProvider alloc] initWithRelyingPartyIdentifier:self.relyingPartyIdentifier];
+
+ ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest *registrationRequest =
+ [provider createCredentialRegistrationRequestWithChallenge:challenge name:userName userID:userId];
+
+ // Configure the request
+ registrationRequest.userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreferenceRequired;
+
+ // Create and configure the authorization controller
+ ASAuthorizationController *authController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[registrationRequest]];
+ authController.delegate = self;
+ authController.presentationContextProvider = self;
+
+ // Perform the request
+ [authController performRequests];
+ });
+}
+
+- (void)signUpWithPassword:(JS::NativeCredentialsManager::CredObject &)credObject
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject {
+ // Apple's Authentication Services only supports AutoFill passwords, not manual credential storage
+ // Manual keychain storage is not part of Authentication Services framework
+ reject(@"UNSUPPORTED_OPERATION", @"Manual password storage is not supported. Use AutoFill passwords through signIn method instead.", nil);
+}
+
+- (void)signIn:(NSArray *)options
+ params:(JS::NativeCredentialsManager::SpecSignInParams &)params
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject {
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ self.currentResolve = resolve;
+ self.currentReject = reject;
+
+ NSMutableArray *authRequests = [[NSMutableArray alloc] init];
+
+ for (NSString *option in options) {
+ if ([option isEqualToString:@"passkeys"]) {
+ // Extract passkeys parameters from the params object
+ id passkeyParams = params.passkeys();
+ if (!passkeyParams || ![passkeyParams isKindOfClass:[NSDictionary class]]) {
+ RCTLogError(@"Missing or invalid passkeys parameters");
+ continue;
+ }
+
+ NSDictionary *passkeyDict = (NSDictionary *)passkeyParams;
+ NSString *challengeString = passkeyDict[@"challenge"];
+
+ if (!challengeString || ![challengeString isKindOfClass:[NSString class]]) {
+ RCTLogError(@"Missing or invalid challenge in passkeys parameters");
+ continue;
+ }
+
+ NSData *challenge = [[NSData alloc] initWithBase64EncodedString:challengeString options:0];
+
+ if (challenge) {
+ // Update relying party identifier if provided
+ NSString *rpId = passkeyDict[@"rpId"];
+ if (rpId && [rpId isKindOfClass:[NSString class]]) {
+ self.relyingPartyIdentifier = rpId;
+ }
+
+ ASAuthorizationPlatformPublicKeyCredentialProvider *provider =
+ [[ASAuthorizationPlatformPublicKeyCredentialProvider alloc] initWithRelyingPartyIdentifier:self.relyingPartyIdentifier];
+
+ ASAuthorizationPlatformPublicKeyCredentialAssertionRequest *assertionRequest =
+ [provider createCredentialAssertionRequestWithChallenge:challenge];
+ assertionRequest.userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreferenceRequired;
+
+ [authRequests addObject:assertionRequest];
+ } else {
+ RCTLogError(@"Failed to decode challenge for passkey authentication");
+ }
+ } else if ([option isEqualToString:@"password"]) {
+ // Use Apple's AutoFill password provider (not manual keychain)
+ ASAuthorizationPasswordProvider *passwordProvider = [[ASAuthorizationPasswordProvider alloc] init];
+ ASAuthorizationPasswordRequest *passwordRequest = [passwordProvider createRequest];
+ [authRequests addObject:passwordRequest];
+ } else if ([option isEqualToString:@"apple-signin"]) {
+ // Apple Sign In is supported by Authentication Services
+ ASAuthorizationAppleIDProvider *appleIDProvider = [[ASAuthorizationAppleIDProvider alloc] init];
+ ASAuthorizationAppleIDRequest *appleIDRequest = [appleIDProvider createRequest];
+
+ // Default scopes - always use these
+ NSArray *defaultScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
+ appleIDRequest.requestedScopes = defaultScopes;
+
+ [authRequests addObject:appleIDRequest];
+ } else if ([option isEqualToString:@"google-signin"]) {
+ // Google Sign In is not part of Apple's Authentication Services framework
+ RCTLogError(@"Google Sign In is not supported on iOS. Use Apple Sign In instead.");
+ continue;
+ }
+ }
+
+ if (authRequests.count == 0) {
+ reject(@"NO_AUTH_METHODS", @"No valid authentication methods provided", nil);
+ return;
+ }
+
+ // Create and configure the authorization controller
+ ASAuthorizationController *authController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:authRequests];
+ authController.delegate = self;
+ authController.presentationContextProvider = self;
+
+ // Perform the request
+ [authController performRequests];
+ });
+}
+
+- (void)signUpWithGoogle:(JS::NativeCredentialsManager::GoogleSignInParams &)params
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject {
+ // Google Sign In is not part of Apple's Authentication Services framework
+ // This should be handled at the JavaScript layer for cross-platform compatibility
+ reject(@"UNSUPPORTED_OPERATION", @"Google Sign In is not available on iOS. Use Apple Sign In instead.", nil);
+}
+
+- (void)signUpWithApple:(JS::NativeCredentialsManager::AppleSignInParams &)params
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject {
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ self.currentResolve = resolve;
+ self.currentReject = reject;
+
+ // Apple Sign In is officially supported by Authentication Services framework
+ ASAuthorizationAppleIDProvider *appleIDProvider = [[ASAuthorizationAppleIDProvider alloc] init];
+ ASAuthorizationAppleIDRequest *appleIDRequest = [appleIDProvider createRequest];
+
+ // Always use the default scopes
+ appleIDRequest.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
+
+ ASAuthorizationController *authController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[appleIDRequest]];
+ authController.delegate = self;
+ authController.presentationContextProvider = self;
+
+ [authController performRequests];
+ });
+}
+
+- (void)signOut:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject {
+ // Apple's Authentication Services doesn't provide a direct sign-out method
+ // Sign-out is typically handled at the application level
+ // AutoFill passwords and passkeys are managed by the system
+ resolve([NSNull null]);
+}
+
+#pragma mark - ASAuthorizationControllerDelegate
+
+- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization {
+ if (!self.currentResolve) {
+ return;
+ }
+
+ RCTPromiseResolveBlock resolve = self.currentResolve;
+ self.currentReject = nil;
+ self.currentResolve = nil;
+
+ if ([authorization.credential isKindOfClass:[ASAuthorizationPlatformPublicKeyCredentialRegistration class]]) {
+ // Passkey registration - handled by Apple's Authentication Services
+ ASAuthorizationPlatformPublicKeyCredentialRegistration *registration = (ASAuthorizationPlatformPublicKeyCredentialRegistration *)authorization.credential;
+
+ NSDictionary *result = @{
+ @"id": registration.credentialID ? [registration.credentialID base64EncodedStringWithOptions:0] : @"",
+ @"rawId": registration.credentialID ? [registration.credentialID base64EncodedStringWithOptions:0] : @"",
+ @"response": @{
+ @"attestationObject": registration.rawAttestationObject ? [registration.rawAttestationObject base64EncodedStringWithOptions:0] : @"",
+ @"clientDataJSON": registration.rawClientDataJSON ? [registration.rawClientDataJSON base64EncodedStringWithOptions:0] : @""
+ },
+ @"type": @"public-key"
+ };
+
+ resolve(result);
+ return;
+ } else if ([authorization.credential isKindOfClass:[ASAuthorizationPlatformPublicKeyCredentialAssertion class]]) {
+ // Passkey authentication - handled by Apple's Authentication Services
+ ASAuthorizationPlatformPublicKeyCredentialAssertion *assertion = (ASAuthorizationPlatformPublicKeyCredentialAssertion *)authorization.credential;
+
+ NSDictionary *result = @{
+ @"type": @"passkey",
+ @"authenticationResponseJson": [self createAuthenticationResponseJSON:assertion]
+ };
+
+ resolve(result);
+ return;
+ } else if ([authorization.credential isKindOfClass:[ASPasswordCredential class]]) {
+ // AutoFill password authentication - handled by Apple's Authentication Services
+ ASPasswordCredential *passwordCredential = (ASPasswordCredential *)authorization.credential;
+
+ NSDictionary *result = @{
+ @"type": @"password",
+ @"username": passwordCredential.user,
+ @"password": passwordCredential.password
+ };
+
+ resolve(result);
+ return;
+ } else if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
+ // Apple Sign In - officially supported by Authentication Services
+ ASAuthorizationAppleIDCredential *appleIDCredential = (ASAuthorizationAppleIDCredential *)authorization.credential;
+
+ NSMutableDictionary *result = [@{
+ @"type": @"apple-signin",
+ @"id": appleIDCredential.user,
+ @"idToken": appleIDCredential.identityToken ? [[NSString alloc] initWithData:appleIDCredential.identityToken encoding:NSUTF8StringEncoding] : @""
+ } mutableCopy];
+
+ if (appleIDCredential.fullName) {
+ if (appleIDCredential.fullName.givenName) {
+ result[@"givenName"] = appleIDCredential.fullName.givenName;
+ }
+ if (appleIDCredential.fullName.familyName) {
+ result[@"familyName"] = appleIDCredential.fullName.familyName;
+ }
+ if (appleIDCredential.fullName.givenName || appleIDCredential.fullName.familyName) {
+ NSString *fullName = [NSString stringWithFormat:@"%@ %@",
+ appleIDCredential.fullName.givenName ?: @"",
+ appleIDCredential.fullName.familyName ?: @""];
+ result[@"displayName"] = [fullName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
+ }
+ }
+
+ if (appleIDCredential.email) {
+ result[@"email"] = appleIDCredential.email;
+ }
+
+ resolve([result copy]);
+ return;
+ }
+
+ // If we reach here, we couldn't handle the credential type
+ resolve(@{@"type": @"unknown"});
+}
+
+- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error {
+ if (!self.currentReject) {
+ return;
+ }
+
+ RCTPromiseRejectBlock reject = self.currentReject;
+ self.currentResolve = nil;
+ self.currentReject = nil;
+
+ NSString *errorCode = @"UNKNOWN_ERROR";
+ NSString *errorMessage = error.localizedDescription;
+
+ if ([error.domain isEqualToString:ASAuthorizationErrorDomain]) {
+ switch (error.code) {
+ case ASAuthorizationErrorCanceled:
+ errorCode = @"USER_CANCELED";
+ errorMessage = @"User canceled the authorization request";
+ break;
+ case ASAuthorizationErrorFailed:
+ errorCode = @"AUTHORIZATION_FAILED";
+ break;
+ case ASAuthorizationErrorInvalidResponse:
+ errorCode = @"INVALID_RESPONSE";
+ break;
+ case ASAuthorizationErrorNotHandled:
+ errorCode = @"NOT_HANDLED";
+ break;
+ case ASAuthorizationErrorUnknown:
+ errorCode = @"UNKNOWN_ERROR";
+ break;
+ }
+ }
+
+ reject(errorCode, errorMessage, error);
+}
+
+#pragma mark - ASAuthorizationControllerPresentationContextProviding
+
+- (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller {
+ if (self.authenticationAnchor) {
+ return self.authenticationAnchor;
+ }
+
+ // Fallback to finding a key window
+ UIWindow *keyWindow = nil;
+ for (UIWindowScene *windowScene in [UIApplication sharedApplication].connectedScenes) {
+ if (windowScene.activationState == UISceneActivationStateForegroundActive) {
+ for (UIWindow *window in windowScene.windows) {
+ if (window.isKeyWindow) {
+ keyWindow = window;
+ break;
+ }
+ }
+ }
+ }
+
+ return keyWindow ?: [[UIApplication sharedApplication] windows].firstObject;
+}
+
+#pragma mark - Helper Methods
+
+- (NSString *)createAuthenticationResponseJSON:(ASAuthorizationPlatformPublicKeyCredentialAssertion *)assertion {
+ NSDictionary *response = @{
+ @"id": assertion.credentialID ? [assertion.credentialID base64EncodedStringWithOptions:0] : @"",
+ @"rawId": assertion.credentialID ? [assertion.credentialID base64EncodedStringWithOptions:0] : @"",
+ @"response": @{
+ @"authenticatorData": assertion.rawAuthenticatorData ? [assertion.rawAuthenticatorData base64EncodedStringWithOptions:0] : @"",
+ @"clientDataJSON": assertion.rawClientDataJSON ? [assertion.rawClientDataJSON base64EncodedStringWithOptions:0] : @"",
+ @"signature": assertion.signature ? [assertion.signature base64EncodedStringWithOptions:0] : @"",
+ @"userHandle": assertion.userID ? [assertion.userID base64EncodedStringWithOptions:0] : @""
+ },
+ @"type": @"public-key"
+ };
+
+ NSError *error;
+ NSData *jsonData = [NSJSONSerialization dataWithJSONObject:response options:0 error:&error];
+ if (error) {
+ return @"{}";
+ }
+
+ return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
+}
+
@end
diff --git a/react-native-credentials-manager.podspec b/react-native-credentials-manager.podspec
index ad66fcc..8cf3b51 100644
--- a/react-native-credentials-manager.podspec
+++ b/react-native-credentials-manager.podspec
@@ -11,12 +11,15 @@ Pod::Spec.new do |s|
s.license = package["license"]
s.authors = package["author"]
- s.platforms = { :ios => min_ios_version_supported }
+ s.platforms = { :ios => "15.1" }
s.source = { :git => "https://github.com/benjamineruvieru/react-native-credentials-manager.git", :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm,cpp}"
s.private_header_files = "ios/generated/**/*.h"
+ # iOS frameworks for credential management
+ s.frameworks = 'AuthenticationServices', 'LocalAuthentication', 'Security'
+
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
if respond_to?(:install_modules_dependencies, true)
diff --git a/react-native.config.js b/react-native.config.js
index 1b17f34..21980d4 100644
--- a/react-native.config.js
+++ b/react-native.config.js
@@ -7,7 +7,6 @@ module.exports = {
android: {
cmakeListsPath: 'generated/jni/CMakeLists.txt',
},
- ios: null,
},
},
};
diff --git a/src/NativeCredentialsManager.ts b/src/NativeCredentialsManager.ts
index 97061d6..e965a52 100644
--- a/src/NativeCredentialsManager.ts
+++ b/src/NativeCredentialsManager.ts
@@ -1,18 +1,31 @@
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
-export type SignInOption = 'passkeys' | 'password' | 'google-signin';
+export type SignInOption =
+ | 'passkeys'
+ | 'password'
+ | 'google-signin'
+ | 'apple-signin';
+// Password authentication types
type CredObject = {
username: string;
password: string;
};
+type PasswordCredential = {
+ type: 'password';
+ username: string;
+ password: string;
+};
+
+// Passkey authentication types
type PasskeyCredential = {
type: 'passkey';
authenticationResponseJson: string;
};
+// Google Sign In types
type GoogleSignInParams = {
nonce: string;
serverClientId: string;
@@ -30,32 +43,78 @@ export type GoogleCredential = {
phoneNumber?: string;
};
-type PasswordCredential = {
- type: 'password';
- username: string;
- password: string;
+// Apple Sign In types
+type AppleSignInParams = {
+ nonce: string;
+ requestedScopes: string[];
};
+export type AppleCredential = {
+ type: 'apple-signin';
+ id: string;
+ idToken: string;
+ displayName?: string;
+ familyName?: string;
+ givenName?: string;
+ email?: string;
+};
+
+// Combined credential type
export type Credential =
| PasskeyCredential
| PasswordCredential
- | GoogleCredential;
+ | GoogleCredential
+ | AppleCredential;
+// Native module interface
export interface Spec extends TurboModule {
+ /**
+ * Sign up with passkeys (supported on both Android and iOS)
+ * @param requestJson WebAuthn request object
+ * @param preferImmediatelyAvailableCredentials Android-specific parameter, ignored on iOS
+ */
signUpWithPasskeys(
requestJson: Object,
preferImmediatelyAvailableCredentials: boolean
): Promise;
- signUpWithPassword(credObject: CredObject): void;
+ /**
+ * Sign up with password (Android only - not supported on iOS)
+ * iOS will reject with UNSUPPORTED_OPERATION error
+ */
+ signUpWithPassword(credObject: CredObject): Promise;
+
+ /**
+ * Sign in with various methods
+ * - 'passkeys': Supported on both platforms
+ * - 'password': Supported on both platforms (uses AutoFill)
+ * - 'google-signin': Android only
+ * - 'apple-signin': iOS only (not available on Android)
+ */
signIn(
options: SignInOption[],
params: {
passkeys?: Object;
- googleSignIn?: GoogleSignInParams;
+ googleSignIn?: GoogleSignInParams; // Used only on Android
+ appleSignIn?: AppleSignInParams; // Used only on iOS
}
): Promise;
+
+ /**
+ * Sign up with Google (Android-specific implementation)
+ */
signUpWithGoogle(params: GoogleSignInParams): Promise;
+
+ /**
+ * Sign up with Apple (iOS-specific implementation)
+ * Will reject with UNSUPPORTED_OPERATION on Android
+ */
+ signUpWithApple(params: AppleSignInParams): Promise;
+
+ /**
+ * Sign out (behavior varies by platform)
+ * On iOS, this is a no-op as AuthenticationServices doesn't provide a sign-out method
+ */
signOut(): Promise;
}
diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts
new file mode 100644
index 0000000..d8bc202
--- /dev/null
+++ b/src/__tests__/index.test.ts
@@ -0,0 +1,81 @@
+import {
+ signUpWithPasskeys,
+ signUpWithPassword,
+ signUpWithGoogle,
+ signUpWithApple,
+ signIn,
+ signOut,
+ type Credential,
+ type GoogleCredential,
+ type AppleCredential,
+ type SignInOption,
+ type GoogleSignInParams,
+ type AppleSignInParams,
+} from '../index';
+
+describe('react-native-credentials-manager', () => {
+ it('should export all required functions', () => {
+ expect(typeof signUpWithPasskeys).toBe('function');
+ expect(typeof signUpWithPassword).toBe('function');
+ expect(typeof signUpWithGoogle).toBe('function');
+ expect(typeof signUpWithApple).toBe('function');
+ expect(typeof signIn).toBe('function');
+ expect(typeof signOut).toBe('function');
+ });
+
+ it('should have correct TypeScript types', () => {
+ // Test that types are properly exported and can be used
+ const mockCredential: Credential = {
+ type: 'passkey',
+ authenticationResponseJson: 'mock-response',
+ };
+
+ const mockGoogleCredential: GoogleCredential = {
+ type: 'google-signin',
+ id: 'mock-id',
+ idToken: 'mock-token',
+ };
+
+ const mockAppleCredential: AppleCredential = {
+ type: 'apple-signin',
+ id: 'mock-id',
+ idToken: 'mock-token',
+ email: 'test@example.com',
+ };
+
+ const mockSignInOptions: SignInOption[] = [
+ 'passkeys',
+ 'password',
+ 'google-signin',
+ ];
+
+ const mockGoogleParams: GoogleSignInParams = {
+ serverClientId: 'test-client-id',
+ autoSelectEnabled: true,
+ };
+
+ const mockAppleParams: AppleSignInParams = {
+ requestedScopes: ['fullName', 'email'],
+ };
+
+ expect(mockCredential.type).toBe('passkey');
+ expect(mockGoogleCredential.type).toBe('google-signin');
+ expect(mockAppleCredential.type).toBe('apple-signin');
+ expect(mockSignInOptions).toHaveLength(3);
+ expect(mockGoogleParams.serverClientId).toBe('test-client-id');
+ expect(mockAppleParams.requestedScopes).toEqual(['fullName', 'email']);
+ });
+
+ it('should reject signUpWithApple on non-iOS platforms', async () => {
+ // Mock Platform.OS to be 'android'
+ const originalPlatform = require('react-native').Platform.OS;
+ require('react-native').Platform.OS = 'android';
+
+ await expect(signUpWithApple()).rejects.toThrow(
+ 'Apple Sign In is only available on iOS'
+ );
+
+ // Restore original platform
+ require('react-native').Platform.OS = originalPlatform;
+ });
+});
diff --git a/src/index.tsx b/src/index.tsx
index 4c0dfb3..0473046 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,8 +2,10 @@ import CredentialsManager from './NativeCredentialsManager';
import type {
Credential,
GoogleCredential,
+ AppleCredential,
SignInOption,
} from './NativeCredentialsManager';
+import { Platform } from 'react-native';
type GoogleSignInParams = {
nonce?: string;
@@ -11,6 +13,11 @@ type GoogleSignInParams = {
autoSelectEnabled?: boolean;
};
+type AppleSignInParams = {
+ nonce?: string;
+ requestedScopes?: ('fullName' | 'email')[];
+};
+
export function signUpWithPasskeys(
requestJson: Object,
preferImmediatelyAvailableCredentials: boolean = false
@@ -27,8 +34,17 @@ export function signUpWithPassword({
}: {
username: string;
password: string;
-}) {
- CredentialsManager.signUpWithPassword({ password, username });
+}): Promise {
+ if (Platform.OS === 'ios') {
+ // iOS only supports AutoFill passwords through Apple's Authentication Services
+ // Manual password storage is not supported
+ return Promise.reject(
+ new Error(
+ 'Manual password storage is not supported on iOS. Use AutoFill passwords through signIn method instead.'
+ )
+ );
+ }
+ return CredentialsManager.signUpWithPassword({ password, username });
}
export function signIn(
@@ -36,21 +52,54 @@ export function signIn(
params: {
passkeys?: Object;
googleSignIn?: GoogleSignInParams;
+ appleSignIn?: AppleSignInParams;
}
): Promise {
- return CredentialsManager.signIn(options, {
+ // Transform options for iOS compatibility
+ const processedOptions = options.map((option) => {
+ if (option === 'google-signin' && Platform.OS === 'ios') {
+ // Automatically replace google-signin with apple-signin on iOS
+ return 'apple-signin';
+ }
+ return option;
+ });
+
+ // Prepare parameters for both platforms
+ const signInParams: any = {
...params,
googleSignIn: {
serverClientId: params?.googleSignIn?.serverClientId ?? '',
nonce: params?.googleSignIn?.nonce ?? '',
autoSelectEnabled: params?.googleSignIn?.autoSelectEnabled ?? true,
},
- });
+ };
+
+ // If we have Apple Sign In option on iOS, add Apple params
+ if (Platform.OS === 'ios' && processedOptions.includes('apple-signin')) {
+ signInParams.appleSignIn = {
+ nonce: params?.appleSignIn?.nonce || params?.googleSignIn?.nonce || '',
+ requestedScopes: params?.appleSignIn?.requestedScopes || [
+ 'fullName',
+ 'email',
+ ],
+ };
+ }
+
+ return CredentialsManager.signIn(processedOptions, signInParams);
}
export function signUpWithGoogle(
params: GoogleSignInParams
-): Promise {
+): Promise {
+ if (Platform.OS === 'ios') {
+ // On iOS, automatically use Apple Sign In instead of Google Sign In
+ // This provides seamless cross-platform compatibility
+ return signUpWithApple({
+ nonce: params.nonce,
+ requestedScopes: ['fullName', 'email'],
+ }) as Promise;
+ }
+
return CredentialsManager.signUpWithGoogle({
...params,
nonce: params.nonce ?? '',
@@ -58,6 +107,34 @@ export function signUpWithGoogle(
});
}
+export function signUpWithApple(
+ params: AppleSignInParams = {}
+): Promise {
+ if (Platform.OS !== 'ios') {
+ return Promise.reject(
+ new Error(
+ 'Apple Sign In is only available on iOS. Use signUpWithGoogle on Android.'
+ )
+ );
+ }
+
+ // Call the native signUpWithApple method directly - uses Apple's Authentication Services
+ return CredentialsManager.signUpWithApple({
+ nonce: params.nonce || '',
+ requestedScopes: params.requestedScopes || ['fullName', 'email'],
+ });
+}
+
export function signOut(): Promise {
return CredentialsManager.signOut();
}
+
+// Export types
+export type {
+ Credential,
+ GoogleCredential,
+ AppleCredential,
+ SignInOption,
+ GoogleSignInParams,
+ AppleSignInParams,
+};