Skip to content

feat(auth, oauth): add native oauth support / no external packages needed #7019

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -54,6 +54,15 @@ public static void rejectPromiseWithCodeAndMessage(
promise.reject(code, message, userInfoMap);
}

public static void rejectPromiseWithMap(
Promise promise, String code, String message, ReadableMap map) {
WritableMap userInfoMap = Arguments.createMap();
userInfoMap.putString("code", code);
userInfoMap.putString("message", message);
userInfoMap.merge(map);
promise.reject(code, message, userInfoMap);
}

public static void rejectPromiseWithCodeAndMessage(Promise promise, String code, String message) {
WritableMap userInfoMap = Arguments.createMap();
userInfoMap.putString("code", code);
5 changes: 5 additions & 0 deletions packages/app/lib/index.d.ts
Original file line number Diff line number Diff line change
@@ -42,6 +42,11 @@ export namespace ReactNativeFirebase {
*/
readonly message: string;

/**
* The email address of the user's account used in the operation that triggered the error, if applicable
*/
readonly email?: string;

/**
* The firebase module namespace that this error originated from, e.g. 'analytics'
*/
9 changes: 9 additions & 0 deletions packages/app/lib/internal/NativeFirebaseError.js
Original file line number Diff line number Diff line change
@@ -39,6 +39,15 @@
value: `[${this.code}] ${userInfo.message || nativeError.message}`,
});

if (typeof userInfo === 'object' && userInfo !== null) {
if ('email' in userInfo) {
Object.defineProperty(this, 'email', {

Check warning on line 44 in packages/app/lib/internal/NativeFirebaseError.js

Codecov / codecov/patch

packages/app/lib/internal/NativeFirebaseError.js#L44

Added line #L44 was not covered by tests
enumerable: true,
value: userInfo.email,
});
}
}

Object.defineProperty(this, 'jsStack', {
enumerable: false,
value: jsStack,
Original file line number Diff line number Diff line change
@@ -30,6 +30,9 @@
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseException;
import com.google.firebase.FirebaseNetworkException;
@@ -46,6 +49,7 @@
import com.google.firebase.auth.FirebaseAuthMultiFactorException;
import com.google.firebase.auth.FirebaseAuthProvider;
import com.google.firebase.auth.FirebaseAuthSettings;
import com.google.firebase.auth.FirebaseAuthUserCollisionException;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.auth.FirebaseUserMetadata;
import com.google.firebase.auth.GetTokenResult;
@@ -55,6 +59,7 @@
import com.google.firebase.auth.MultiFactorInfo;
import com.google.firebase.auth.MultiFactorResolver;
import com.google.firebase.auth.MultiFactorSession;
import com.google.firebase.auth.OAuthCredential;
import com.google.firebase.auth.OAuthProvider;
import com.google.firebase.auth.PhoneAuthCredential;
import com.google.firebase.auth.PhoneAuthOptions;
@@ -203,7 +208,6 @@ public void addIdTokenListener(final String appName) {

FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);

if (!mIdTokenListeners.containsKey(appName)) {
FirebaseAuth.IdTokenListener newIdTokenListener =
firebaseAuth1 -> {
@@ -862,6 +866,93 @@ private void signInWithCredential(
}
}

@ReactMethod
public void signInWithProvider(
String appName, String providerId, @Nullable String email, Promise promise) {
OAuthProvider.Builder provider = OAuthProvider.newBuilder(providerId);

if (email != null) {
provider.addCustomParameter("login_hint", email);
}
provider.addCustomParameter("prompt", "select_account");

Activity activity = getCurrentActivity();
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);

OnSuccessListener onSuccess =
new OnSuccessListener<AuthResult>() {
@Override
public void onSuccess(AuthResult authResult) {
Log.d(TAG, "signInWithProvider:onComplete:success");
promiseWithAuthResult(authResult, promise);
}
};

OnFailureListener onFailure =
new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.w(TAG, "signInWithProvider:onComplete:failure", e);
promiseRejectAuthException(promise, e);
}
};

Task<AuthResult> pendingResultTask = firebaseAuth.getPendingAuthResult();
if (pendingResultTask != null) {
pendingResultTask.addOnSuccessListener(onSuccess).addOnFailureListener(onFailure);
} else {
firebaseAuth
.startActivityForSignInWithProvider(activity, provider.build())
.addOnSuccessListener(onSuccess)
.addOnFailureListener(onFailure);
}
}

@ReactMethod
public void linkWithProvider(
String appName, String providerId, @Nullable String email, Promise promise) {
OAuthProvider.Builder provider = OAuthProvider.newBuilder(providerId);

if (email != null) {
provider.addCustomParameter("login_hint", email);
}
provider.addCustomParameter("prompt", "select_account");

Activity activity = getCurrentActivity();
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
FirebaseUser firebaseUser = firebaseAuth.getCurrentUser();

OnSuccessListener onSuccess =
new OnSuccessListener<AuthResult>() {
@Override
public void onSuccess(AuthResult authResult) {
Log.d(TAG, "linkWithProvider:onComplete:success");
promiseWithAuthResult(authResult, promise);
}
};

OnFailureListener onFailure =
new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.w(TAG, "linkInWithProvider:onComplete:failure", e);
promiseRejectAuthException(promise, e);
}
};

