diff --git a/.github/scripts/compare-types/configs/auth.ts b/.github/scripts/compare-types/configs/auth.ts index 1d1c8916d5..7a5df49022 100644 --- a/.github/scripts/compare-types/configs/auth.ts +++ b/.github/scripts/compare-types/configs/auth.ts @@ -186,13 +186,23 @@ const config: PackageConfig = { reason: 'RN Firebase exported MultiFactor interface type. firebase-js-sdk exposes multi-factor enrollment through the multiFactor() helper without exporting this interface at the package root.', }, + { + name: 'AppleFullPersonName', + reason: + 'RN Firebase exported type for the fullName option on Sign in with Apple credentials. iOS: forwarded to FIROAuthProvider.appleCredentialWithIDToken:rawNonce:fullName: so Firebase stores it as the displayName on first sign-in. Android: Firebase Android SDK has no equivalent credential-level API (https://firebase.google.com/docs/auth/android/apple#handle-sign-in); ignored, use updateProfile() after sign-in instead. firebase-js-sdk OAuthCredentialOptions has no fullName field on any platform.', + }, ], differentShape: [ { name: 'OAuthCredential', reason: - 'RN Firebase OAuthCredential exposes rawNonce for Apple / limited-login flows. OAuth 1.0 token secrets use the inherited AuthCredential.secret field (firebase-js-sdk optional secret on OAuthCredential).', + 'RN Firebase OAuthCredential exposes rawNonce for Apple / limited-login flows, plus fullName for Sign in with Apple (iOS only; see AppleFullPersonName). OAuth 1.0 token secrets use the inherited AuthCredential.secret field (firebase-js-sdk optional secret on OAuthCredential).', + }, + { + name: 'OAuthCredentialOptions', + reason: + 'RN Firebase adds an optional fullName field (see AppleFullPersonName) so OAuthProvider("apple.com").credential({ idToken, rawNonce, fullName }) can forward Sign in with Apple full-name data on iOS. firebase-js-sdk OAuthCredentialOptions has no fullName field on any platform.', }, { name: 'FacebookAuthProvider', diff --git a/packages/auth/__tests__/auth.test.ts b/packages/auth/__tests__/auth.test.ts index 53527185c0..54eb5ff438 100644 --- a/packages/auth/__tests__/auth.test.ts +++ b/packages/auth/__tests__/auth.test.ts @@ -509,6 +509,92 @@ describe('Auth', function () { expect(credential?.token).toBe('vid'); expect(credential?.secret).toBe('123456'); }); + + it('AppleAuthProvider.credential maps fullName alongside the native bridge fields', function () { + const fullName = { givenName: 'Jonny', familyName: 'Appleseed' }; + const credential: any = AppleAuthProvider.credential( + 'apple-id-token', + 'apple-raw-nonce', + fullName, + ); + expect(credential.providerId).toBe('apple.com'); + expect(credential.signInMethod).toBe('apple.com'); + expect(credential.idToken).toBe('apple-id-token'); + expect(credential.rawNonce).toBe('apple-raw-nonce'); + expect(credential.fullName).toEqual(fullName); + // Native bridge slots are unaffected by fullName (backwards-compatible with pre-fullName behavior). + expect(credential.token).toBe('apple-id-token'); + expect(credential.secret).toBe('apple-raw-nonce'); + }); + + it('AppleAuthProvider.credential works without fullName', function () { + const credential: any = AppleAuthProvider.credential('apple-id-token', 'apple-raw-nonce'); + expect(credential.fullName).toBeUndefined(); + expect(credential.token).toBe('apple-id-token'); + expect(credential.secret).toBe('apple-raw-nonce'); + }); + + it('OAuthProvider(apple.com).credential carries fullName', function () { + const fullName = { givenName: 'Jonny', familyName: 'Appleseed' }; + const credential = new OAuthProvider('apple.com').credential({ + idToken: 'apple-id-token', + rawNonce: 'apple-raw-nonce', + fullName, + }); + expect(credential.fullName).toEqual(fullName); + }); + + it('OAuthCredential.fromJSON round-trips fullName', function () { + const fullName = { givenName: 'Jonny', familyName: 'Appleseed' }; + const credential = AppleAuthProvider.credential( + 'apple-id-token', + 'apple-raw-nonce', + fullName, + ) as OAuthCredential; + const roundTripped = OAuthCredential.fromJSON(credential.toJSON()); + expect(roundTripped?.fullName).toEqual(fullName); + }); + }); + + describe('Sign in with Apple fullName', function () { + it('signInWithCredential forwards fullName to the native bridge', async function () { + const { TurboModuleRegistry } = require('react-native'); + const nativeAuth = TurboModuleRegistry.getEnforcing('NativeRNFBTurboAuth'); + nativeAuth.signInWithCredential.mockClear(); + + const fullName = { givenName: 'Jonny', familyName: 'Appleseed' }; + const credential = AppleAuthProvider.credential( + 'apple-id-token', + 'apple-raw-nonce', + fullName, + ); + await signInWithCredential(getAuth(), credential); + + expect(nativeAuth.signInWithCredential).toHaveBeenCalledWith( + '[DEFAULT]', + 'apple.com', + 'apple-id-token', + 'apple-raw-nonce', + fullName, + ); + }); + + it('signInWithCredential forwards null when fullName is absent', async function () { + const { TurboModuleRegistry } = require('react-native'); + const nativeAuth = TurboModuleRegistry.getEnforcing('NativeRNFBTurboAuth'); + nativeAuth.signInWithCredential.mockClear(); + + const credential = GoogleAuthProvider.credential('google-id-token'); + await signInWithCredential(getAuth(), credential); + + expect(nativeAuth.signInWithCredential).toHaveBeenCalledWith( + '[DEFAULT]', + 'google.com', + 'google-id-token', + '', + null, + ); + }); }); describe('User.reauthenticateWithRedirect', function () { diff --git a/packages/auth/android/src/main/java/io/invertase/firebase/auth/NativeRNFBTurboAuth.java b/packages/auth/android/src/main/java/io/invertase/firebase/auth/NativeRNFBTurboAuth.java index 4db668ccd6..3c0b029b1b 100644 --- a/packages/auth/android/src/main/java/io/invertase/firebase/auth/NativeRNFBTurboAuth.java +++ b/packages/auth/android/src/main/java/io/invertase/firebase/auth/NativeRNFBTurboAuth.java @@ -915,7 +915,15 @@ public void updateProfile(String appName, ReadableMap props, final Promise promi @Override public void signInWithCredential( - String appName, String provider, String authToken, String authSecret, final Promise promise) { + String appName, + String provider, + String authToken, + String authSecret, + @Nullable ReadableMap fullName, + final Promise promise) { + // Sign in with Apple fullName has no equivalent credential-level API on the Firebase Android + // SDK (see AppleFullPersonName in lib/types/auth.ts); intentionally ignored here for spec + // parity with iOS. Callers should use updateProfile() after sign-in on Android. FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp); diff --git a/packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboAuthSpec.java b/packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboAuthSpec.java index dbe4d7f4fd..bdb554d3fa 100644 --- a/packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboAuthSpec.java +++ b/packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboAuthSpec.java @@ -188,7 +188,7 @@ public NativeRNFBTurboAuthSpec(ReactApplicationContext reactContext) { @ReactMethod @DoNotStrip - public abstract void signInWithCredential(String appName, String provider, String authToken, String authSecret, Promise promise); + public abstract void signInWithCredential(String appName, String provider, String authToken, String authSecret, @Nullable ReadableMap fullName, Promise promise); @ReactMethod @DoNotStrip diff --git a/packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/jni/RNFBAuthTurboModules-generated.cpp b/packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/jni/RNFBAuthTurboModules-generated.cpp index 6c381a13c3..91a0f62a46 100644 --- a/packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/jni/RNFBAuthTurboModules-generated.cpp +++ b/packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/jni/RNFBAuthTurboModules-generated.cpp @@ -169,7 +169,7 @@ static facebook::jsi::Value __hostFunction_NativeRNFBTurboAuthSpecJSI_getIdToken static facebook::jsi::Value __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithCredential(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { static jmethodID cachedMethodId = nullptr; - return static_cast(turboModule).invokeJavaMethod(rt, PromiseKind, "signInWithCredential", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId); + return static_cast(turboModule).invokeJavaMethod(rt, PromiseKind, "signInWithCredential", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId); } static facebook::jsi::Value __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithProvider(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { @@ -350,7 +350,7 @@ NativeRNFBTurboAuthSpecJSI::NativeRNFBTurboAuthSpecJSI(const JavaTurboModule::In methodMap_["updateProfile"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboAuthSpecJSI_updateProfile}; methodMap_["getIdToken"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboAuthSpecJSI_getIdToken}; methodMap_["getIdTokenResult"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboAuthSpecJSI_getIdTokenResult}; - methodMap_["signInWithCredential"] = MethodMetadata {4, __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithCredential}; + methodMap_["signInWithCredential"] = MethodMetadata {5, __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithCredential}; methodMap_["signInWithProvider"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithProvider}; methodMap_["signInWithPhoneNumber"] = MethodMetadata {3, __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithPhoneNumber}; methodMap_["verifyPhoneNumberWithMultiFactorInfo"] = MethodMetadata {3, __hostFunction_NativeRNFBTurboAuthSpecJSI_verifyPhoneNumberWithMultiFactorInfo}; diff --git a/packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/jni/react/renderer/components/RNFBAuthTurboModules/RNFBAuthTurboModulesJSI-generated.cpp b/packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/jni/react/renderer/components/RNFBAuthTurboModules/RNFBAuthTurboModulesJSI-generated.cpp index 806538eb38..c2430a335a 100644 --- a/packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/jni/react/renderer/components/RNFBAuthTurboModules/RNFBAuthTurboModulesJSI-generated.cpp +++ b/packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/jni/react/renderer/components/RNFBAuthTurboModules/RNFBAuthTurboModulesJSI-generated.cpp @@ -236,7 +236,8 @@ static jsi::Value __hostFunction_NativeRNFBTurboAuthCxxSpecJSI_signInWithCredent count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt), count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asString(rt), count <= 2 ? throw jsi::JSError(rt, "Expected argument in position 2 to be passed") : args[2].asString(rt), - count <= 3 ? throw jsi::JSError(rt, "Expected argument in position 3 to be passed") : args[3].asString(rt) + count <= 3 ? throw jsi::JSError(rt, "Expected argument in position 3 to be passed") : args[3].asString(rt), + count <= 4 || args[4].isNull() || args[4].isUndefined() ? std::nullopt : std::make_optional(args[4].asObject(rt)) ); } static jsi::Value __hostFunction_NativeRNFBTurboAuthCxxSpecJSI_signInWithProvider(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { @@ -502,7 +503,7 @@ NativeRNFBTurboAuthCxxSpecJSI::NativeRNFBTurboAuthCxxSpecJSI(std::shared_ptr fullName) = 0; virtual jsi::Value signInWithProvider(jsi::Runtime &rt, jsi::String appName, jsi::Object provider) = 0; virtual jsi::Value signInWithPhoneNumber(jsi::Runtime &rt, jsi::String appName, jsi::String phoneNumber, bool forceResend) = 0; virtual jsi::Value verifyPhoneNumberWithMultiFactorInfo(jsi::Runtime &rt, jsi::String appName, jsi::String hintUid, jsi::String sessionKey) = 0; @@ -359,13 +359,13 @@ class JSI_EXPORT NativeRNFBTurboAuthCxxSpec : public TurboModule { return bridging::callFromJs( rt, &T::getIdTokenResult, jsInvoker_, instance_, std::move(appName), std::move(forceRefresh)); } - jsi::Value signInWithCredential(jsi::Runtime &rt, jsi::String appName, jsi::String provider, jsi::String authToken, jsi::String authSecret) override { + jsi::Value signInWithCredential(jsi::Runtime &rt, jsi::String appName, jsi::String provider, jsi::String authToken, jsi::String authSecret, std::optional fullName) override { static_assert( - bridging::getParameterCount(&T::signInWithCredential) == 5, - "Expected signInWithCredential(...) to have 5 parameters"); + bridging::getParameterCount(&T::signInWithCredential) == 6, + "Expected signInWithCredential(...) to have 6 parameters"); return bridging::callFromJs( - rt, &T::signInWithCredential, jsInvoker_, instance_, std::move(appName), std::move(provider), std::move(authToken), std::move(authSecret)); + rt, &T::signInWithCredential, jsInvoker_, instance_, std::move(appName), std::move(provider), std::move(authToken), std::move(authSecret), std::move(fullName)); } jsi::Value signInWithProvider(jsi::Runtime &rt, jsi::String appName, jsi::Object provider) override { static_assert( diff --git a/packages/auth/e2e/provider.e2e.js b/packages/auth/e2e/provider.e2e.js index ddeed7f580..865a7f5ed5 100644 --- a/packages/auth/e2e/provider.e2e.js +++ b/packages/auth/e2e/provider.e2e.js @@ -430,6 +430,59 @@ describe('auth() -> Providers', function () { credential.secret.should.equal(accessToken); credential.accessToken.should.equal(accessToken); }); + + it('should reach iOS native signInWithCredential for apple.com credentials with fullName', async function () { + if (!Platform.ios) { + this.skip(); + } + const { getApp } = modular; + const { OAuthProvider, signInWithCredential, getAuth } = authModular; + const defaultAuth = getAuth(getApp()); + const provider = new OAuthProvider('apple.com'); + const credential = provider.credential({ + idToken: 'apple-id-token-fullname-e2e', + rawNonce: 'apple-raw-nonce-fullname-e2e', + fullName: { + givenName: 'Ada', + familyName: 'Lovelace', + }, + }); + + try { + await signInWithCredential(defaultAuth, credential); + throw new Error('Did not error.'); + } catch (error) { + error.code.should.be.a.String(); + error.code.should.not.equal('auth/unknown'); + String(error.message).should.not.match(/fullName/i); + } + }); + + it('should reach iOS native signInWithCredential for apple.com credentials with an empty fullName', async function () { + if (!Platform.ios) { + this.skip(); + } + const { getApp } = modular; + const { OAuthProvider, signInWithCredential, getAuth } = authModular; + const defaultAuth = getAuth(getApp()); + const provider = new OAuthProvider('apple.com'); + const credential = provider.credential({ + idToken: 'apple-id-token-empty-fullname-e2e', + rawNonce: 'apple-raw-nonce-empty-fullname-e2e', + // No recognized NSPersonNameComponents fields; native should fall back to the + // credential built without fullName rather than an empty name components object. + fullName: {}, + }); + + try { + await signInWithCredential(defaultAuth, credential); + throw new Error('Did not error.'); + } catch (error) { + error.code.should.be.a.String(); + error.code.should.not.equal('auth/unknown'); + String(error.message).should.not.match(/fullName/i); + } + }); }); describe('PROVIDER_ID', function () { diff --git a/packages/auth/ios/RNFBAuth/RNFBAuthModule.mm b/packages/auth/ios/RNFBAuth/RNFBAuthModule.mm index 82fbeee8b7..4eba3c932d 100644 --- a/packages/auth/ios/RNFBAuth/RNFBAuthModule.mm +++ b/packages/auth/ios/RNFBAuth/RNFBAuthModule.mm @@ -641,6 +641,7 @@ - (void)signInWithCredential:(NSString *)appName provider:(NSString *)provider authToken:(NSString *)authToken authSecret:(NSString *)authSecret + fullName:(NSDictionary *)fullName resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { FIRApp *firebaseApp = [RCTConvert firAppFromString:appName]; @@ -649,6 +650,20 @@ - (void)signInWithCredential:(NSString *)appName token:authToken secret:authSecret firebaseApp:firebaseApp]; + + // Sign in with Apple: if the caller supplied fullName (only available on the user's first + // authorization), rebuild the credential via appleCredentialWithIDToken:rawNonce:fullName: so + // Firebase can store it as the account's displayName. See AppleFullPersonName in auth.ts. + if ([provider compare:@"apple.com" options:NSCaseInsensitiveSearch] == NSOrderedSame) { + NSPersonNameComponents *personNameComponents = + [self personNameComponentsFromDictionary:fullName]; + if (personNameComponents != nil) { + credential = [FIROAuthProvider appleCredentialWithIDToken:authToken + rawNonce:authSecret + fullName:personNameComponents]; + } + } + if (credential == nil) { [RNFBSharedUtils rejectPromiseWithUserInfo:reject userInfo:(NSMutableDictionary *)@{ @@ -1605,6 +1620,40 @@ - (void)useEmulator:(NSString *)appName host:(nonnull NSString *)host port:(doub } } +- (NSPersonNameComponents *)personNameComponentsFromDictionary:(NSDictionary *)dictionary { + if (dictionary == nil || (id)dictionary == [NSNull null]) { + return nil; + } + + NSPersonNameComponents *components = [[NSPersonNameComponents alloc] init]; + BOOL hasAnyComponent = NO; + if ([dictionary[@"namePrefix"] isKindOfClass:[NSString class]]) { + components.namePrefix = dictionary[@"namePrefix"]; + hasAnyComponent = YES; + } + if ([dictionary[@"givenName"] isKindOfClass:[NSString class]]) { + components.givenName = dictionary[@"givenName"]; + hasAnyComponent = YES; + } + if ([dictionary[@"middleName"] isKindOfClass:[NSString class]]) { + components.middleName = dictionary[@"middleName"]; + hasAnyComponent = YES; + } + if ([dictionary[@"familyName"] isKindOfClass:[NSString class]]) { + components.familyName = dictionary[@"familyName"]; + hasAnyComponent = YES; + } + if ([dictionary[@"nameSuffix"] isKindOfClass:[NSString class]]) { + components.nameSuffix = dictionary[@"nameSuffix"]; + hasAnyComponent = YES; + } + if ([dictionary[@"nickname"] isKindOfClass:[NSString class]]) { + components.nickname = dictionary[@"nickname"]; + hasAnyComponent = YES; + } + return hasAnyComponent ? components : nil; +} + - (FIRAuthCredential *)getCredentialForProvider:(NSString *)provider token:(NSString *)authToken secret:(NSString *)authTokenSecret diff --git a/packages/auth/ios/generated/RNFBAuthTurboModules/RNFBAuthTurboModules-generated.mm b/packages/auth/ios/generated/RNFBAuthTurboModules/RNFBAuthTurboModules-generated.mm index 3669361cc9..4cbe4c3fc4 100644 --- a/packages/auth/ios/generated/RNFBAuthTurboModules/RNFBAuthTurboModules-generated.mm +++ b/packages/auth/ios/generated/RNFBAuthTurboModules/RNFBAuthTurboModules-generated.mm @@ -147,7 +147,7 @@ - (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallb } static facebook::jsi::Value __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithCredential(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { - return static_cast(turboModule).invokeObjCMethod(rt, PromiseKind, "signInWithCredential", @selector(signInWithCredential:provider:authToken:authSecret:resolve:reject:), args, count); + return static_cast(turboModule).invokeObjCMethod(rt, PromiseKind, "signInWithCredential", @selector(signInWithCredential:provider:authToken:authSecret:fullName:resolve:reject:), args, count); } static facebook::jsi::Value __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithProvider(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { @@ -363,7 +363,7 @@ - (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallb methodMap_["getIdTokenResult"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboAuthSpecJSI_getIdTokenResult}; - methodMap_["signInWithCredential"] = MethodMetadata {4, __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithCredential}; + methodMap_["signInWithCredential"] = MethodMetadata {5, __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithCredential}; methodMap_["signInWithProvider"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithProvider}; diff --git a/packages/auth/ios/generated/RNFBAuthTurboModules/RNFBAuthTurboModules.h b/packages/auth/ios/generated/RNFBAuthTurboModules/RNFBAuthTurboModules.h index 19047bf239..99d2e0f5f7 100644 --- a/packages/auth/ios/generated/RNFBAuthTurboModules/RNFBAuthTurboModules.h +++ b/packages/auth/ios/generated/RNFBAuthTurboModules/RNFBAuthTurboModules.h @@ -173,6 +173,7 @@ namespace JS { provider:(NSString *)provider authToken:(NSString *)authToken authSecret:(NSString *)authSecret + fullName:(NSDictionary * _Nullable)fullName resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; - (void)signInWithProvider:(NSString *)appName diff --git a/packages/auth/ios/generated/RNFBAuthTurboModulesJSI-generated.cpp b/packages/auth/ios/generated/RNFBAuthTurboModulesJSI-generated.cpp index 806538eb38..c2430a335a 100644 --- a/packages/auth/ios/generated/RNFBAuthTurboModulesJSI-generated.cpp +++ b/packages/auth/ios/generated/RNFBAuthTurboModulesJSI-generated.cpp @@ -236,7 +236,8 @@ static jsi::Value __hostFunction_NativeRNFBTurboAuthCxxSpecJSI_signInWithCredent count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt), count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asString(rt), count <= 2 ? throw jsi::JSError(rt, "Expected argument in position 2 to be passed") : args[2].asString(rt), - count <= 3 ? throw jsi::JSError(rt, "Expected argument in position 3 to be passed") : args[3].asString(rt) + count <= 3 ? throw jsi::JSError(rt, "Expected argument in position 3 to be passed") : args[3].asString(rt), + count <= 4 || args[4].isNull() || args[4].isUndefined() ? std::nullopt : std::make_optional(args[4].asObject(rt)) ); } static jsi::Value __hostFunction_NativeRNFBTurboAuthCxxSpecJSI_signInWithProvider(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { @@ -502,7 +503,7 @@ NativeRNFBTurboAuthCxxSpecJSI::NativeRNFBTurboAuthCxxSpecJSI(std::shared_ptr fullName) = 0; virtual jsi::Value signInWithProvider(jsi::Runtime &rt, jsi::String appName, jsi::Object provider) = 0; virtual jsi::Value signInWithPhoneNumber(jsi::Runtime &rt, jsi::String appName, jsi::String phoneNumber, bool forceResend) = 0; virtual jsi::Value verifyPhoneNumberWithMultiFactorInfo(jsi::Runtime &rt, jsi::String appName, jsi::String hintUid, jsi::String sessionKey) = 0; @@ -359,13 +359,13 @@ class JSI_EXPORT NativeRNFBTurboAuthCxxSpec : public TurboModule { return bridging::callFromJs( rt, &T::getIdTokenResult, jsInvoker_, instance_, std::move(appName), std::move(forceRefresh)); } - jsi::Value signInWithCredential(jsi::Runtime &rt, jsi::String appName, jsi::String provider, jsi::String authToken, jsi::String authSecret) override { + jsi::Value signInWithCredential(jsi::Runtime &rt, jsi::String appName, jsi::String provider, jsi::String authToken, jsi::String authSecret, std::optional fullName) override { static_assert( - bridging::getParameterCount(&T::signInWithCredential) == 5, - "Expected signInWithCredential(...) to have 5 parameters"); + bridging::getParameterCount(&T::signInWithCredential) == 6, + "Expected signInWithCredential(...) to have 6 parameters"); return bridging::callFromJs( - rt, &T::signInWithCredential, jsInvoker_, instance_, std::move(appName), std::move(provider), std::move(authToken), std::move(authSecret)); + rt, &T::signInWithCredential, jsInvoker_, instance_, std::move(appName), std::move(provider), std::move(authToken), std::move(authSecret), std::move(fullName)); } jsi::Value signInWithProvider(jsi::Runtime &rt, jsi::String appName, jsi::Object provider) override { static_assert( diff --git a/packages/auth/lib/credentials/OAuthCredential.ts b/packages/auth/lib/credentials/OAuthCredential.ts index 856a008b96..89c1ab853b 100644 --- a/packages/auth/lib/credentials/OAuthCredential.ts +++ b/packages/auth/lib/credentials/OAuthCredential.ts @@ -16,6 +16,7 @@ */ import { AuthCredential, parseCredentialJSON } from './AuthCredential'; +import type { AppleFullPersonName } from '../types/auth'; type OAuthCredentialJSON = { providerId?: string; @@ -25,6 +26,7 @@ type OAuthCredentialJSON = { rawNonce?: string; nonce?: string; secret?: string; + fullName?: AppleFullPersonName; }; type OAuthCredentialParams = { @@ -32,6 +34,8 @@ type OAuthCredentialParams = { accessToken?: string; rawNonce?: string; secret?: string; + /** @remarks Sign in with Apple only; see {@link OAuthCredentialOptions.fullName}. */ + fullName?: AppleFullPersonName; /** @internal RNFB native bridge token slot override */ bridgeToken?: string; /** @internal RNFB native bridge secret slot override */ @@ -75,6 +79,8 @@ export class OAuthCredential extends AuthCredential { readonly accessToken?: string; /** @remarks Used for Sign in with Apple and Facebook limited-login flows. OAuth 1.0 token secrets (e.g. Twitter) use the inherited AuthCredential secret bridge field instead. */ readonly rawNonce?: string; + /** @remarks Sign in with Apple only; see {@link OAuthCredentialOptions.fullName}. */ + readonly fullName?: AppleFullPersonName; constructor(providerId: string, params: OAuthCredentialParams) { const bridge = resolveOAuthBridgeFields(params); @@ -82,6 +88,7 @@ export class OAuthCredential extends AuthCredential { this.idToken = params.idToken; this.accessToken = params.accessToken; this.rawNonce = params.rawNonce; + this.fullName = params.fullName; } toJSON(): object { @@ -97,6 +104,9 @@ export class OAuthCredential extends AuthCredential { if (this.secret && !this.rawNonce) { json.secret = this.secret; } + if (this.fullName) { + json.fullName = this.fullName; + } return json; } @@ -111,6 +121,7 @@ export class OAuthCredential extends AuthCredential { accessToken: parsed.accessToken, rawNonce: parsed.rawNonce ?? parsed.nonce, secret: parsed.secret, + fullName: parsed.fullName, }); } } diff --git a/packages/auth/lib/index.ts b/packages/auth/lib/index.ts index bc8585f575..657bd0d15e 100644 --- a/packages/auth/lib/index.ts +++ b/packages/auth/lib/index.ts @@ -88,6 +88,7 @@ import type { ActionCodeSettings, AdditionalUserInfo, AdditionalUserInfoNative, + AppleFullPersonName, ApplicationVerifier, Auth, AuthCredential, @@ -601,8 +602,14 @@ class FirebaseAuthModule extends FirebaseModule { } signInWithCredential(credential: AuthCredential): Promise { + const fullName = (credential as { fullName?: AppleFullPersonName }).fullName; return this.native - .signInWithCredential(credential.providerId, credential.token, credential.secret) + .signInWithCredential( + credential.providerId, + credential.token, + credential.secret, + fullName ? { ...fullName } : null, + ) .then((userCredential: NativeUserCredentialInternal) => this._setUserCredential(userCredential), ); diff --git a/packages/auth/lib/providers/AppleAuthProvider.ts b/packages/auth/lib/providers/AppleAuthProvider.ts index 80322ad662..7af24bf6e3 100644 --- a/packages/auth/lib/providers/AppleAuthProvider.ts +++ b/packages/auth/lib/providers/AppleAuthProvider.ts @@ -15,8 +15,8 @@ * */ -import { AuthCredential } from '../credentials'; -import type { AuthCredential as AuthCredentialType } from '../types/auth'; +import { OAuthCredential } from '../credentials'; +import type { AppleFullPersonName, AuthCredential as AuthCredentialType } from '../types/auth'; const providerId = 'apple.com' as const; @@ -26,7 +26,7 @@ const providerId = 'apple.com' as const; * @example * ```js * const provider = new OAuthProvider('apple.com'); - * const credential = provider.credential({ idToken, rawNonce }); + * const credential = provider.credential({ idToken, rawNonce, fullName }); * ``` */ export default class AppleAuthProvider { @@ -38,7 +38,23 @@ export default class AppleAuthProvider { return providerId; } - static credential(token: string, secret?: string): AuthCredentialType { - return new AuthCredential(providerId, providerId, token, secret ?? ''); + /** + * @param token The Apple identity token. + * @param secret The raw nonce used to obtain the identity token. + * @param fullName The user's full name, as shared by Sign in with Apple on the user's first + * authorization. See {@link OAuthCredentialOptions.fullName} for platform support. + */ + static credential( + token: string, + secret?: string, + fullName?: AppleFullPersonName, + ): AuthCredentialType { + return new OAuthCredential(providerId, { + idToken: token, + rawNonce: secret, + fullName, + bridgeToken: token, + bridgeSecret: secret ?? '', + }); } } diff --git a/packages/auth/lib/providers/OAuthProvider.ts b/packages/auth/lib/providers/OAuthProvider.ts index 8029ebcb1a..3872ae521c 100644 --- a/packages/auth/lib/providers/OAuthProvider.ts +++ b/packages/auth/lib/providers/OAuthProvider.ts @@ -94,6 +94,7 @@ export default class OAuthProvider { idToken: credential.idToken, accessToken: credential.accessToken, rawNonce: credential.rawNonce ?? credential.nonce, + fullName: credential.fullName, }); } @@ -112,6 +113,7 @@ export default class OAuthProvider { idToken: params.idToken, accessToken: params.accessToken, rawNonce: params.rawNonce, + fullName: params.fullName, }); } diff --git a/packages/auth/lib/types/auth.ts b/packages/auth/lib/types/auth.ts index 101a49f32c..64c25c16e6 100644 --- a/packages/auth/lib/types/auth.ts +++ b/packages/auth/lib/types/auth.ts @@ -169,10 +169,36 @@ export interface ApplicationVerifier { export type CustomParameters = Record; +/** + * The user's full name, as shared by Sign in with Apple. + * + * Apple only shares this data on the user's first authorization for a given app; it is + * omitted from the `ASAuthorizationAppleIDCredential` on subsequent sign-ins, so it should be + * captured and forwarded on the first sign-in. + */ +export interface AppleFullPersonName { + namePrefix?: string | null; + givenName?: string | null; + middleName?: string | null; + familyName?: string | null; + nameSuffix?: string | null; + nickname?: string | null; +} + export interface OAuthCredentialOptions { idToken?: string; accessToken?: string; rawNonce?: string; + /** + * The user's full name, as shared by Sign in with Apple on the user's first authorization. + * + * @remarks Only forwarded to the native SDK for the `apple.com` provider on iOS, where it is + * passed to `FIROAuthProvider.appleCredentialWithIDToken:rawNonce:fullName:` so Firebase can + * store it as {@link User.displayName} while creating the account. The Firebase Android SDK + * has no equivalent credential-level API; on Android (and Web) this field is ignored and the + * display name must be set with {@link updateProfile} after sign-in. + */ + fullName?: AppleFullPersonName; } export interface AuthProvider { diff --git a/packages/auth/lib/types/internal.ts b/packages/auth/lib/types/internal.ts index 3121be7ac3..df899b1cbd 100644 --- a/packages/auth/lib/types/internal.ts +++ b/packages/auth/lib/types/internal.ts @@ -267,6 +267,8 @@ export interface RNFBAuthModule { providerId: string, token: string, secret?: string | null, + // Sign in with Apple full name (iOS only; see AppleFullPersonName in ./auth.ts). + fullName?: Record | null, ): Promise; revokeToken(authorizationCode: string): Promise; sendPasswordResetEmail( diff --git a/packages/auth/lib/web/RNFBAuthModule.ts b/packages/auth/lib/web/RNFBAuthModule.ts index 964d5aea0b..210985a5d1 100644 --- a/packages/auth/lib/web/RNFBAuthModule.ts +++ b/packages/auth/lib/web/RNFBAuthModule.ts @@ -863,6 +863,8 @@ export default { provider: string, authToken: string, authSecret?: string | null, + // Sign in with Apple full name: unsupported by firebase-js-sdk's OAuthCredential, ignored on web. + _fullName?: object | null, ) { return guard(async () => { const auth = getCachedAuthInstance(appName); diff --git a/packages/auth/specs/NativeRNFBTurboAuth.ts b/packages/auth/specs/NativeRNFBTurboAuth.ts index 328281a9e1..13d4636da5 100644 --- a/packages/auth/specs/NativeRNFBTurboAuth.ts +++ b/packages/auth/specs/NativeRNFBTurboAuth.ts @@ -60,6 +60,8 @@ export interface Spec extends TurboModule { provider: string, authToken: string, authSecret: string, + // Sign in with Apple full name (iOS only; see AppleFullPersonName in lib/types/auth.ts). + fullName?: Object | null, ): Promise; signInWithProvider(appName: string, provider: Object): Promise; signInWithPhoneNumber( diff --git a/packages/auth/type-test.ts b/packages/auth/type-test.ts index a2b86104c4..337214ed62 100644 --- a/packages/auth/type-test.ts +++ b/packages/auth/type-test.ts @@ -6,6 +6,7 @@ import { getApp } from '@react-native-firebase/app'; import { applyActionCode, ActionCodeOperation, + AppleAuthProvider, beforeAuthStateChanged, checkActionCode, confirmPasswordReset, @@ -57,8 +58,10 @@ import { SDK_VERSION, type ActionCodeInfo, type ActionCodeSettings, + type AppleFullPersonName, type ApplicationVerifier, type Auth, + type AuthCredential, type AuthError, type AuthProvider, type AuthSettings, @@ -142,14 +145,28 @@ const phoneAuthCredential: PhoneAuthCredential = PhoneAuthProvider.credential( 'verification-id', '123456', ); +const appleFullPersonName: AppleFullPersonName = { + namePrefix: null, + givenName: 'Jonny', + middleName: null, + familyName: 'Appleseed', + nameSuffix: null, + nickname: null, +}; const oauthCredentialOptions: OAuthCredentialOptions = { idToken: 'id-token', accessToken: 'access-token', rawNonce: 'nonce', + fullName: appleFullPersonName, }; const oauthCredential: OAuthCredential = new OAuthProvider('apple.com').credential( oauthCredentialOptions, ); +const appleCredential: AuthCredential = AppleAuthProvider.credential( + 'apple-id-token', + 'apple-raw-nonce', + appleFullPersonName, +); const oauthCredentialFromJSON: OAuthCredential = OAuthProvider.credentialFromJSON({ providerId: 'apple.com', idToken: 'apple-id-token', @@ -195,6 +212,7 @@ console.log(phoneAuthCredential.signInMethod); console.log(oauthCredentialOptions.idToken); console.log(oauthCredential.providerId); console.log(oauthCredential.rawNonce); +console.log(appleCredential.providerId); console.log(oauthCredentialFromJSON.accessToken); console.log(facebookCredential.accessToken); console.log(facebookCredentialFromResult?.accessToken);