diff --git a/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuth.java b/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuth.java index 0d6d5d54..58803e80 100644 --- a/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuth.java +++ b/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuth.java @@ -568,11 +568,12 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { /** * signInWithPhoneNumber * + * @param appName * @param phoneNumber - * @param promise + * @param phoneAuthRequestKey */ @ReactMethod - public void signInWithPhoneNumber(String appName, final String phoneNumber, final Promise promise) { + public void signInWithPhoneNumber(final String appName, final String phoneNumber, final String phoneAuthRequestKey) { Log.d(TAG, "signInWithPhoneNumber"); FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); final FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp); @@ -580,48 +581,64 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { // Reset the verification Id mVerificationId = null; - PhoneAuthProvider.getInstance(firebaseAuth).verifyPhoneNumber(phoneNumber, 60, TimeUnit.SECONDS, - getCurrentActivity(), new PhoneAuthProvider.OnVerificationStateChangedCallbacks() { + PhoneAuthProvider.getInstance(firebaseAuth).verifyPhoneNumber(phoneNumber, 120, TimeUnit.SECONDS, + mReactContext.getCurrentActivity(), new PhoneAuthProvider.OnVerificationStateChangedCallbacks() { @Override - public void onVerificationCompleted(PhoneAuthCredential phoneAuthCredential) { + public void onVerificationCompleted(final PhoneAuthCredential phoneAuthCredential) { // User has been automatically verified, log them in firebaseAuth.signInWithCredential(phoneAuthCredential) .addOnSuccessListener(new OnSuccessListener() { @Override public void onSuccess(AuthResult authResult) { - // onAuthStateChanged will pick up the user change Log.d(TAG, "signInWithPhoneNumber:autoVerified:success"); + WritableMap event = Arguments.createMap(); + WritableMap user = firebaseUserToMap(authResult.getUser()); + event.putMap("user", user); + event.putString("type", "user"); + event.putString("appName", appName); + event.putString("appName", phoneAuthCredential.getSmsCode()); + event.putString("phoneAuthRequestKey", phoneAuthRequestKey); + Utils.sendEvent(mReactContext, "phone_auth_event", event); } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception exception) { Log.e(TAG, "signInWithPhoneNumber:autoVerified:failure", exception); - // TODO: Will this ever error? How do we get it back to the JS side? - // promiseRejectAuthException(promise, exception); + WritableMap event = Arguments.createMap(); + WritableMap error = getJSError(exception); + event.putMap("error", error); + event.putString("type", "error"); + event.putString("appName", appName); + event.putString("phoneAuthRequestKey", phoneAuthRequestKey); + Utils.sendEvent(mReactContext, "phone_auth_event", event); } }); } @Override public void onVerificationFailed(FirebaseException e) { - // TODO: Will this ever get sent after we've received an onCodeSent? - // If so, then this will cause an exception as the promise has already been used - promiseRejectAuthException(promise, e); + WritableMap event = Arguments.createMap(); + WritableMap error = getJSError(e); + event.putMap("error", error); + event.putString("type", "error"); + event.putString("appName", appName); + event.putString("phoneAuthRequestKey", phoneAuthRequestKey); + Utils.sendEvent(mReactContext, "phone_auth_event", event); } @Override public void onCodeSent(String verificationId, PhoneAuthProvider.ForceResendingToken forceResendingToken) { - // TODO: This isn't being saved anywhere if the activity gets restarted when going to the SMS app - mVerificationId = verificationId; WritableMap verificationMap = Arguments.createMap(); + verificationMap.putString("appName", appName); + verificationMap.putString("type", "confirm"); verificationMap.putString("verificationId", verificationId); - - promise.resolve(verificationMap); + verificationMap.putString("phoneAuthRequestKey", phoneAuthRequestKey); + Utils.sendEvent(mReactContext, "phone_auth_event", verificationMap); } @Override - public void onCodeAutoRetrievalTimeOut (String verificationId) { + public void onCodeAutoRetrievalTimeOut(String verificationId) { super.onCodeAutoRetrievalTimeOut(verificationId); // Purposefully not doing anything with this at the moment } @@ -629,11 +646,11 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { } @ReactMethod - public void _confirmVerificationCode(String appName, final String verificationCode, final Promise promise) { + public void _confirmVerificationCode(final String appName, final String phoneAuthRequestKey, String verificationId, String verificationCode, final Promise promise) { FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp); - PhoneAuthCredential credential = PhoneAuthProvider.getCredential(mVerificationId, verificationCode); + PhoneAuthCredential credential = PhoneAuthProvider.getCredential(verificationId, verificationCode); firebaseAuth.signInWithCredential(credential) .addOnCompleteListener(new OnCompleteListener() { @@ -641,10 +658,24 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { public void onComplete(@NonNull Task task) { if (task.isSuccessful()) { Log.d(TAG, "signInWithCredential:onComplete:success"); + WritableMap event = Arguments.createMap(); + WritableMap user = firebaseUserToMap(task.getResult().getUser()); + event.putMap("user", user); + event.putString("type", "user"); + event.putString("appName", appName); + event.putString("phoneAuthRequestKey", phoneAuthRequestKey); + Utils.sendEvent(mReactContext, "phone_auth_event", event); promiseWithUser(task.getResult().getUser(), promise); } else { Exception exception = task.getException(); Log.e(TAG, "signInWithCredential:onComplete:failure", exception); + WritableMap event = Arguments.createMap(); + WritableMap error = getJSError(exception); + event.putMap("error", error); + event.putString("type", "error"); + event.putString("appName", appName); + event.putString("phoneAuthRequestKey", phoneAuthRequestKey); + Utils.sendEvent(mReactContext, "phone_auth_event", event); promiseRejectAuthException(promise, exception); } } @@ -1017,6 +1048,17 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { * @param exception */ private void promiseRejectAuthException(Promise promise, Exception exception) { + WritableMap error = getJSError(exception); + promise.reject(error.getString("code"), error.getString("message"), exception); + } + + /** + * getJSError + * + * @param exception + */ + private WritableMap getJSError(Exception exception) { + WritableMap error = Arguments.createMap(); String code = "UNKNOWN"; String message = exception.getMessage(); String invalidEmail = "The email address is badly formatted."; @@ -1024,6 +1066,7 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { try { FirebaseAuthException authException = (FirebaseAuthException) exception; code = authException.getErrorCode(); + error.putString("nativeErrorCode", code); message = authException.getMessage(); } catch (Exception e) { Matcher matcher = Pattern.compile("\\[(.*):.*\\]").matcher(message); @@ -1088,7 +1131,10 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { } code = "auth/" + code.toLowerCase().replace("error_", "").replace('_', '-'); - promise.reject(code, message, exception); + error.putString("code", code); + error.putString("message", message); + error.putString("nativeErrorMessage", exception.getMessage()); + return error; } diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java index 9dc57fc6..a3b7e09f 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java @@ -544,7 +544,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @return */ private static String getCodeWithService(String service, String code) { - return service.toUpperCase() + "/" + code.toUpperCase(); + return service.toLowerCase() + "/" + code.toLowerCase(); } /** diff --git a/lib/internals.js b/lib/internals.js index 6b378a25..16a99932 100644 --- a/lib/internals.js +++ b/lib/internals.js @@ -56,6 +56,13 @@ export default { ERROR_INIT_OBJECT: 'Firebase.initializeApp(options <-- requires a valid configuration object.', ERROR_INIT_STRING_NAME: 'Firebase.initializeApp(options, name <-- requires a valid string value.', + /** + * @return {string} + */ + ERROR_MISSING_CB(method) { + return `Missing required callback for method ${method}().`; + }, + /** * @return {string} * @param namespace diff --git a/lib/modules/auth/ConfirmationResult.js b/lib/modules/auth/ConfirmationResult.js index 18695e8e..c97ad8d5 100644 --- a/lib/modules/auth/ConfirmationResult.js +++ b/lib/modules/auth/ConfirmationResult.js @@ -4,19 +4,31 @@ export default class ConfirmationResult { _auth: Object; _verificationId: string; + _phoneAuthRequestKey: string; /** * + * @param auth * @param verificationId The phone number authentication operation's verification ID. + * @param phoneAuthRequestKey */ - constructor(auth: Object, verificationId: string) { + constructor(auth: Object, verificationId: string, phoneAuthRequestKey: string) { this._auth = auth; this._verificationId = verificationId; + this._phoneAuthRequestKey = phoneAuthRequestKey; } + /** + * + * @param verificationCode + * @return {*} + */ confirm(verificationCode: string): Promise { - // verificationId is stored server side in case the app is shut when opening the SMS app - return this._auth._native.confirmVerificationCode(verificationCode); + return this._auth._native._confirmVerificationCode( + this._phoneAuthRequestKey, + this._verificationId, + verificationCode, + ); } get verificationId(): String | null { diff --git a/lib/modules/auth/index.js b/lib/modules/auth/index.js index 3adcbb9e..3e666090 100644 --- a/lib/modules/auth/index.js +++ b/lib/modules/auth/index.js @@ -1,6 +1,8 @@ // @flow import User from './user'; +import INTERNALS from './../../internals'; import ModuleBase from './../../utils/ModuleBase'; +import { nativeToJSError, generatePushID, isFunction } from './../../utils'; import ConfirmationResult from './ConfirmationResult'; // providers @@ -31,9 +33,48 @@ export default class Auth extends ModuleBase { this._onAuthStateChanged.bind(this), ); + this.addListener( + // sub to phone auth native event - this can + // be either a code sent confirmation result + // event, a successful signIn event or an + // error event + this._getAppEventName('phone_auth_event'), + this._onPhoneAuthEvent.bind(this), + ); + this._native.addAuthStateListener(); } + /** + * Handles an incoming phone auth event and emits to the internal once listeners. + * @param event + * @private + */ + _onPhoneAuthEvent(event) { + const { type, phoneAuthRequestKey } = event; + + if (type === 'confirm') { + return this.emit( + `phone:auth:${phoneAuthRequestKey}:confirm`, + new ConfirmationResult(this, event.verificationId, phoneAuthRequestKey), + ); + } + + if (type === 'user') { + return this.emit( + `phone:auth:${phoneAuthRequestKey}:user`, + this._onAuthStateChanged({ authenticated: true, user: event.user }, false), + ); + } + + if (type === 'error') { + const { code, message } = event.error; + return this.emit(`phone:auth:${phoneAuthRequestKey}:error`, nativeToJSError(code, message)); + } + + throw new Error('Internal RNFirebase Error: Invalid phone auth event received.'); + } + /** * Internal auth changed listener * @param auth @@ -141,18 +182,58 @@ export default class Auth extends ModuleBase { * @return {Promise} A promise resolved upon completion */ signInWithCredential(credential: CredentialType): Promise { - return this._interceptUserValue(this._native.signInWithCredential(credential.provider, credential.token, credential.secret)); + return this._interceptUserValue( + this._native.signInWithCredential( + credential.provider, credential.token, credential.secret, + ), + ); } /** * * @param phoneNumber - * @return {Promise.} + * @return Object */ signInWithPhoneNumber(phoneNumber: string): Promise { - return this._native.signInWithPhoneNumber(phoneNumber).then((result) => { - return new ConfirmationResult(this, result.verificationId); - }); + const phoneAuthRequestKey = generatePushID(); + + return { + onCodeSent: (cb) => { + if (!isFunction(cb)) { + throw new Error(INTERNALS.STRINGS.ERROR_MISSING_CB('onCodeSent')); + } + + this.once(`phone:auth:${phoneAuthRequestKey}:confirm`, cb); + + return new Promise((resolve, reject) => { + // start auth flow + this._native.signInWithPhoneNumber( + phoneNumber, + phoneAuthRequestKey, + ); + + let hadEvent = false; + const successEvent = `phone:auth:${phoneAuthRequestKey}:user`; + const errorEvent = `phone:auth:${phoneAuthRequestKey}:error`; + + this.once(successEvent, (user) => { + if (!hadEvent) { + hadEvent = true; + this.removeListener(errorEvent, reject); + resolve(user); + } + }); + + this.once(errorEvent, (error) => { + if (!hadEvent) { + hadEvent = true; + this.removeListener(successEvent, resolve); + reject(error); + } + }); + }); + }, + }; } /** diff --git a/lib/utils/ModuleBase.js b/lib/utils/ModuleBase.js index 7e965dad..6956a136 100644 --- a/lib/utils/ModuleBase.js +++ b/lib/utils/ModuleBase.js @@ -23,6 +23,7 @@ const NATIVE_MODULE_EVENTS = { ], Auth: [ 'onAuthStateChanged', + 'phone_auth_event', ], Database: [ // 'database_on_event', @@ -133,6 +134,10 @@ export default class ModuleBase { return INTERNALS.SharedEventEmitter.addListener.bind(INTERNALS.SharedEventEmitter); } + get once() { + return INTERNALS.SharedEventEmitter.once.bind(INTERNALS.SharedEventEmitter); + } + get on() { return INTERNALS.SharedEventEmitter.addListener.bind(INTERNALS.SharedEventEmitter); }