Task<AuthResult> pendingResultTask = firebaseAuth.getPendingAuthResult();
if (pendingResultTask != null) {
pendingResultTask.addOnSuccessListener(onSuccess).addOnFailureListener(onFailure);
} else {
firebaseUser
.startActivityForLinkWithProvider(activity, provider.build())
.addOnSuccessListener(onSuccess)
.addOnFailureListener(onFailure);
}
}

/**
* signInWithPhoneNumber
*
@@ -1889,6 +1980,30 @@ private void promiseWithAuthResult(AuthResult authResult, Promise promise) {
WritableMap authResultMap = Arguments.createMap();
WritableMap userMap = firebaseUserToMap(authResult.getUser());

if (authResult.getCredential() != null) {
if (authResult.getCredential() instanceof OAuthCredential) {
OAuthCredential creds = (OAuthCredential) authResult.getCredential();
WritableMap credentialMap = Arguments.createMap();

credentialMap.putString("providerId", creds.getProvider());
credentialMap.putString("signInMethod", creds.getSignInMethod());

if (creds.getIdToken() != null) {
credentialMap.putString("idToken", creds.getIdToken());
}

if (creds.getAccessToken() != null) {
credentialMap.putString("accessToken", creds.getAccessToken());
}

if (creds.getSecret() != null) {
credentialMap.putString("secret", creds.getSecret());
}

authResultMap.putMap("credential", credentialMap);
}
}

if (authResult.getAdditionalUserInfo() != null) {
WritableMap additionalUserInfoMap = Arguments.createMap();

@@ -1929,14 +2044,22 @@ private void promiseWithAuthResult(AuthResult authResult, Promise promise) {
*/
private void promiseRejectAuthException(Promise promise, Exception exception) {
WritableMap error = getJSError(exception);

final String sessionId = error.getString("sessionId");
final MultiFactorResolver multiFactorResolver = mCachedResolvers.get(sessionId);
WritableMap resolverAsMap = Arguments.createMap();

WritableMap map = Arguments.createMap();
if (multiFactorResolver != null) {
resolverAsMap = resolverToMap(sessionId, multiFactorResolver);
map.putMap("resolver", resolverAsMap);
}

if (error.getString("email") != null) {
map.putString("email", error.getString("email"));
}
rejectPromiseWithCodeAndMessage(
promise, error.getString("code"), error.getString("message"), resolverAsMap);

rejectPromiseWithMap(promise, error.getString("code"), error.getString("message"), map);
}

/**
@@ -1950,6 +2073,7 @@ private WritableMap getJSError(Exception exception) {
String message = exception.getMessage();
String invalidEmail = "The email address is badly formatted.";

System.out.print(exception);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dumping an error to System.out is discouraged, if it is already being propagated up, then I think this could be removed?

try {
FirebaseAuthException authException = (FirebaseAuthException) exception;
code = authException.getErrorCode();
@@ -2023,6 +2147,10 @@ private WritableMap getJSError(Exception exception) {
}
}

if (exception instanceof FirebaseAuthUserCollisionException) {
error.putString("email", ((FirebaseAuthUserCollisionException) exception).getEmail());
}

if (exception instanceof FirebaseAuthMultiFactorException) {
final FirebaseAuthMultiFactorException multiFactorException =
(FirebaseAuthMultiFactorException) exception;
22 changes: 10 additions & 12 deletions packages/auth/e2e/auth.e2e.js
Original file line number Diff line number Diff line change
@@ -912,12 +912,11 @@ describe('auth()', function () {
});

describe('signInWithPopup', function () {
it('should throw an unsupported error', function () {
(() => {
firebase.auth().signInWithPopup();
}).should.throw(
'firebase.auth().signInWithPopup() is unsupported by the native Firebase SDKs.',
);
it('should trigger the oauth flow', async function () {
await (async () => {
const provider = new firebase.auth.OAuthProvider('oidc.react.com');
await firebase.auth().signInWithPopup(provider);
}).should.not.throw();
});
});

@@ -1025,12 +1024,11 @@ describe('auth()', function () {
});

describe('signInWithRedirect()', function () {
it('should throw an unsupported error', function () {
(() => {
firebase.auth().signInWithRedirect();
}).should.throw(
'firebase.auth().signInWithRedirect() is unsupported by the native Firebase SDKs.',
);
it('should trigger the oauth flow', async function () {
await (async () => {
const provider = new firebase.auth.OAuthProvider('oidc.react.com');
await firebase.auth().signInWithRedirect(provider);
}).should.not.throw();
});
});

4 changes: 1 addition & 3 deletions packages/auth/e2e/provider.e2e.js
Original file line number Diff line number Diff line change
@@ -149,9 +149,7 @@ describe('auth() -> Providers', function () {
describe('OAuthProvider', function () {
describe('constructor', function () {
it('should throw an unsupported error', function () {
(() => new firebase.auth.OAuthProvider()).should.throw(
'`new OAuthProvider()` is not supported on the native Firebase SDKs.',
);
(() => new firebase.auth.OAuthProvider('oidc.react.com')).should.not.throw();
});
});

Loading