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 9ee2f4c5..0d6d5d54 100644 --- a/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuth.java +++ b/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuth.java @@ -6,6 +6,7 @@ import android.support.annotation.NonNull; import java.util.HashMap; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -25,11 +26,14 @@ 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.auth.ActionCodeResult; import com.google.firebase.auth.AuthCredential; import com.google.firebase.auth.AuthResult; import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException; import com.google.firebase.auth.GithubAuthProvider; +import com.google.firebase.auth.PhoneAuthCredential; +import com.google.firebase.auth.PhoneAuthProvider; import com.google.firebase.auth.ProviderQueryResult; import com.google.firebase.auth.TwitterAuthProvider; import com.google.firebase.auth.UserInfo; @@ -47,6 +51,7 @@ import io.invertase.firebase.Utils; @SuppressWarnings("ThrowableResultOfMethodCallIgnored") class RNFirebaseAuth extends ReactContextBaseJavaModule { private static final String TAG = "RNFirebaseAuth"; + private String mVerificationId; private ReactContext mReactContext; private HashMap mAuthListeners = new HashMap<>(); @@ -560,6 +565,92 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { } } + /** + * signInWithPhoneNumber + * + * @param phoneNumber + * @param promise + */ + @ReactMethod + public void signInWithPhoneNumber(String appName, final String phoneNumber, final Promise promise) { + Log.d(TAG, "signInWithPhoneNumber"); + FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); + final FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp); + + // Reset the verification Id + mVerificationId = null; + + PhoneAuthProvider.getInstance(firebaseAuth).verifyPhoneNumber(phoneNumber, 60, TimeUnit.SECONDS, + getCurrentActivity(), new PhoneAuthProvider.OnVerificationStateChangedCallbacks() { + @Override + public void onVerificationCompleted(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"); + } + }) + .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); + } + }); + } + + @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); + } + + @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("verificationId", verificationId); + + promise.resolve(verificationMap); + } + + @Override + public void onCodeAutoRetrievalTimeOut (String verificationId) { + super.onCodeAutoRetrievalTimeOut(verificationId); + // Purposefully not doing anything with this at the moment + } + }); + } + + @ReactMethod + public void _confirmVerificationCode(String appName, final String verificationCode, final Promise promise) { + FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); + FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp); + + PhoneAuthCredential credential = PhoneAuthProvider.getCredential(mVerificationId, verificationCode); + + firebaseAuth.signInWithCredential(credential) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + Log.d(TAG, "signInWithCredential:onComplete:success"); + promiseWithUser(task.getResult().getUser(), promise); + } else { + Exception exception = task.getException(); + Log.e(TAG, "signInWithCredential:onComplete:failure", exception); + promiseRejectAuthException(promise, exception); + } + } + }); + } + /** * confirmPasswordReset * @@ -1026,6 +1117,13 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { userInfoMap.putNull("photoURL"); } + final String phoneNumber = userInfo.getPhoneNumber(); + if (phoneNumber != null) { + userInfoMap.putString("phoneNumber", phoneNumber); + } else { + userInfoMap.putNull("phoneNumber"); + } + userInfoMap.putString("email", userInfo.getEmail()); output.pushMap(userInfoMap); @@ -1044,12 +1142,13 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { private WritableMap firebaseUserToMap(FirebaseUser user) { WritableMap userMap = Arguments.createMap(); - final String email = user.getEmail(); final String uid = user.getUid(); - final String provider = user.getProviderId(); - final String name = user.getDisplayName(); - final Boolean verified = user.isEmailVerified(); + final String email = user.getEmail(); final Uri photoUrl = user.getPhotoUrl(); + final String name = user.getDisplayName(); + final String provider = user.getProviderId(); + final Boolean verified = user.isEmailVerified(); + final String phoneNumber = user.getPhoneNumber(); userMap.putString("uid", uid); userMap.putString("providerId", provider); @@ -1074,6 +1173,12 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { userMap.putNull("photoURL"); } + if (phoneNumber != null) { + userMap.putString("phoneNumber", phoneNumber); + } else { + userMap.putNull("phoneNumber"); + } + userMap.putArray("providerData", convertProviderData(user.getProviderData())); return userMap; diff --git a/ios/RNFirebase/auth/RNFirebaseAuth.m b/ios/RNFirebase/auth/RNFirebaseAuth.m index 042f2eb3..59371314 100644 --- a/ios/RNFirebase/auth/RNFirebaseAuth.m +++ b/ios/RNFirebase/auth/RNFirebaseAuth.m @@ -604,6 +604,51 @@ RCT_EXPORT_METHOD(signInWithCustomToken: }]; } +/** + signInWithPhoneNumber + + @param string phoneNumber + @param RCTPromiseResolveBlock resolve + @param RCTPromiseRejectBlock reject + @return + */ +RCT_EXPORT_METHOD(signInWithPhoneNumber:(NSString *) appName + phoneNumber:(NSString *) phoneNumber + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject) { + FIRApp *firApp = [FIRApp appNamed:appName]; + + [[FIRPhoneAuthProvider providerWithAuth:[FIRAuth authWithApp:firApp]] verifyPhoneNumber:phoneNumber completion:^(NSString * _Nullable verificationID, NSError * _Nullable error) { + if (error) { + [self promiseRejectAuthException:reject error:error]; + } else { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setObject:verificationID forKey:@"authVerificationID"]; + resolve(@{ + @"verificationId": verificationID + }); + } + }]; +} + +RCT_EXPORT_METHOD(_confirmVerificationCode:(NSString *) appName + verificationCode:(NSString *) verificationCode + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject) { + FIRApp *firApp = [FIRApp appNamed:appName]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *verificationId = [defaults stringForKey:@"authVerificationID"]; + FIRAuthCredential *credential = [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationId verificationCode:verificationCode]; + + [[FIRAuth authWithApp:firApp] signInWithCredential:credential completion:^(FIRUser *user, NSError *error) { + if (error) { + [self promiseRejectAuthException:reject error:error]; + } else { + [self promiseWithUser:resolve rejecter:reject user:user]; + } + }]; +} + /** link - *insert zelda joke here* @@ -999,6 +1044,10 @@ RCT_EXPORT_METHOD(fetchProvidersForEmail: [pData setValue:userInfo.email forKey:@"email"]; } + if (userInfo.phoneNumber != nil) { + [pData setValue:userInfo.phoneNumber forKey:@"phoneNumber"]; + } + [output addObject:pData]; } @@ -1012,7 +1061,7 @@ RCT_EXPORT_METHOD(fetchProvidersForEmail: @return NSDictionary */ - (NSMutableDictionary *)firebaseUserToDict:(FIRUser *)user { - NSMutableDictionary *userDict = [@{@"uid": user.uid, @"email": user.email ? user.email : [NSNull null], @"emailVerified": @(user.emailVerified), @"isAnonymous": @(user.anonymous), @"displayName": user.displayName ? user.displayName : [NSNull null], @"refreshToken": user.refreshToken, @"providerId": [user.providerID lowercaseString], @"providerData": [self convertProviderData:user.providerData]} mutableCopy]; + NSMutableDictionary *userDict = [@{@"uid": user.uid, @"email": user.email ? user.email : [NSNull null], @"emailVerified": @(user.emailVerified), @"isAnonymous": @(user.anonymous), @"displayName": user.displayName ? user.displayName : [NSNull null], @"refreshToken": user.refreshToken, @"providerId": [user.providerID lowercaseString], @"phoneNumber": user.phoneNumber ? user.phoneNumber : [NSNull null], @"providerData": [self convertProviderData:user.providerData]} mutableCopy]; if ([user valueForKey:@"photoURL"] != nil) { [userDict setValue:[user.photoURL absoluteString] forKey:@"photoURL"]; diff --git a/ios/RNFirebase/database/RNFirebaseDatabase.h b/ios/RNFirebase/database/RNFirebaseDatabase.h index d793a9ff..7c1f1aa2 100644 --- a/ios/RNFirebase/database/RNFirebaseDatabase.h +++ b/ios/RNFirebase/database/RNFirebaseDatabase.h @@ -1,20 +1,26 @@ #ifndef RNFirebaseDatabase_h #define RNFirebaseDatabase_h + #import #if __has_include() + +#import #import #import -@interface RNFirebaseDatabase : RCTEventEmitter {} +@interface RNFirebaseDatabase : RCTEventEmitter {} @property NSMutableDictionary *dbReferences; @property NSMutableDictionary *transactions; @property dispatch_queue_t transactionQueue; -+ (void)handlePromise:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock) reject databaseError:(NSError *)databaseError; -+ (FIRDatabase *)getDatabaseForApp:(NSString*)appName; -+ (NSString *) getMessageWithService:(NSString *) message service:(NSString *) service fullCode:(NSString *) fullCode; -+ (NSString *) getCodeWithService:(NSString *) service code:(NSString *) code; ++ (void)handlePromise:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject databaseError:(NSError *)databaseError; + ++ (FIRDatabase *)getDatabaseForApp:(NSString *)appName; + ++ (NSString *)getMessageWithService:(NSString *)message service:(NSString *)service fullCode:(NSString *)fullCode; + ++ (NSString *)getCodeWithService:(NSString *)service code:(NSString *)code; @end diff --git a/ios/RNFirebase/database/RNFirebaseDatabaseReference.h b/ios/RNFirebase/database/RNFirebaseDatabaseReference.h index a9a594b9..f914a2f9 100644 --- a/ios/RNFirebase/database/RNFirebaseDatabaseReference.h +++ b/ios/RNFirebase/database/RNFirebaseDatabaseReference.h @@ -3,6 +3,8 @@ #import #if __has_include() +#import +#import "RNFirebaseDatabase.h" #import "RNFirebaseEvents.h" #import #import "Firebase.h" diff --git a/lib/modules/auth/ConfirmationResult.js b/lib/modules/auth/ConfirmationResult.js new file mode 100644 index 00000000..18695e8e --- /dev/null +++ b/lib/modules/auth/ConfirmationResult.js @@ -0,0 +1,25 @@ +/** + * @url https://firebase.google.com/docs/reference/js/firebase.User + */ +export default class ConfirmationResult { + _auth: Object; + _verificationId: string; + + /** + * + * @param verificationId The phone number authentication operation's verification ID. + */ + constructor(auth: Object, verificationId: string) { + this._auth = auth; + this._verificationId = verificationId; + } + + 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); + } + + get verificationId(): String | null { + return _verificationId; + } +} diff --git a/lib/modules/auth/index.js b/lib/modules/auth/index.js index 483a0ee5..ff96797e 100644 --- a/lib/modules/auth/index.js +++ b/lib/modules/auth/index.js @@ -1,13 +1,15 @@ // @flow import User from './user'; import ModuleBase from './../../utils/ModuleBase'; +import ConfirmationResult from './ConfirmationResult'; // providers import EmailAuthProvider from './providers/Email'; +import PhoneAuthProvider from './providers/Phone'; import GoogleAuthProvider from './providers/Google'; -import FacebookAuthProvider from './providers/Facebook'; -import TwitterAuthProvider from './providers/Twitter'; import GithubAuthProvider from './providers/Github'; +import TwitterAuthProvider from './providers/Twitter'; +import FacebookAuthProvider from './providers/Facebook'; export default class Auth extends ModuleBase { _user: User | null; @@ -139,6 +141,17 @@ export default class Auth extends ModuleBase { return this._interceptUserValue(this._native.signInWithCredential(credential.provider, credential.token, credential.secret)); } + /** + * + * @param phoneNumber + * @return {Promise.} + */ + signInWithPhoneNumber(phoneNumber: string): Promise { + return this._native.signInWithPhoneNumber(phoneNumber).then((result) => { + return new ConfirmationResult(this, result.verificationId); + }); + } + /** * Send reset password instructions via email * @param {string} email The email to send password reset instructions @@ -211,9 +224,10 @@ export default class Auth extends ModuleBase { } export const statics = { - GoogleAuthProvider, EmailAuthProvider, - FacebookAuthProvider, - TwitterAuthProvider, + PhoneAuthProvider, + GoogleAuthProvider, GithubAuthProvider, + TwitterAuthProvider, + FacebookAuthProvider, }; diff --git a/lib/modules/auth/providers/Phone.js b/lib/modules/auth/providers/Phone.js new file mode 100644 index 00000000..9fa405d6 --- /dev/null +++ b/lib/modules/auth/providers/Phone.js @@ -0,0 +1,10 @@ +export default { + credential(verificationId, code) { + return { + token: verificationId, + secret: code, + provider: 'phone', + providerId: 'phone', + }; + }, +}; diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index 9fc6e353..30ecd954 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -104,7 +104,7 @@ PODS: - React/cxxreact (0.44.3): - React/jschelpers - React/jschelpers (0.44.3) - - RNFirebase (2.1.0): + - RNFirebase (2.1.2): - React - Yoga (0.44.3.React) @@ -126,11 +126,11 @@ DEPENDENCIES: EXTERNAL SOURCES: React: - :path: ../node_modules/react-native + :path: "../node_modules/react-native" RNFirebase: - :path: ./../../ + :path: "./../../" Yoga: - :path: ../node_modules/react-native/ReactCommon/yoga + :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: Firebase: 1492bf04e1b73a7353b4fb2cf5a20bac9692f341 @@ -151,9 +151,9 @@ SPEC CHECKSUMS: nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 Protobuf: d582fecf68201eac3d79ed61369ef45734394b9c React: 6361345ebeb769a929e10a06baf0c868d6d03ad5 - RNFirebase: aef6fb5e509449546aa65cef2efae02d903c5fce + RNFirebase: f796cb5ff9c09f72f181f72a8b9fd972fccc9c49 Yoga: c90474ca3ec1edba44c97b6c381f03e222a9e287 PODFILE CHECKSUM: 45666f734ebfc8b3b0f2be0a83bc2680caeb502f -COCOAPODS: 1.3.1 +COCOAPODS: 1.2.1 diff --git a/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj b/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj index e9d6a8f6..fa0e9847 100644 --- a/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj +++ b/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj @@ -1062,7 +1062,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; 2D02E4CB1E0B4B27006451C7 /* Bundle React Native Code And Images */ = { diff --git a/tests/src/tests/database/ref/offTests.js b/tests/src/tests/database/ref/offTests.js index 6c7835a1..7d439fcc 100644 --- a/tests/src/tests/database/ref/offTests.js +++ b/tests/src/tests/database/ref/offTests.js @@ -3,8 +3,8 @@ import sinon from 'sinon'; import DatabaseContents from '../../support/DatabaseContents'; -function offTests({ describe, it, xcontext, context, firebase }) { - describe('ref().off()', () => { +function offTests({ fdescribe, it, xcontext, context, firebase }) { + fdescribe('ref().off()', () => { it('doesn\'t unbind children callbacks', async () => { // Setup diff --git a/tests/src/tests/database/ref/on/onChildAddedTests.js b/tests/src/tests/database/ref/on/onChildAddedTests.js index 0eb80ab5..0e94d281 100644 --- a/tests/src/tests/database/ref/on/onChildAddedTests.js +++ b/tests/src/tests/database/ref/on/onChildAddedTests.js @@ -1,9 +1,9 @@ import 'should-sinon'; import Promise from 'bluebird'; -export default function onChildAddedTests({ describe, beforeEach, afterEach, it, firebase }) { - describe('ref().on(\'child_added\')', () => { - describe('the snapshot', () => { +export default function onChildAddedTests({ fdescribe, beforeEach, afterEach, it, firebase }) { + fdescribe('ref().on(\'child_added\')', () => { + fdescribe('the snapshot', () => { let ref; let childRef; let childVal; diff --git a/tests/src/tests/database/ref/on/onTests.js b/tests/src/tests/database/ref/on/onTests.js index 32aeb754..eff5d97d 100644 --- a/tests/src/tests/database/ref/on/onTests.js +++ b/tests/src/tests/database/ref/on/onTests.js @@ -1,7 +1,7 @@ import 'should-sinon'; -function onTests({ describe, it, firebase, context }) { - describe('ref().on()', () => { +function onTests({ fdescribe, it, firebase, context }) { + fdescribe('ref().on()', () => { // Observed Web API Behaviour context('when no eventName is provided', () => { it('then raises an error', () => { diff --git a/tests/src/tests/database/ref/on/onValueTests.js b/tests/src/tests/database/ref/on/onValueTests.js index 77044733..d487ffc2 100644 --- a/tests/src/tests/database/ref/on/onValueTests.js +++ b/tests/src/tests/database/ref/on/onValueTests.js @@ -4,8 +4,8 @@ import Promise from 'bluebird'; import DatabaseContents from '../../../support/DatabaseContents'; -function onTests({ describe, context, it, firebase, tryCatch }) { - describe('ref().on(\'value\')', () => { +function onTests({ fdescribe, context, it, firebase, tryCatch }) { + fdescribe('ref().on(\'value\')', () => { // Documented Web API Behaviour it('returns the success callback', () => { // Setup