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 d4bc7a1f..601f5cb5 100644 --- a/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuth.java +++ b/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuth.java @@ -33,6 +33,7 @@ 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.FirebaseAuthProvider; import com.google.firebase.auth.GithubAuthProvider; import com.google.firebase.auth.PhoneAuthCredential; import com.google.firebase.auth.PhoneAuthProvider; @@ -54,6 +55,7 @@ import io.invertase.firebase.Utils; class RNFirebaseAuth extends ReactContextBaseJavaModule { private static final String TAG = "RNFirebaseAuth"; private String mVerificationId; + private PhoneAuthCredential mCredential; private ReactContext mReactContext; private HashMap mAuthListeners = new HashMap<>(); private HashMap mIdTokenListeners = new HashMap<>(); @@ -738,10 +740,16 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { Log.d(TAG, "verifyPhoneNumber:" + phoneNumber); + // Reset the credential + mCredential = null; + PhoneAuthProvider.OnVerificationStateChangedCallbacks callbacks = new PhoneAuthProvider.OnVerificationStateChangedCallbacks() { @Override public void onVerificationCompleted(final PhoneAuthCredential phoneAuthCredential) { + // Cache the credential to protect against null verificationId + mCredential = phoneAuthCredential; + Log.d(TAG, "verifyPhoneNumber:verification:onVerificationCompleted"); WritableMap state = Arguments.createMap(); @@ -1068,6 +1076,15 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { case "github.com": return GithubAuthProvider.getCredential(authToken); case "phone": + // If the phone number is auto-verified quickly, then the verificationId can be null + // We cached the credential as part of the verifyPhoneNumber request to be re-used here + // if possible + if (authToken == null && mCredential != null) { + PhoneAuthCredential credential = mCredential; + // Reset the cached credential + mCredential = null; + return credential; + } return PhoneAuthProvider.getCredential(authToken, authSecret); case "password": return EmailAuthProvider.getCredential(authToken, authSecret); @@ -1282,12 +1299,12 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { * @param providerData List user.getProviderData() * @return WritableArray array */ - private WritableArray convertProviderData(List providerData) { + private WritableArray convertProviderData(List providerData, FirebaseUser user) { WritableArray output = Arguments.createArray(); for (UserInfo userInfo : providerData) { // remove 'firebase' provider data - android fb sdk // should not be returning this as the ios/web ones don't - if (!userInfo.getProviderId().equals("firebase")) { + if (!FirebaseAuthProvider.PROVIDER_ID.equals(userInfo.getProviderId())) { WritableMap userInfoMap = Arguments.createMap(); userInfoMap.putString("providerId", userInfo.getProviderId()); userInfoMap.putString("uid", userInfo.getUid()); @@ -1295,20 +1312,34 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { final Uri photoUrl = userInfo.getPhotoUrl(); - if (photoUrl != null) { + if (photoUrl != null && !"".equals(photoUrl)) { userInfoMap.putString("photoURL", photoUrl.toString()); } else { userInfoMap.putNull("photoURL"); } final String phoneNumber = userInfo.getPhoneNumber(); - if (phoneNumber != null) { + // The Android SDK is missing the phone number property for the phone provider when the + // user first signs up using their phone number. Use the phone number from the user + // object instead + if (PhoneAuthProvider.PROVIDER_ID.equals(userInfo.getProviderId()) + && (userInfo.getPhoneNumber() == null || "".equals(userInfo.getPhoneNumber()))) { + userInfoMap.putString("phoneNumber", user.getPhoneNumber()); + } else if (phoneNumber != null && !"".equals(phoneNumber)) { userInfoMap.putString("phoneNumber", phoneNumber); } else { userInfoMap.putNull("phoneNumber"); } - userInfoMap.putString("email", userInfo.getEmail()); + // The Android SDK is missing the email property for the email provider, so we use UID instead + if (EmailAuthProvider.PROVIDER_ID.equals(userInfo.getProviderId()) + && (userInfo.getEmail() == null || "".equals(userInfo.getEmail()))) { + userInfoMap.putString("email", userInfo.getUid()); + } else if (userInfo.getEmail() != null && !"".equals(userInfo.getEmail())) { + userInfoMap.putString("email", userInfo.getEmail()); + } else { + userInfoMap.putNull("email"); + } output.pushMap(userInfoMap); } @@ -1339,31 +1370,31 @@ class RNFirebaseAuth extends ReactContextBaseJavaModule { userMap.putBoolean("emailVerified", verified); userMap.putBoolean("isAnonymous", user.isAnonymous()); - if (email != null) { + if (email != null && !"".equals(email)) { userMap.putString("email", email); } else { userMap.putNull("email"); } - if (name != null) { + if (name != null && !"".equals(name)) { userMap.putString("displayName", name); } else { userMap.putNull("displayName"); } - if (photoUrl != null) { + if (photoUrl != null && !"".equals(photoUrl)) { userMap.putString("photoURL", photoUrl.toString()); } else { userMap.putNull("photoURL"); } - if (phoneNumber != null) { + if (phoneNumber != null && !"".equals(phoneNumber)) { userMap.putString("phoneNumber", phoneNumber); } else { userMap.putNull("phoneNumber"); } - userMap.putArray("providerData", convertProviderData(user.getProviderData())); + userMap.putArray("providerData", convertProviderData(user.getProviderData(), user)); return userMap; } 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 93ce73f4..da448ba8 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java @@ -399,7 +399,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { */ @ReactMethod public void on(String appName, ReadableMap props) { - getInternalReferenceForApp(appName, props) + getCachedInternalReferenceForApp(appName, props) .on( props.getString("eventType"), props.getMap("registration") @@ -481,19 +481,13 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @return */ private RNFirebaseDatabaseReference getInternalReferenceForApp(String appName, String key, String path, ReadableArray modifiers) { - RNFirebaseDatabaseReference existingRef = references.get(key); - - if (existingRef == null) { - existingRef = new RNFirebaseDatabaseReference( - getReactApplicationContext(), - appName, - key, - path, - modifiers - ); - } - - return existingRef; + return new RNFirebaseDatabaseReference( + getReactApplicationContext(), + appName, + key, + path, + modifiers + ); } /** @@ -503,7 +497,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param props * @return */ - private RNFirebaseDatabaseReference getInternalReferenceForApp(String appName, ReadableMap props) { + private RNFirebaseDatabaseReference getCachedInternalReferenceForApp(String appName, ReadableMap props) { String key = props.getString("key"); String path = props.getString("path"); ReadableArray modifiers = props.getArray("modifiers"); @@ -511,14 +505,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { RNFirebaseDatabaseReference existingRef = references.get(key); if (existingRef == null) { - existingRef = new RNFirebaseDatabaseReference( - getReactApplicationContext(), - appName, - key, - path, - modifiers - ); - + existingRef = getInternalReferenceForApp(appName, key, path, modifiers); references.put(key, existingRef); } diff --git a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java index 8139e527..982189b5 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java +++ b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java @@ -31,8 +31,6 @@ import io.invertase.firebase.Utils; public class FirestoreSerialize { private static final String TAG = "FirestoreSerialize"; - private static final DateFormat READ_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); - private static final DateFormat WRITE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); private static final String KEY_CHANGES = "changes"; private static final String KEY_DATA = "data"; private static final String KEY_DOC_CHANGE_DOCUMENT = "document"; @@ -43,12 +41,6 @@ public class FirestoreSerialize { private static final String KEY_METADATA = "metadata"; private static final String KEY_PATH = "path"; - static { - // Javascript Date.toISOString is always formatted to UTC - // We set the read TimeZone to UTC to account for this - READ_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); - } - /** * Convert a DocumentSnapshot instance into a React Native WritableMap * @@ -220,7 +212,7 @@ public class FirestoreSerialize { typeMap.putMap("value", geoPoint); } else if (value instanceof Date) { typeMap.putString("type", "date"); - typeMap.putString("value", WRITE_DATE_FORMAT.format((Date) value)); + typeMap.putDouble("value", ((Date) value).getTime()); } else { // TODO: Changed to log an error rather than crash - is this correct? Log.e(TAG, "buildTypeMap: Cannot convert object of type " + value.getClass()); @@ -275,13 +267,8 @@ public class FirestoreSerialize { ReadableMap geoPoint = typeMap.getMap("value"); return new GeoPoint(geoPoint.getDouble("latitude"), geoPoint.getDouble("longitude")); } else if ("date".equals(type)) { - try { - String date = typeMap.getString("value"); - return READ_DATE_FORMAT.parse(date); - } catch (ParseException exception) { - Log.e(TAG, "parseTypeMap", exception); - return null; - } + Double time = typeMap.getDouble("value"); + return new Date(time.longValue()); } else if ("fieldvalue".equals(type)) { String value = typeMap.getString("value"); if ("delete".equals(value)) { diff --git a/ios/RNFirebase.xcodeproj/project.pbxproj b/ios/RNFirebase.xcodeproj/project.pbxproj index ac43d99f..7b6b7675 100644 --- a/ios/RNFirebase.xcodeproj/project.pbxproj +++ b/ios/RNFirebase.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 839D91741EF3E20B0077C7C8 /* RNFirebaseMessaging.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D91651EF3E20A0077C7C8 /* RNFirebaseMessaging.m */; }; 839D91751EF3E20B0077C7C8 /* RNFirebasePerformance.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D91681EF3E20A0077C7C8 /* RNFirebasePerformance.m */; }; 839D91761EF3E20B0077C7C8 /* RNFirebaseStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D916B1EF3E20A0077C7C8 /* RNFirebaseStorage.m */; }; + 83C3EEEE1FA1EACC00B64D3C /* RNFirebaseUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 83C3EEEC1FA1EACC00B64D3C /* RNFirebaseUtil.m */; }; D950369E1D19C77400F7094D /* RNFirebase.m in Sources */ = {isa = PBXBuildFile; fileRef = D950369D1D19C77400F7094D /* RNFirebase.m */; }; /* End PBXBuildFile section */ @@ -85,6 +86,8 @@ 839D916A1EF3E20A0077C7C8 /* RNFirebaseStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseStorage.h; sourceTree = ""; }; 839D916B1EF3E20A0077C7C8 /* RNFirebaseStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseStorage.m; sourceTree = ""; }; 839D91771EF3E22F0077C7C8 /* RNFirebaseEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNFirebaseEvents.h; path = RNFirebase/RNFirebaseEvents.h; sourceTree = ""; }; + 83C3EEEC1FA1EACC00B64D3C /* RNFirebaseUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNFirebaseUtil.m; path = RNFirebase/RNFirebaseUtil.m; sourceTree = ""; }; + 83C3EEED1FA1EACC00B64D3C /* RNFirebaseUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNFirebaseUtil.h; path = RNFirebase/RNFirebaseUtil.h; sourceTree = ""; }; D950369C1D19C77400F7094D /* RNFirebase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNFirebase.h; path = RNFirebase/RNFirebase.h; sourceTree = ""; }; D950369D1D19C77400F7094D /* RNFirebase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNFirebase.m; path = RNFirebase/RNFirebase.m; sourceTree = ""; }; /* End PBXFileReference section */ @@ -131,11 +134,13 @@ 8376F70D1F7C141500D45A85 /* firestore */, 839D91631EF3E20A0077C7C8 /* messaging */, 839D91661EF3E20A0077C7C8 /* perf */, - 134814211AA4EA7D00B7C361 /* Products */, + 134814211AA4EA7D00B7C361 /* storage */, D950369C1D19C77400F7094D /* RNFirebase.h */, D950369D1D19C77400F7094D /* RNFirebase.m */, 839D91771EF3E22F0077C7C8 /* RNFirebaseEvents.h */, - 839D91691EF3E20A0077C7C8 /* storage */, + 83C3EEED1FA1EACC00B64D3C /* RNFirebaseUtil.h */, + 83C3EEEC1FA1EACC00B64D3C /* RNFirebaseUtil.m */, + 134814211AA4EA7D00B7C361 /* Products */, ); sourceTree = ""; }; @@ -327,6 +332,7 @@ D950369E1D19C77400F7094D /* RNFirebase.m in Sources */, 839D91731EF3E20B0077C7C8 /* RNFirebaseDatabase.m in Sources */, 8323CF071F6FBD870071420B /* NativeExpressComponent.m in Sources */, + 83C3EEEE1FA1EACC00B64D3C /* RNFirebaseUtil.m in Sources */, 839D91721EF3E20B0077C7C8 /* RNFirebaseCrash.m in Sources */, 839D91741EF3E20B0077C7C8 /* RNFirebaseMessaging.m in Sources */, 839D91751EF3E20B0077C7C8 /* RNFirebasePerformance.m in Sources */, diff --git a/ios/RNFirebase/RNFirebaseUtil.h b/ios/RNFirebase/RNFirebaseUtil.h new file mode 100644 index 00000000..ab0ef52d --- /dev/null +++ b/ios/RNFirebase/RNFirebaseUtil.h @@ -0,0 +1,14 @@ +#ifndef RNFirebaseUtil_h +#define RNFirebaseUtil_h + +#import +#import + +@interface RNFirebaseUtil : NSObject + ++ (void)sendJSEvent:(RCTEventEmitter *)emitter name:(NSString *)name body:(NSDictionary *)body; ++ (void)sendJSEventWithAppName:(RCTEventEmitter *)emitter appName:(NSString *)appName name:(NSString *)name body:(NSDictionary *)body; + +@end + +#endif diff --git a/ios/RNFirebase/RNFirebaseUtil.m b/ios/RNFirebase/RNFirebaseUtil.m new file mode 100644 index 00000000..898e31cb --- /dev/null +++ b/ios/RNFirebase/RNFirebaseUtil.m @@ -0,0 +1,25 @@ +#import "RNFirebaseUtil.h" + +@implementation RNFirebaseUtil + ++ (void)sendJSEvent:(RCTEventEmitter *)emitter name:(NSString *)name body:(NSDictionary *)body { + @try { + // TODO: Temporary fix for https://github.com/invertase/react-native-firebase/issues/233 + // until a better solution comes around + if (emitter.bridge) { + [emitter sendEventWithName:name body:body]; + } + } @catch (NSException *error) { + NSLog(@"An error occurred in sendJSEvent: %@", [error debugDescription]); + } +} + ++ (void)sendJSEventWithAppName:(RCTEventEmitter *)emitter appName:(NSString *)appName name:(NSString *)name body:(NSDictionary *)body { + // Add the appName to the body + NSMutableDictionary *newBody = [body mutableCopy]; + newBody[@"appName"] = appName; + + [RNFirebaseUtil sendJSEvent:emitter name:name body:newBody]; +} + +@end diff --git a/ios/RNFirebase/admob/RNFirebaseAdMobInterstitial.m b/ios/RNFirebase/admob/RNFirebaseAdMobInterstitial.m index 0846632a..04a22400 100644 --- a/ios/RNFirebase/admob/RNFirebaseAdMobInterstitial.m +++ b/ios/RNFirebase/admob/RNFirebaseAdMobInterstitial.m @@ -1,4 +1,5 @@ #import "RNFirebaseAdMobInterstitial.h" +#import "RNFirebaseUtil.h" @implementation RNFirebaseAdMobInterstitial @@ -31,7 +32,7 @@ } - (void)sendJSEvent:(NSString *)type payload:(NSDictionary *)payload { - [_delegate sendEventWithName:ADMOB_INTERSTITIAL_EVENT body:@{ + [RNFirebaseUtil sendJSEvent:self.delegate name:ADMOB_INTERSTITIAL_EVENT body:@{ @"type": type, @"adUnit": _adUnitID, @"payload": payload diff --git a/ios/RNFirebase/admob/RNFirebaseAdMobRewardedVideo.m b/ios/RNFirebase/admob/RNFirebaseAdMobRewardedVideo.m index 6c08ac09..0e2a27ba 100644 --- a/ios/RNFirebase/admob/RNFirebaseAdMobRewardedVideo.m +++ b/ios/RNFirebase/admob/RNFirebaseAdMobRewardedVideo.m @@ -1,4 +1,5 @@ #import "RNFirebaseAdMobRewardedVideo.h" +#import "RNFirebaseUtil.h" @implementation RNFirebaseAdMobRewardedVideo @@ -31,7 +32,7 @@ } - (void)sendJSEvent:(NSString *)type payload:(NSDictionary *)payload { - [_delegate sendEventWithName:ADMOB_REWARDED_VIDEO_EVENT body:@{ + [RNFirebaseUtil sendJSEvent:self.delegate name:ADMOB_REWARDED_VIDEO_EVENT body:@{ @"type": type, @"adUnit": _adUnitID, @"payload": payload @@ -73,4 +74,4 @@ #endif -@end \ No newline at end of file +@end diff --git a/ios/RNFirebase/auth/RNFirebaseAuth.m b/ios/RNFirebase/auth/RNFirebaseAuth.m index 66a60dfb..1aa0004c 100644 --- a/ios/RNFirebase/auth/RNFirebaseAuth.m +++ b/ios/RNFirebase/auth/RNFirebaseAuth.m @@ -1,5 +1,6 @@ #import "RNFirebaseAuth.h" #import "RNFirebaseEvents.h" +#import "RNFirebaseUtil.h" #import "RCTDefines.h" @@ -28,9 +29,9 @@ RCT_EXPORT_METHOD(addAuthStateListener: FIRApp *firApp = [FIRApp appNamed:appName]; FIRAuthStateDidChangeListenerHandle newListenerHandle = [[FIRAuth authWithApp:firApp] addAuthStateDidChangeListener:^(FIRAuth *_Nonnull auth, FIRUser *_Nullable user) { if (user != nil) { - [self sendJSEventWithAppName:appName title:AUTH_CHANGED_EVENT props:[@{@"authenticated": @(true), @"user": [self firebaseUserToDict:user]} mutableCopy]]; + [RNFirebaseUtil sendJSEventWithAppName:self appName:appName name:AUTH_CHANGED_EVENT body:@{@"authenticated": @(true), @"user": [self firebaseUserToDict:user]}]; } else { - [self sendJSEventWithAppName:appName title:AUTH_CHANGED_EVENT props:[@{@"authenticated": @(false)} mutableCopy]]; + [RNFirebaseUtil sendJSEventWithAppName:self appName:appName name:AUTH_CHANGED_EVENT body:@{@"authenticated": @(false)}]; } }]; @@ -63,9 +64,9 @@ RCT_EXPORT_METHOD(addIdTokenListener: FIRApp *firApp = [FIRApp appNamed:appName]; FIRIDTokenDidChangeListenerHandle newListenerHandle = [[FIRAuth authWithApp:firApp] addIDTokenDidChangeListener:^(FIRAuth * _Nonnull auth, FIRUser * _Nullable user) { if (user != nil) { - [self sendJSEventWithAppName:appName title:AUTH_ID_TOKEN_CHANGED_EVENT props:[@{@"authenticated": @(true), @"user": [self firebaseUserToDict:user]} mutableCopy]]; + [RNFirebaseUtil sendJSEventWithAppName:self appName:appName name:AUTH_ID_TOKEN_CHANGED_EVENT body:@{@"authenticated": @(true), @"user": [self firebaseUserToDict:user]}]; } else { - [self sendJSEventWithAppName:appName title:AUTH_ID_TOKEN_CHANGED_EVENT props:[@{@"authenticated": @(false)} mutableCopy]]; + [RNFirebaseUtil sendJSEventWithAppName:self appName:appName name:AUTH_ID_TOKEN_CHANGED_EVENT body:@{@"authenticated": @(false)}]; } }]; @@ -248,14 +249,7 @@ RCT_EXPORT_METHOD(reload: FIRUser *user = [FIRAuth authWithApp:firApp].currentUser; if (user) { - [user reloadWithCompletion:^(NSError *_Nullable error) { - if (error) { - [self promiseRejectAuthException:reject error:error]; - } else { - FIRUser *userAfterReload = [FIRAuth authWithApp:firApp].currentUser; - [self promiseWithUser:resolve rejecter:reject user:userAfterReload]; - } - }]; + [self reloadAndReturnUser:user resolver:resolve rejecter: reject]; } else { [self promiseNoUser:resolve rejecter:reject isError:YES]; } @@ -315,8 +309,7 @@ RCT_EXPORT_METHOD(updateEmail: if (error) { [self promiseRejectAuthException:reject error:error]; } else { - FIRUser *userAfterUpdate = [FIRAuth authWithApp:firApp].currentUser; - [self promiseWithUser:resolve rejecter:reject user:userAfterUpdate]; + [self reloadAndReturnUser:user resolver:resolve rejecter: reject]; } }]; } else { @@ -399,8 +392,7 @@ RCT_EXPORT_METHOD(updateProfile: if (error) { [self promiseRejectAuthException:reject error:error]; } else { - FIRUser *userAfterUpdate = [FIRAuth authWithApp:firApp].currentUser; - [self promiseWithUser:resolve rejecter:reject user:userAfterUpdate]; + [self reloadAndReturnUser:user resolver:resolve rejecter: reject]; } }]; } else { @@ -686,21 +678,21 @@ RCT_EXPORT_METHOD(verifyPhoneNumber:(NSString *) appName [[FIRPhoneAuthProvider providerWithAuth:[FIRAuth authWithApp:firApp]] verifyPhoneNumber:phoneNumber UIDelegate:nil completion:^(NSString * _Nullable verificationID, NSError * _Nullable error) { if (error) { NSDictionary * jsError = [self getJSError:(error)]; - NSMutableDictionary * props = [@{ - @"type": @"onVerificationFailed", - @"requestKey":requestKey, - @"state": @{@"error": jsError}, - } mutableCopy]; - [self sendJSEventWithAppName:appName title:PHONE_AUTH_STATE_CHANGED_EVENT props: props]; + NSDictionary *body = @{ + @"type": @"onVerificationFailed", + @"requestKey":requestKey, + @"state": @{@"error": jsError}, + }; + [RNFirebaseUtil sendJSEventWithAppName:self appName:appName name:PHONE_AUTH_STATE_CHANGED_EVENT body:body]; } else { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setObject:verificationID forKey:@"authVerificationID"]; - NSMutableDictionary * props = [@{ - @"type": @"onCodeSent", - @"requestKey":requestKey, - @"state": @{@"verificationId": verificationID}, - } mutableCopy]; - [self sendJSEventWithAppName:appName title:PHONE_AUTH_STATE_CHANGED_EVENT props: props]; + NSDictionary *body = @{ + @"type": @"onCodeSent", + @"requestKey":requestKey, + @"state": @{@"verificationId": verificationID}, + }; + [RNFirebaseUtil sendJSEventWithAppName:self appName:appName name:PHONE_AUTH_STATE_CHANGED_EVENT body:body]; } }]; } @@ -794,15 +786,7 @@ RCT_EXPORT_METHOD(unlink: if (error) { [self promiseRejectAuthException:reject error:error]; } else { - // This is here to protect against bugs in the iOS SDK which don't - // correctly refresh the user object when unlinking certain accounts - [user reloadWithCompletion:^(NSError * _Nullable error) { - if (error) { - [self promiseRejectAuthException:reject error:error]; - } else { - [self promiseWithUser:resolve rejecter:reject user:user]; - } - }]; + [self reloadAndReturnUser:user resolver:resolve rejecter: reject]; } }]; } else { @@ -916,6 +900,19 @@ RCT_EXPORT_METHOD(fetchProvidersForEmail: return credential; } +// This is here to protect against bugs in the iOS SDK which don't +// correctly refresh the user object when performing certain operations +- (void)reloadAndReturnUser:(FIRUser *)user + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject { + [user reloadWithCompletion:^(NSError * _Nullable error) { + if (error) { + [self promiseRejectAuthException:reject error:error]; + } else { + [self promiseWithUser:resolve rejecter:reject user:user]; + } + }]; +} /** Resolve or reject a promise based on isError value @@ -1087,31 +1084,6 @@ RCT_EXPORT_METHOD(fetchProvidersForEmail: } - -/** - wrapper for sendEventWithName for auth events - - @param title sendEventWithName - @param props <#props description#> - */ -- (void)sendJSEvent:(NSString *)title props:(NSDictionary *)props { - @try { - [self sendEventWithName:title body:props]; - } @catch (NSException *error) { - NSLog(@"An error occurred in sendJSEvent: %@", [error debugDescription]); - } -} - -- (void)sendJSEventWithAppName:(NSString *)appName title:(NSString *)title props:(NSMutableDictionary *)props { - props[@"appName"] = appName; - - @try { - [self sendEventWithName:title body:props]; - } @catch (NSException *error) { - NSLog(@"An error occurred in sendJSEvent: %@", [error debugDescription]); - } -} - /** Converts an array of FIRUserInfo instances into the correct format to match the web sdk diff --git a/ios/RNFirebase/database/RNFirebaseDatabase.m b/ios/RNFirebase/database/RNFirebaseDatabase.m index 70efdf8d..4185e1af 100644 --- a/ios/RNFirebase/database/RNFirebaseDatabase.m +++ b/ios/RNFirebase/database/RNFirebaseDatabase.m @@ -5,6 +5,7 @@ #import #import "RNFirebaseDatabaseReference.h" #import "RNFirebaseEvents.h" +#import "RNFirebaseUtil.h" @implementation RNFirebaseDatabase RCT_EXPORT_MODULE(); @@ -39,7 +40,7 @@ RCT_EXPORT_METHOD(keepSynced:(NSString *) appName path:(NSString *) path modifiers:(NSArray *) modifiers state:(BOOL) state) { - FIRDatabaseQuery *query = [self getInternalReferenceForApp:appName key:key path:path modifiers:modifiers keep:false].query; + FIRDatabaseQuery *query = [self getInternalReferenceForApp:appName key:key path:path modifiers:modifiers].query; [query keepSynced:state]; } @@ -87,11 +88,7 @@ RCT_EXPORT_METHOD(transactionStart:(NSString *) appName dispatch_barrier_async(_transactionQueue, ^{ [_transactions setValue:transactionState forKey:transactionId]; NSDictionary *updateMap = [self createTransactionUpdateMap:appName transactionId:transactionId updatesData:currentData]; - // TODO: Temporary fix for https://github.com/invertase/react-native-firebase/issues/233 - // until a better solution comes around - if (self.bridge) { - [self sendEventWithName:DATABASE_TRANSACTION_EVENT body:updateMap]; - } + [RNFirebaseUtil sendJSEvent:self name:DATABASE_TRANSACTION_EVENT body:updateMap]; }); // wait for the js event handler to call tryCommitTransaction @@ -118,11 +115,7 @@ RCT_EXPORT_METHOD(transactionStart:(NSString *) appName andCompletionBlock: ^(NSError *_Nullable databaseError, BOOL committed, FIRDataSnapshot *_Nullable snapshot) { NSDictionary *resultMap = [self createTransactionResultMap:appName transactionId:transactionId error:databaseError committed:committed snapshot:snapshot]; - // TODO: Temporary fix for https://github.com/invertase/react-native-firebase/issues/233 - // until a better solution comes around - if (self.bridge) { - [self sendEventWithName:DATABASE_TRANSACTION_EVENT body:resultMap]; - } + [RNFirebaseUtil sendJSEvent:self name:DATABASE_TRANSACTION_EVENT body:resultMap]; } withLocalEvents: applyLocally]; @@ -233,13 +226,13 @@ RCT_EXPORT_METHOD(once:(NSString *) appName eventName:(NSString *) eventName resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) { - RNFirebaseDatabaseReference *ref = [self getInternalReferenceForApp:appName key:key path:path modifiers:modifiers keep:false]; + RNFirebaseDatabaseReference *ref = [self getInternalReferenceForApp:appName key:key path:path modifiers:modifiers]; [ref once:eventName resolver:resolve rejecter:reject]; } RCT_EXPORT_METHOD(on:(NSString *) appName props:(NSDictionary *) props) { - RNFirebaseDatabaseReference *ref = [self getInternalReferenceForApp:appName key:props[@"key"] path:props[@"path"] modifiers:props[@"modifiers"] keep:false]; + RNFirebaseDatabaseReference *ref = [self getCachedInternalReferenceForApp:appName props:props]; [ref on:props[@"eventType"] registration:props[@"registration"]]; } @@ -278,15 +271,20 @@ RCT_EXPORT_METHOD(off:(NSString *) key return [[RNFirebaseDatabase getDatabaseForApp:appName] referenceWithPath:path]; } -- (RNFirebaseDatabaseReference *)getInternalReferenceForApp:(NSString *)appName key:(NSString *)key path:(NSString *)path modifiers:(NSArray *)modifiers keep:(BOOL)keep { +- (RNFirebaseDatabaseReference *)getInternalReferenceForApp:(NSString *)appName key:(NSString *)key path:(NSString *)path modifiers:(NSArray *)modifiers { + return [[RNFirebaseDatabaseReference alloc] initWithPathAndModifiers:self app:appName key:key refPath:path modifiers:modifiers]; +} + +- (RNFirebaseDatabaseReference *)getCachedInternalReferenceForApp:(NSString *)appName props:(NSDictionary *)props { + NSString *key = props[@"key"]; + NSString *path = props[@"path"]; + NSDictionary *modifiers = props[@"modifiers"]; + RNFirebaseDatabaseReference *ref = _dbReferences[key]; if (ref == nil) { ref = [[RNFirebaseDatabaseReference alloc] initWithPathAndModifiers:self app:appName key:key refPath:path modifiers:modifiers]; - - if (keep) { - _dbReferences[key] = ref; - } + _dbReferences[key] = ref; } return ref; } diff --git a/ios/RNFirebase/database/RNFirebaseDatabaseReference.h b/ios/RNFirebase/database/RNFirebaseDatabaseReference.h index c25f0b17..ec02e502 100644 --- a/ios/RNFirebase/database/RNFirebaseDatabaseReference.h +++ b/ios/RNFirebase/database/RNFirebaseDatabaseReference.h @@ -6,6 +6,7 @@ #import #import "RNFirebaseDatabase.h" #import "RNFirebaseEvents.h" +#import "RNFirebaseUtil.h" #import @interface RNFirebaseDatabaseReference : NSObject diff --git a/ios/RNFirebase/database/RNFirebaseDatabaseReference.m b/ios/RNFirebase/database/RNFirebaseDatabaseReference.m index 35737536..3a3d6730 100644 --- a/ios/RNFirebase/database/RNFirebaseDatabaseReference.m +++ b/ios/RNFirebase/database/RNFirebaseDatabaseReference.m @@ -71,11 +71,7 @@ [event setValue:eventType forKey:@"eventType"]; [event setValue:registration forKey:@"registration"]; - // TODO: Temporary fix for https://github.com/invertase/react-native-firebase/issues/233 - // until a better solution comes around - if (_emitter.bridge) { - [_emitter sendEventWithName:DATABASE_SYNC_EVENT body:event]; - } + [RNFirebaseUtil sendJSEvent:self.emitter name:DATABASE_SYNC_EVENT body:event]; } - (void)handleDatabaseError:(NSDictionary *) registration @@ -85,11 +81,7 @@ [event setValue:[RNFirebaseDatabase getJSError:error] forKey:@"error"]; [event setValue:registration forKey:@"registration"]; - // TODO: Temporary fix for https://github.com/invertase/react-native-firebase/issues/233 - // until a better solution comes around - if (_emitter) { - [_emitter sendEventWithName:DATABASE_SYNC_EVENT body:event]; - } + [RNFirebaseUtil sendJSEvent:self.emitter name:DATABASE_SYNC_EVENT body:event]; } + (NSDictionary *)snapshotToDictionary:(FIRDataSnapshot *) dataSnapshot diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h index 7371f2b1..8fc07cbe 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h @@ -9,6 +9,7 @@ #import "RNFirebaseEvents.h" #import "RNFirebaseFirestore.h" #import "RNFirebaseFirestoreDocumentReference.h" +#import "RNFirebaseUtil.h" @interface RNFirebaseFirestoreCollectionReference : NSObject @property RCTEventEmitter *emitter; diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m index 99f8d914..b3816340 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m @@ -128,6 +128,9 @@ queryListenOptions:(NSDictionary *) queryListenOptions { if (_options[@"endBefore"]) { query = [query queryEndingBeforeValues:_options[@"endBefore"]]; } + if (_options[@"limit"]) { + query = [query queryLimitedTo:_options[@"limit"]]; + } if (_options[@"offset"]) { // iOS doesn't support offset } @@ -151,11 +154,7 @@ queryListenOptions:(NSDictionary *) queryListenOptions { [event setValue:listenerId forKey:@"listenerId"]; [event setValue:[RNFirebaseFirestore getJSError:error] forKey:@"error"]; - // TODO: Temporary fix for https://github.com/invertase/react-native-firebase/issues/233 - // until a better solution comes around - if (_emitter.bridge) { - [_emitter sendEventWithName:FIRESTORE_COLLECTION_SYNC_EVENT body:event]; - } + [RNFirebaseUtil sendJSEvent:self.emitter name:FIRESTORE_COLLECTION_SYNC_EVENT body:event]; } - (void)handleQuerySnapshotEvent:(NSString *)listenerId @@ -166,11 +165,7 @@ queryListenOptions:(NSDictionary *) queryListenOptions { [event setValue:listenerId forKey:@"listenerId"]; [event setValue:[RNFirebaseFirestoreCollectionReference snapshotToDictionary:querySnapshot] forKey:@"querySnapshot"]; - // TODO: Temporary fix for https://github.com/invertase/react-native-firebase/issues/233 - // until a better solution comes around - if (_emitter.bridge) { - [_emitter sendEventWithName:FIRESTORE_COLLECTION_SYNC_EVENT body:event]; - } + [RNFirebaseUtil sendJSEvent:self.emitter name:FIRESTORE_COLLECTION_SYNC_EVENT body:event]; } + (NSDictionary *)snapshotToDictionary:(FIRQuerySnapshot *)querySnapshot { diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h index f3178e8c..dd729bf8 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h @@ -9,6 +9,7 @@ #import #import "RNFirebaseEvents.h" #import "RNFirebaseFirestore.h" +#import "RNFirebaseUtil.h" @interface RNFirebaseFirestoreDocumentReference : NSObject @property RCTEventEmitter *emitter; diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m index caa5495f..d9c5a950 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m @@ -136,11 +136,7 @@ static NSMutableDictionary *_listeners; [event setValue:listenerId forKey:@"listenerId"]; [event setValue:[RNFirebaseFirestore getJSError:error] forKey:@"error"]; - // TODO: Temporary fix for https://github.com/invertase/react-native-firebase/issues/233 - // until a better solution comes around - if (_emitter.bridge) { - [_emitter sendEventWithName:FIRESTORE_DOCUMENT_SYNC_EVENT body:event]; - } + [RNFirebaseUtil sendJSEvent:self.emitter name:FIRESTORE_DOCUMENT_SYNC_EVENT body:event]; } - (void)handleDocumentSnapshotEvent:(NSString *)listenerId @@ -151,11 +147,7 @@ static NSMutableDictionary *_listeners; [event setValue:listenerId forKey:@"listenerId"]; [event setValue:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:documentSnapshot] forKey:@"documentSnapshot"]; - // TODO: Temporary fix for https://github.com/invertase/react-native-firebase/issues/233 - // until a better solution comes around - if (_emitter.bridge) { - [_emitter sendEventWithName:FIRESTORE_DOCUMENT_SYNC_EVENT body:event]; - } + [RNFirebaseUtil sendJSEvent:self.emitter name:FIRESTORE_DOCUMENT_SYNC_EVENT body:event]; } @@ -205,9 +197,9 @@ static NSMutableDictionary *_listeners; typeMap[@"value"] = geopoint; } else if ([value isKindOfClass:[NSDate class]]) { typeMap[@"type"] = @"date"; - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZ"]; - typeMap[@"value"] = [dateFormatter stringFromDate:(NSDate *)value]; + // NOTE: The round() is important as iOS ends up giving .999 otherwise, + // and loses a millisecond when going between native and JS + typeMap[@"value"] = @(round([(NSDate *)value timeIntervalSince1970] * 1000.0)); } else if ([value isKindOfClass:[NSNumber class]]) { NSNumber *number = (NSNumber *)value; if (number == (void*)kCFBooleanFalse || number == (void*)kCFBooleanTrue) { @@ -262,9 +254,7 @@ static NSMutableDictionary *_listeners; NSNumber *longitude = geopoint[@"longitude"]; return [[FIRGeoPoint alloc] initWithLatitude:[latitude doubleValue] longitude:[longitude doubleValue]]; } else if ([type isEqualToString:@"date"]) { - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZ"]; - return [dateFormatter dateFromString:value]; + return [NSDate dateWithTimeIntervalSince1970:([(NSNumber *)value doubleValue] / 1000.0)]; } else if ([type isEqualToString:@"fieldvalue"]) { NSString *string = (NSString*)value; if ([string isEqualToString:@"delete"]) { diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 22a948d2..36294b79 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -3,6 +3,7 @@ @import UserNotifications; #if __has_include() #import "RNFirebaseEvents.h" +#import "RNFirebaseUtil.h" #import #import @@ -217,7 +218,7 @@ RCT_EXPORT_MODULE() data[@"_completionHandlerId"] = completionHandlerId; } - [self sendEventWithName:MESSAGING_NOTIFICATION_RECEIVED body:data]; + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_NOTIFICATION_RECEIVED body:data]; } @@ -234,13 +235,13 @@ RCT_EXPORT_MODULE() // ** Start FIRMessagingDelegate methods ** // Handle data messages in the background - (void)applicationReceivedRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage { - [self sendEventWithName:MESSAGING_NOTIFICATION_RECEIVED body:[remoteMessage appData]]; + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_NOTIFICATION_RECEIVED body:[remoteMessage appData]]; } // Listen for token refreshes - (void)messaging:(nonnull FIRMessaging *)messaging didRefreshRegistrationToken:(nonnull NSString *)fcmToken { NSLog(@"FCM registration token: %@", fcmToken); - [self sendEventWithName:MESSAGING_TOKEN_REFRESHED body:fcmToken]; + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_TOKEN_REFRESHED body:fcmToken]; } // ** End FIRMessagingDelegate methods ** @@ -297,7 +298,9 @@ RCT_EXPORT_METHOD(requestPermissions:(RCTPromiseResolveBlock)resolve rejecter:(R #endif } - [RCTSharedApplication() registerForRemoteNotifications]; + dispatch_async(dispatch_get_main_queue(), ^{ + [RCTSharedApplication() registerForRemoteNotifications]; + }); } RCT_EXPORT_METHOD(subscribeToTopic: (NSString*) topic) { diff --git a/ios/RNFirebase/storage/RNFirebaseStorage.m b/ios/RNFirebase/storage/RNFirebaseStorage.m index b67f5173..e73038d5 100644 --- a/ios/RNFirebase/storage/RNFirebaseStorage.m +++ b/ios/RNFirebase/storage/RNFirebaseStorage.m @@ -3,6 +3,7 @@ #if __has_include() #import "RNFirebaseEvents.h" +#import "RNFirebaseUtil.h" #import #import #import @@ -392,12 +393,7 @@ RCT_EXPORT_METHOD(putFile:(NSString *) appName } - (void)sendJSEvent:(NSString *)appName type:(NSString *)type path:(NSString *)path title:(NSString *)title props:(NSDictionary *)props { - @try { - [self sendEventWithName:type body:@{@"eventName": title, @"appName": appName, @"path": path, @"body": props}]; - } @catch (NSException *err) { - NSLog(@"An error occurred in sendJSEvent: %@", [err debugDescription]); - NSLog(@"Tried to send: %@ with %@", title, props); - } + [RNFirebaseUtil sendJSEvent:self name:type body:@{@"eventName": title, @"appName": appName, @"path": path, @"body": props}]; } /** diff --git a/lib/modules/auth/PhoneAuthListener.js b/lib/modules/auth/PhoneAuthListener.js index 942780be..620ad7f1 100644 --- a/lib/modules/auth/PhoneAuthListener.js +++ b/lib/modules/auth/PhoneAuthListener.js @@ -141,18 +141,14 @@ export default class PhoneAuthListener { _removeAllListeners() { setTimeout(() => { // move to next event loop - not sure if needed // internal listeners - const events = Object.values(this._internalEvents); - - for (let i = 0, len = events.length; i < len; i++) { - this._auth.removeAllListeners(events[i]); - } + Object.values(this._internalEvents).forEach((event) => { + this._auth.removeAllListeners(event); + }); // user observer listeners - const publicEvents = Object.values(this._publicEvents); - - for (let i = 0, len = events.length; i < len; i++) { - this._auth.removeAllListeners(publicEvents[i]); - } + Object.values(this._publicEvents).forEach((publicEvent) => { + this._auth.removeAllListeners(publicEvent); + }); }, 0); } diff --git a/lib/modules/auth/index.js b/lib/modules/auth/index.js index 675d0f07..378a23aa 100644 --- a/lib/modules/auth/index.js +++ b/lib/modules/auth/index.js @@ -22,33 +22,31 @@ export default class Auth extends ModuleBase { _native: Object; _getAppEventName: Function; _authResult: AuthResultType | null; - authenticated: boolean; constructor(firebaseApp: Object, options: Object = {}) { super(firebaseApp, options, true); this._user = null; this._authResult = null; - this.authenticated = false; this.addListener( // sub to internal native event - this fans out to // public event name: onAuthStateChanged this._getAppEventName('auth_state_changed'), - this._onAuthStateChanged.bind(this), + this._onInternalAuthStateChanged.bind(this), ); this.addListener( // sub to internal native event - this fans out to // public events based on event.type this._getAppEventName('phone_auth_state_changed'), - this._onPhoneAuthStateChanged.bind(this), + this._onInternalPhoneAuthStateChanged.bind(this), ); this.addListener( // sub to internal native event - this fans out to // public event name: onIdTokenChanged this._getAppEventName('auth_id_token_changed'), - this._onIdTokenChanged.bind(this), + this._onInternalIdTokenChanged.bind(this), ); this._native.addAuthStateListener(); @@ -60,34 +58,25 @@ export default class Auth extends ModuleBase { * @param event * @private */ - _onPhoneAuthStateChanged(event: Object) { + _onInternalPhoneAuthStateChanged(event: Object) { const eventKey = `phone:auth:${event.requestKey}:${event.type}`; this.emit(eventKey, event.state); } - /** - * Internal auth changed listener - * @param auth - * @param emit - * @private - */ - _onAuthStateChanged(auth: AuthResultType, emit: boolean = true) { + _setAuthState(auth: AuthResultType) { this._authResult = auth; - this.authenticated = auth ? auth.authenticated || false : false; - if (auth && auth.user && !this._user) this._user = new User(this, auth); - else if ((!auth || !auth.user) && this._user) this._user = null; - else if (this._user) this._user._updateValues(auth); - if (emit) this.emit(this._getAppEventName('onAuthStateChanged'), this._user); - return auth ? this._user : null; + this._user = auth && auth.user ? new User(this, auth.user) : null; + this.emit(this._getAppEventName('onUserChanged'), this._user); } /** - * Remove auth change listener - * @param listener + * Internal auth changed listener + * @param auth + * @private */ - _offAuthStateChanged(listener: Function) { - this.log.info('Removing onAuthStateChanged listener'); - this.removeListener(this._getAppEventName('onAuthStateChanged'), listener); + _onInternalAuthStateChanged(auth: AuthResultType) { + this._setAuthState(auth); + this.emit(this._getAppEventName('onAuthStateChanged'), this._user); } /** @@ -96,23 +85,9 @@ export default class Auth extends ModuleBase { * @param emit * @private */ - _onIdTokenChanged(auth: AuthResultType, emit: boolean = true) { - this._authResult = auth; - this.authenticated = auth ? auth.authenticated || false : false; - if (auth && auth.user && !this._user) this._user = new User(this, auth); - else if ((!auth || !auth.user) && this._user) this._user = null; - else if (this._user) this._user._updateValues(auth); - if (emit) this.emit(this._getAppEventName('onIdTokenChanged'), this._user); - return auth ? this._user : null; - } - - /** - * Remove id token change listener - * @param listener - */ - _offIdTokenChanged(listener: Function) { - this.log.info('Removing onIdTokenChanged listener'); - this.removeListener(this._getAppEventName('onIdTokenChanged'), listener); + _onInternalIdTokenChanged(auth: AuthResultType) { + this._setAuthState(auth); + this.emit(this._getAppEventName('onIdTokenChanged'), this._user); } /** @@ -124,10 +99,10 @@ export default class Auth extends ModuleBase { */ _interceptUserValue(promise) { return promise.then((result) => { - if (!result) return this._onAuthStateChanged(null, false); - if (result.user) return this._onAuthStateChanged(result, false); - if (result.uid) return this._onAuthStateChanged({ authenticated: true, user: result }, false); - return result; + if (!result) this._setAuthState(null); + else if (result.user) this._setAuthState(result); + else if (result.uid) this._setAuthState({ authenticated: true, user: result }); + return this._user; }); } @@ -146,6 +121,15 @@ export default class Auth extends ModuleBase { return this._offAuthStateChanged.bind(this, listener); } + /** + * Remove auth change listener + * @param listener + */ + _offAuthStateChanged(listener: Function) { + this.log.info('Removing onAuthStateChanged listener'); + this.removeListener(this._getAppEventName('onAuthStateChanged'), listener); + } + /** * Listen for id token changes. * @param listener @@ -157,6 +141,35 @@ export default class Auth extends ModuleBase { return this._offIdTokenChanged.bind(this, listener); } + /** + * Remove id token change listener + * @param listener + */ + _offIdTokenChanged(listener: Function) { + this.log.info('Removing onIdTokenChanged listener'); + this.removeListener(this._getAppEventName('onIdTokenChanged'), listener); + } + + /** + * Listen for user changes. + * @param listener + */ + onUserChanged(listener: Function) { + this.log.info('Creating onUserChanged listener'); + this.on(this._getAppEventName('onUserChanged'), listener); + if (this._authResult) listener(this._user || null); + return this._offUserChanged.bind(this, listener); + } + + /** + * Remove user change listener + * @param listener + */ + _offUserChanged(listener: Function) { + this.log.info('Removing onUserChanged listener'); + this.removeListener(this._getAppEventName('onUserChanged'), listener); + } + /** * Sign the current user out * @return {Promise} diff --git a/lib/modules/auth/user.js b/lib/modules/auth/user.js index 2e559702..2eea4850 100644 --- a/lib/modules/auth/user.js +++ b/lib/modules/auth/user.js @@ -7,32 +7,17 @@ export default class User { /** * * @param authClass Instance of Authentication class - * @param authObj authentication result object from native + * @param user user result object from native */ - constructor(authClass, authObj) { + constructor(authClass, userObj) { this._auth = authClass; - this._user = null; - this._updateValues(authObj); + this._user = userObj; } /** * INTERNALS */ - /** - * - * @param authObj - * @private - */ - _updateValues(authObj) { - this._authObj = authObj; - if (authObj.user) { - this._user = authObj.user; - } else { - this._user = null; - } - } - /** * Returns a user property or null if does not exist * @param prop @@ -40,7 +25,6 @@ export default class User { * @private */ _valueOrNull(prop) { - if (!this._user) return null; if (!Object.hasOwnProperty.call(this._user, prop)) return null; return this._user[prop]; } @@ -52,7 +36,6 @@ export default class User { * @private */ _valueOrFalse(prop) { - if (!this._user) return false; if (!Object.hasOwnProperty.call(this._user, prop)) return false; return this._user[prop]; } diff --git a/lib/modules/database/query.js b/lib/modules/database/query.js index 4b29b1c0..ce1e41b0 100644 --- a/lib/modules/database/query.js +++ b/lib/modules/database/query.js @@ -26,6 +26,7 @@ export default class Query { */ orderBy(name: string, key?: string) { this.modifiers.push({ + id: `orderBy-${name}:${key}`, type: 'orderBy', name, key, @@ -42,6 +43,7 @@ export default class Query { */ limit(name: string, limit: number) { this.modifiers.push({ + id: `limit-${name}:${limit}`, type: 'limit', name, limit, @@ -59,6 +61,7 @@ export default class Query { */ filter(name: string, value: any, key?: string) { this.modifiers.push({ + id: `filter-${name}:${objectToUniqueId(value)}:${key}`, type: 'filter', name, value, @@ -82,14 +85,21 @@ export default class Query { * @return {*} */ queryIdentifier() { - // convert query modifiers array into an object for generating a unique key - const object = {}; + // sort modifiers to enforce ordering + const sortedModifiers = this.getModifiers().sort((a, b) => { + if (a.id < b.id) return -1; + if (a.id > b.id) return 1; + return 0; + }); - for (let i = 0, len = this.modifiers.length; i < len; i++) { - const { name, type, value } = this.modifiers[i]; - object[`${type}-${name}`] = value; + // Convert modifiers to unique key + let key = '{'; + for (let i = 0; i < sortedModifiers.length; i++) { + if (i !== 0) key += ','; + key += sortedModifiers[i].id; } + key += '}'; - return objectToUniqueId(object); + return key; } } diff --git a/lib/modules/database/reference.js b/lib/modules/database/reference.js index ab3a1da9..ffb1af9f 100644 --- a/lib/modules/database/reference.js +++ b/lib/modules/database/reference.js @@ -736,14 +736,15 @@ export default class Reference extends ReferenceBase { // remove the callback. // Remove only a single registration if (eventType && originalCallback) { - const registrations = this._syncTree.getRegistrationsByPathEvent(this.path, eventType); + const registration = this._syncTree.getOneByPathEventListener(this.path, eventType, originalCallback); + if (!registration) return []; // remove the paired cancellation registration if any exist - this._syncTree.removeListenersForRegistrations([`${registrations[0]}$cancelled`]); + this._syncTree.removeListenersForRegistrations([`${registration}$cancelled`]); // remove only the first registration to match firebase web sdk // call multiple times to remove multiple registrations - return this._syncTree.removeListenerRegistrations(originalCallback, [registrations[0]]); + return this._syncTree.removeListenerRegistrations(originalCallback, [registration]); } // Firebase Docs: diff --git a/lib/modules/database/transaction.js b/lib/modules/database/transaction.js index f632714a..4c1f4864 100644 --- a/lib/modules/database/transaction.js +++ b/lib/modules/database/transaction.js @@ -1,12 +1,12 @@ /** * @flow - * Database representation wrapper + * Database Transaction representation wrapper */ let transactionId = 0; /** - * @class Database + * @class TransactionHandler */ export default class TransactionHandler { constructor(database: Object) { diff --git a/lib/modules/firestore/utils/serialize.js b/lib/modules/firestore/utils/serialize.js index 3ef0b09a..5bdebf26 100644 --- a/lib/modules/firestore/utils/serialize.js +++ b/lib/modules/firestore/utils/serialize.js @@ -67,7 +67,7 @@ const buildTypeMap = (value: any): any => { }; } else if (value instanceof Date) { typeMap.type = 'date'; - typeMap.value = value.toISOString(); + typeMap.value = value.getTime(); } else { typeMap.type = 'object'; typeMap.value = buildNativeMap(value); diff --git a/lib/utils/SyncTree.js b/lib/utils/SyncTree.js index 072bbc1a..12cc2078 100644 --- a/lib/utils/SyncTree.js +++ b/lib/utils/SyncTree.js @@ -11,6 +11,7 @@ type Registration = { once?: Boolean, appName: String, eventType: String, + listener: Function, eventRegistrationKey: String, ref: DatabaseReference, } @@ -197,6 +198,28 @@ export default class SyncTree { return Object.keys(this._tree[path][eventType]); } + /** + * Returns a single registration key for the specified path, eventType, and listener + * + * @param path + * @param eventType + * @param listener + * @return {Array} + */ + getOneByPathEventListener(path: string, eventType: string, listener: Function): Array { + if (!this._tree[path]) return []; + if (!this._tree[path][eventType]) return []; + + const registrationsForPathEvent = Object.entries(this._tree[path][eventType]); + + for (let i = 0; i < registrationsForPathEvent.length; i++) { + const registration = registrationsForPathEvent[i]; + if (registration[1] === listener) return registration[0]; + } + + return null; + } + /** * Register a new listener. @@ -211,8 +234,8 @@ export default class SyncTree { if (!this._tree[path]) this._tree[path] = {}; if (!this._tree[path][eventType]) this._tree[path][eventType] = {}; - this._tree[path][eventType][eventRegistrationKey] = 0; - this._reverseLookup[eventRegistrationKey] = Object.assign({}, parameters); + this._tree[path][eventType][eventRegistrationKey] = listener; + this._reverseLookup[eventRegistrationKey] = Object.assign({ listener }, parameters); if (once) { INTERNALS.SharedEventEmitter.once( diff --git a/package-lock.json b/package-lock.json index d699d662..685a8fcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-native-firebase", - "version": "3.0.4", + "version": "3.0.6", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index 198a5f69..ff9f03fd 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -150,7 +150,7 @@ PODS: - React/Core - React/fishhook - React/RCTBlob - - RNFirebase (3.0.0): + - RNFirebase (3.0.5): - React - yoga (0.49.1.React) @@ -208,7 +208,7 @@ SPEC CHECKSUMS: nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 Protobuf: 03eef2ee0b674770735cf79d9c4d3659cf6908e8 React: cf892fb84b7d06bf5fea7f328e554c6dcabe85ee - RNFirebase: 901a473c68fcbaa28125c56a911923f2fbe5d61b + RNFirebase: 7c86b4efd2860700048d927f34db237fbce1d5fc yoga: 3abf02d6d9aeeb139b4c930eb1367feae690a35a PODFILE CHECKSUM: b5674be55653f5dda937c8b794d0479900643d45 diff --git a/tests/src/tests/database/ref/issueSpecificTests.js b/tests/src/tests/database/ref/issueSpecificTests.js index 9abbb868..58cd5ce8 100644 --- a/tests/src/tests/database/ref/issueSpecificTests.js +++ b/tests/src/tests/database/ref/issueSpecificTests.js @@ -1,4 +1,6 @@ import should from 'should'; +import sinon from 'sinon'; +import 'should-sinon'; import DatabaseContents from '../../support/DatabaseContents'; function issueTests({ describe, it, context, firebase }) { @@ -81,6 +83,212 @@ function issueTests({ describe, it, context, firebase }) { }); }); }); + + describe('issue_489', () => { + context('long numbers should', () => { + it('return as longs', async () => { + // Setup + + const long1Ref = firebase.native.database().ref('tests/issues/489/long1'); + const long2Ref = firebase.native.database().ref('tests/issues/489/long2'); + const long2 = 1234567890123456; + + // Test + + let snapshot = await long1Ref.once('value'); + snapshot.val().should.eql(DatabaseContents.ISSUES[489].long1); + + + await long2Ref.set(long2); + snapshot = await long2Ref.once('value'); + snapshot.val().should.eql(long2); + + return Promise.resolve(); + }); + }); + }); + + describe('issue_521', () => { + context('orderByChild (numerical field) and limitToLast', () => { + it('once() returns correct results', async () => { + // Setup + + const ref = firebase.native.database().ref('tests/issues/521'); + // Test + + return ref + .orderByChild('number') + .limitToLast(1) + .once('value') + .then((snapshot) => { + const val = snapshot.val(); + // Assertion + val.key3.should.eql(DatabaseContents.ISSUES[521].key3); + should.equal(Object.keys(val).length, 1); + + return Promise.resolve(); + }); + }); + + it('on() returns correct initial results', async () => { + // Setup + + const ref = firebase.native.database().ref('tests/issues/521').orderByChild('number').limitToLast(2); + const callback = sinon.spy(); + + // Test + + await new Promise((resolve) => { + ref.on('value', (snapshot) => { + callback(snapshot.val()); + resolve(); + }); + }); + + callback.should.be.calledWith({ + key2: DatabaseContents.ISSUES[521].key2, + key3: DatabaseContents.ISSUES[521].key3, + }); + callback.should.be.calledOnce(); + + return Promise.resolve(); + }); + + it('on() returns correct subsequent results', async () => { + // Setup + + const ref = firebase.native.database().ref('tests/issues/521').orderByChild('number').limitToLast(2); + const callback = sinon.spy(); + + // Test + + await new Promise((resolve) => { + ref.on('value', (snapshot) => { + callback(snapshot.val()); + resolve(); + }); + }); + + callback.should.be.calledWith({ + key2: DatabaseContents.ISSUES[521].key2, + key3: DatabaseContents.ISSUES[521].key3, + }); + callback.should.be.calledOnce(); + + const newDataValue = { + name: 'Item 4', + number: 4, + string: 'item4', + }; + const newRef = firebase.native.database().ref('tests/issues/521/key4'); + await newRef.set(newDataValue); + + await new Promise((resolve) => { + setTimeout(() => resolve(), 5); + }); + + // Assertions + + callback.should.be.calledWith({ + key3: DatabaseContents.ISSUES[521].key3, + key4: newDataValue, + }); + callback.should.be.calledTwice(); + + return Promise.resolve(); + }); + }); + + context('orderByChild (string field) and limitToLast', () => { + it('once() returns correct results', async () => { + // Setup + + const ref = firebase.native.database().ref('tests/issues/521'); + // Test + + return ref + .orderByChild('string') + .limitToLast(1) + .once('value') + .then((snapshot) => { + const val = snapshot.val(); + // Assertion + val.key3.should.eql(DatabaseContents.ISSUES[521].key3); + should.equal(Object.keys(val).length, 1); + + return Promise.resolve(); + }); + }); + + it('on() returns correct initial results', async () => { + // Setup + + const ref = firebase.native.database().ref('tests/issues/521').orderByChild('string').limitToLast(2); + const callback = sinon.spy(); + + // Test + + await new Promise((resolve) => { + ref.on('value', (snapshot) => { + callback(snapshot.val()); + resolve(); + }); + }); + + callback.should.be.calledWith({ + key2: DatabaseContents.ISSUES[521].key2, + key3: DatabaseContents.ISSUES[521].key3, + }); + callback.should.be.calledOnce(); + + return Promise.resolve(); + }); + + it('on() returns correct subsequent results', async () => { + // Setup + + const ref = firebase.native.database().ref('tests/issues/521').orderByChild('string').limitToLast(2); + const callback = sinon.spy(); + + // Test + + await new Promise((resolve) => { + ref.on('value', (snapshot) => { + callback(snapshot.val()); + resolve(); + }); + }); + + callback.should.be.calledWith({ + key2: DatabaseContents.ISSUES[521].key2, + key3: DatabaseContents.ISSUES[521].key3, + }); + callback.should.be.calledOnce(); + + const newDataValue = { + name: 'Item 4', + number: 4, + string: 'item4', + }; + const newRef = firebase.native.database().ref('tests/issues/521/key4'); + await newRef.set(newDataValue); + + await new Promise((resolve) => { + setTimeout(() => resolve(), 5); + }); + + // Assertions + + callback.should.be.calledWith({ + key3: DatabaseContents.ISSUES[521].key3, + key4: newDataValue, + }); + callback.should.be.calledTwice(); + + return Promise.resolve(); + }); + }); + }); } export default issueTests; diff --git a/tests/src/tests/database/ref/offTests.js b/tests/src/tests/database/ref/offTests.js index df6ebe08..a1627aea 100644 --- a/tests/src/tests/database/ref/offTests.js +++ b/tests/src/tests/database/ref/offTests.js @@ -297,6 +297,167 @@ function offTests({ describe, it, xcontext, context, firebase }) { }); }); + context('when 2 different child_added callbacks on the same path', () => { + context('that has been added and removed in the same order', () => { + it('must be completely removed', async () => { + // Setup + + const spyA = sinon.spy(); + let callbackA; + + const spyB = sinon.spy(); + let callbackB; + + const ref = firebase.native.database().ref('tests/types/array'); + const arrayLength = DatabaseContents.DEFAULT.array.length; + // Attach callbackA + await new Promise((resolve) => { + callbackA = () => { + spyA(); + resolve(); + }; + ref.on('child_added', callbackA); + }); + + // Attach callbackB + await new Promise((resolve) => { + callbackB = () => { + spyB(); + resolve(); + }; + ref.on('child_added', callbackB); + }); + + // Add a delay to ensure that the .on() has had time to be registered + await new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 15); + }); + + spyA.should.have.callCount(arrayLength); + spyB.should.have.callCount(arrayLength); + + // Undo the first callback + const resp = await ref.off('child_added', callbackA); + should(resp, undefined); + + // Trigger the event the callback is listening to + await ref.push(DatabaseContents.DEFAULT.number); + + // Add a delay to ensure that the .set() has had time to be registered + await new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 15); + }); + + // CallbackA should have been called zero more times its attachment + // has been removed, and callBackB only one more time becuase it's still attached + spyA.should.have.callCount(arrayLength); + spyB.should.have.callCount(arrayLength + 1); + + // Undo the second attachment + const resp2 = await ref.off('child_added', callbackB); + should(resp2, undefined); + + // Trigger the event the callback is listening to + await ref.push(DatabaseContents.DEFAULT.number); + + // Add a delay to ensure that the .set() has had time to be registered + await new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 15); + }); + + // Both Callbacks should not have been called any more times + spyA.should.have.callCount(arrayLength); + spyB.should.have.callCount(arrayLength + 1); + }); + }); + + // ******This test is failed******* + context('that has been added and removed in reverse order', () => { + it('must be completely removed', async () => { + // Setup + + const spyA = sinon.spy(); + let callbackA; + + const spyB = sinon.spy(); + let callbackB; + + const ref = firebase.native.database().ref('tests/types/array'); + const arrayLength = DatabaseContents.DEFAULT.array.length; + // Attach callbackA + await new Promise((resolve) => { + callbackA = () => { + spyA(); + resolve(); + }; + ref.on('child_added', callbackA); + }); + + // Attach callbackB + await new Promise((resolve) => { + callbackB = () => { + spyB(); + resolve(); + }; + ref.on('child_added', callbackB); + }); + + // Add a delay to ensure that the .on() has had time to be registered + await new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 15); + }); + + spyA.should.have.callCount(arrayLength); + spyB.should.have.callCount(arrayLength); + + // Undo the second callback + const resp = await ref.off('child_added', callbackB); + should(resp, undefined); + + // Trigger the event the callback is listening to + await ref.push(DatabaseContents.DEFAULT.number); + + // Add a delay to ensure that the .set() has had time to be registered + await new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 15); + }); + + // CallbackB should have been called zero more times its attachment + // has been removed, and callBackA only one more time becuase it's still attached + spyA.should.have.callCount(arrayLength + 1); + spyB.should.have.callCount(arrayLength); + + // Undo the second attachment + const resp2 = await ref.off('child_added', callbackA); + should(resp2, undefined); + + // Trigger the event the callback is listening to + await ref.push(DatabaseContents.DEFAULT.number); + + // Add a delay to ensure that the .set() has had time to be registered + await new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 15); + }); + + // Both Callbacks should not have been called any more times + spyA.should.have.callCount(arrayLength + 1); + spyB.should.have.callCount(arrayLength); + }); + }); + }); + xcontext('when a context', () => { /** * @todo Add tests for when a context is passed. Not sure what the intended diff --git a/tests/src/tests/firestore/documentReferenceTests.js b/tests/src/tests/firestore/documentReferenceTests.js index 968cb28b..a6aa2f65 100644 --- a/tests/src/tests/firestore/documentReferenceTests.js +++ b/tests/src/tests/firestore/documentReferenceTests.js @@ -435,6 +435,7 @@ function documentReferenceTests({ describe, it, context, firebase }) { const doc = await docRef.get(); doc.data().field.should.be.instanceof(Date); should.equal(doc.data().field.toISOString(), date.toISOString()); + should.equal(doc.data().field.getTime(), date.getTime()); }); }); diff --git a/tests/src/tests/support/DatabaseContents.js b/tests/src/tests/support/DatabaseContents.js index a4a913c9..5d7180e2 100644 --- a/tests/src/tests/support/DatabaseContents.js +++ b/tests/src/tests/support/DatabaseContents.js @@ -91,5 +91,28 @@ export default { uid: 'aNYxLexOb2WsXGOPiEAu47q5bxH3', }, }, + + 489: { + long1: 1508777379000, + }, + + // https://github.com/invertase/react-native-firebase/issues/521 + 521: { + key1: { + name: 'Item 1', + number: 1, + string: 'item1', + }, + key3: { + name: 'Item 3', + number: 3, + string: 'item3', + }, + key2: { + name: 'Item 2', + number: 2, + string: 'item2', + }, + }, }, };