Skip to content
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

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions packages/app/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
*/
Expand Down
9 changes: 9 additions & 0 deletions packages/app/lib/internal/NativeFirebaseError.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

View check run for this annotation

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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 -> {
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
22 changes: 10 additions & 12 deletions packages/auth/e2e/auth.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});

Expand Down Expand Up @@ -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();
});
});

Expand Down
4 changes: 1 addition & 3 deletions packages/auth/e2e/provider.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});

Expand Down
Loading
Loading