diff --git a/android/.idea/gradle.xml b/android/.idea/gradle.xml new file mode 100644 index 0000000..76a4349 --- /dev/null +++ b/android/.idea/gradle.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 760a0a0..4b6e0e5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -44,7 +44,7 @@ allprojects { dependencies { compile 'com.facebook.react:react-native:0.20.+' - compile 'com.google.android.gms:play-services-base:9.8.0' + compile 'com.google.android.gms:play-services-base:+' compile 'com.google.firebase:firebase-core:9.8.0' compile 'com.google.firebase:firebase-config:9.8.0' compile 'com.google.firebase:firebase-auth:9.8.0' diff --git a/android/src/main/java/io/fullstack/firestack/FirestackStorage.java b/android/src/main/java/io/fullstack/firestack/FirestackStorage.java index b41301d..21ed217 100644 --- a/android/src/main/java/io/fullstack/firestack/FirestackStorage.java +++ b/android/src/main/java/io/fullstack/firestack/FirestackStorage.java @@ -5,6 +5,9 @@ import android.content.Context; import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.Map; import java.util.HashMap; @@ -26,6 +29,8 @@ import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.OnSuccessListener; +import com.google.firebase.storage.StorageException; +import com.google.firebase.storage.StreamDownloadTask; import com.google.firebase.storage.UploadTask; import com.google.firebase.storage.FirebaseStorage; import com.google.firebase.storage.StorageMetadata; @@ -49,6 +54,18 @@ class FirestackStorageModule extends ReactContextBaseJavaModule { private static final String FileTypeRegular = "FILETYPE_REGULAR"; private static final String FileTypeDirectory = "FILETYPE_DIRECTORY"; + private static final String STORAGE_UPLOAD_PROGRESS = "upload_progress"; + private static final String STORAGE_UPLOAD_PAUSED = "upload_paused"; + private static final String STORAGE_UPLOAD_RESUMED = "upload_resumed"; + + private static final String STORAGE_DOWNLOAD_PROGRESS = "download_progress"; + private static final String STORAGE_DOWNLOAD_PAUSED = "download_paused"; + private static final String STORAGE_DOWNLOAD_RESUMED = "download_resumed"; + private static final String STORAGE_DOWNLOAD_SUCCESS = "download_success"; + private static final String STORAGE_DOWNLOAD_FAILURE = "download_failure"; + + private ReactContext mReactContext; + public FirestackStorageModule(ReactApplicationContext reactContext) { super(reactContext); @@ -60,6 +77,118 @@ public String getName() { return TAG; } + + public boolean isExternalStorageWritable() { + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state)) { + return true; + } + return false; + } + + @ReactMethod + public void downloadFile(final String urlStr, + final String fbPath, + final String localFile, + final Callback callback) { + Log.d(TAG, "downloadFile: "+urlStr+", "+localFile); + if (!isExternalStorageWritable()) { + Log.w(TAG, "downloadFile failed: external storage not writable"); + WritableMap error = Arguments.createMap(); + final int errorCode = 1; + error.putDouble("code", errorCode); + error.putString("description", "downloadFile failed: external storage not writable"); + callback.invoke(error); + return; + } + FirebaseStorage storage = FirebaseStorage.getInstance(); + String storageBucket = storage.getApp().getOptions().getStorageBucket(); + String storageUrl = "gs://" + storageBucket; + Log.d(TAG, "Storage url " + storageUrl + fbPath); + + StorageReference storageRef = storage.getReferenceFromUrl(storageUrl); + StorageReference fileRef = storageRef.child(fbPath); + + fileRef.getStream(new StreamDownloadTask.StreamProcessor() { + @Override + public void doInBackground(StreamDownloadTask.TaskSnapshot taskSnapshot, InputStream inputStream) throws IOException { + int indexOfLastSlash = localFile.lastIndexOf("/"); + String pathMinusFileName = localFile.substring(0, indexOfLastSlash) + "/"; + String filename = localFile.substring(indexOfLastSlash+1); + File fileWithJustPath = new File(pathMinusFileName); + if (!fileWithJustPath.mkdirs()) { + Log.e(TAG, "Directory not created"); + WritableMap error = Arguments.createMap(); + error.putString("message", "Directory not created"); + callback.invoke(error); + return; + } + File fileWithFullPath = new File(pathMinusFileName, filename); + FileOutputStream output = new FileOutputStream(fileWithFullPath); + int bufferSize = 1024; + byte[] buffer = new byte[bufferSize]; + int len = 0; + while ((len = inputStream.read(buffer)) != -1) { + output.write(buffer, 0, len); + } + output.close(); + } + }).addOnProgressListener(new OnProgressListener() { + @Override + public void onProgress(StreamDownloadTask.TaskSnapshot taskSnapshot) { + WritableMap data = Arguments.createMap(); + data.putString("ref", taskSnapshot.getStorage().getBucket()); + double percentComplete = taskSnapshot.getTotalByteCount() == 0 ? 0.0f : 100.0f * (taskSnapshot.getBytesTransferred()) / (taskSnapshot.getTotalByteCount()); + data.putDouble("progress", percentComplete); + FirestackUtils.sendEvent(mReactContext, STORAGE_DOWNLOAD_PROGRESS, data); + } + }).addOnPausedListener(new OnPausedListener() { + @Override + public void onPaused(StreamDownloadTask.TaskSnapshot taskSnapshot) { + WritableMap data = Arguments.createMap(); + data.putString("ref", taskSnapshot.getStorage().getBucket()); + FirestackUtils.sendEvent(mReactContext, STORAGE_DOWNLOAD_PAUSED, data); + } + }).addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(StreamDownloadTask.TaskSnapshot taskSnapshot) { + final WritableMap data = Arguments.createMap(); + StorageReference ref = taskSnapshot.getStorage(); + data.putString("fullPath", ref.getPath()); + data.putString("bucket", ref.getBucket()); + data.putString("name", ref.getName()); + ref.getMetadata().addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(final StorageMetadata storageMetadata) { + data.putMap("metadata", getMetadataAsMap(storageMetadata)); + callback.invoke(null, data); + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception exception) { + final int errorCode = 1; + WritableMap data = Arguments.createMap(); + StorageException storageException = StorageException.fromException(exception); + data.putString("description", storageException.getMessage()); + data.putInt("code", errorCode); + callback.invoke(makeErrorPayload(errorCode, exception)); + } + }); + } + }).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception exception) { + final int errorCode = 1; + WritableMap data = Arguments.createMap(); + StorageException storageException = StorageException.fromException(exception); + data.putString("description", storageException.getMessage()); + data.putInt("code", errorCode); + callback.invoke(makeErrorPayload(errorCode, exception)); + } + }); + } + @ReactMethod public void downloadUrl(final String javascriptStorageBucket, final String path, @@ -90,16 +219,7 @@ public void onSuccess(Uri uri) { public void onSuccess(final StorageMetadata storageMetadata) { Log.d(TAG, "getMetadata success " + storageMetadata); - WritableMap metadata = Arguments.createMap(); - metadata.putString("getBucket", storageMetadata.getBucket()); - metadata.putString("getName", storageMetadata.getName()); - metadata.putDouble("sizeBytes", storageMetadata.getSizeBytes()); - metadata.putDouble("created_at", storageMetadata.getCreationTimeMillis()); - metadata.putDouble("updated_at", storageMetadata.getUpdatedTimeMillis()); - metadata.putString("md5hash", storageMetadata.getMd5Hash()); - metadata.putString("encoding", storageMetadata.getContentEncoding()); - - res.putMap("metadata", metadata); + res.putMap("metadata", getMetadataAsMap(storageMetadata)); res.putString("name", storageMetadata.getName()); res.putString("url", storageMetadata.getDownloadUrl().toString()); callback.invoke(null, res); @@ -109,7 +229,8 @@ public void onSuccess(final StorageMetadata storageMetadata) { @Override public void onFailure(@NonNull Exception exception) { Log.e(TAG, "Failure in download " + exception); - callback.invoke(makeErrorPayload(1, exception)); + final int errorCode = 1; + callback.invoke(makeErrorPayload(errorCode, exception)); } }); @@ -129,6 +250,18 @@ public void onFailure(@NonNull Exception exception) { }); } + private WritableMap getMetadataAsMap(StorageMetadata storageMetadata) { + WritableMap metadata = Arguments.createMap(); + metadata.putString("getBucket", storageMetadata.getBucket()); + metadata.putString("getName", storageMetadata.getName()); + metadata.putDouble("sizeBytes", storageMetadata.getSizeBytes()); + metadata.putDouble("created_at", storageMetadata.getCreationTimeMillis()); + metadata.putDouble("updated_at", storageMetadata.getUpdatedTimeMillis()); + metadata.putString("md5hash", storageMetadata.getMd5Hash()); + metadata.putString("encoding", storageMetadata.getContentEncoding()); + return metadata; + } + // STORAGE @ReactMethod public void uploadFile(final String urlStr, final String name, final String filepath, final ReadableMap metadata, final Callback callback) { @@ -191,9 +324,9 @@ public void onProgress(UploadTask.TaskSnapshot taskSnapshot) { if (progress >= 0) { WritableMap data = Arguments.createMap(); - data.putString("eventName", "upload_progress"); + data.putString("eventName", STORAGE_UPLOAD_PROGRESS); data.putDouble("progress", progress); - FirestackUtils.sendEvent(getReactApplicationContext(), "upload_progress", data); + FirestackUtils.sendEvent(getReactApplicationContext(), STORAGE_UPLOAD_PROGRESS, data); } } }) @@ -204,13 +337,14 @@ public void onPaused(UploadTask.TaskSnapshot taskSnapshot) { StorageMetadata d = taskSnapshot.getMetadata(); String bucket = d.getBucket(); WritableMap data = Arguments.createMap(); - data.putString("eventName", "upload_paused"); + data.putString("eventName", STORAGE_UPLOAD_PAUSED); data.putString("ref", bucket); - FirestackUtils.sendEvent(getReactApplicationContext(), "upload_paused", data); + FirestackUtils.sendEvent(getReactApplicationContext(), STORAGE_UPLOAD_PAUSED, data); } }); } catch (Exception ex) { - callback.invoke(makeErrorPayload(2, ex)); + final int errorCode = 2; + callback.invoke(makeErrorPayload(errorCode, ex)); } } @@ -221,7 +355,8 @@ public void getRealPathFromURI(final String uri, final Callback callback) { callback.invoke(null, path); } catch (Exception ex) { ex.printStackTrace(); - callback.invoke(makeErrorPayload(1, ex)); + final int errorCode = 1; + callback.invoke(makeErrorPayload(errorCode, ex)); } } diff --git a/docs/api/cloud-messaging.md b/docs/api/cloud-messaging.md index 8b13789..f53b217 100644 --- a/docs/api/cloud-messaging.md +++ b/docs/api/cloud-messaging.md @@ -1 +1,57 @@ +#cloud messaging +Make this prettier at some point but can't forget these things +setup certificates, enable push settings in app +Add things to app delegate +appdelegate.h -> +@import UserNotifications; +@interface AppDelegate : UIResponder + +appdelegate.m +Appdidfinishwithlaunching blah blah + UILocalNotification *localNotification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey]; + NSDictionary *userInfo = [launchOptions valueForKey:UIApplicationLaunchOptionsRemoteNotificationKey]; + + if (localNotification) { + [[NSNotificationCenter defaultCenter] postNotificationName:MESSAGING_MESSAGE_RECEIVED_LOCAL object:localNotification]; + NSLog(@"fresh launch from local notificaiton"); + } + + if(userInfo){ + [[NSNotificationCenter defaultCenter] postNotificationName:MESSAGING_MESSAGE_RECEIVED_REMOTE object:self userInfo:userInfo]; + NSLog(@"fresh launch from remote"); + } + +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { + // If you are receiving a notification message while your app is in the background, + // this callback will not be fired till the user taps on the notification launching the application. + // TODO: Handle data of notification + + // Print full message. + NSLog(@"%@", userInfo); + + [[NSNotificationCenter defaultCenter] postNotificationName:MESSAGING_MESSAGE_RECEIVED_REMOTE object:self userInfo:userInfo]; + +} + +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler{ + NSLog(@"%@", userInfo); + if ( application.applicationState == UIApplicationStateActive ){ + //user had the app in the foreground + } + else { + //app went from background to foreground + } + [[NSNotificationCenter defaultCenter] postNotificationName:MESSAGING_MESSAGE_RECEIVED_REMOTE object:self userInfo:userInfo]; + completionHandler(UIBackgroundFetchResultNoData); +} + +- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification +{ + NSLog(@"%@", notification); + [[NSNotificationCenter defaultCenter] postNotificationName:MESSAGING_MESSAGE_RECEIVED_LOCAL object:notification]; +} + +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{ + NSLog(@"Notification Registration Error %@", [error description]); +} diff --git a/ios/Firestack/Firestack.m b/ios/Firestack/Firestack.m index 284f38b..7073fe8 100644 --- a/ios/Firestack/Firestack.m +++ b/ios/Firestack/Firestack.m @@ -8,7 +8,7 @@ #import "FirestackErrors.h" #import "FirestackEvents.h" // #import "FirestackAnalytics.h" -// #import "FirestackCloudMessaging.h" +#import "FirestackCloudMessaging.h" static Firestack *_sharedInstance = nil; static dispatch_once_t onceToken; @@ -239,6 +239,8 @@ - (FIRApp *) firebaseApp // if (!self.configured) { + //If you want this method could replace all of the above for the option setting for firebase + //FIROptions *options = [[FIROptions alloc] initWithContentsOfFile:plistPath]; if ([FIRApp defaultApp] == NULL) { [FIRApp configureWithOptions:finalOptions]; } diff --git a/ios/Firestack/FirestackAuth.m b/ios/Firestack/FirestackAuth.m index a7ac803..939933b 100644 --- a/ios/Firestack/FirestackAuth.m +++ b/ios/Firestack/FirestackAuth.m @@ -40,7 +40,7 @@ @implementation FirestackAuth } @catch(NSException *ex) { NSDictionary *eventError = @{ @"eventName": AUTH_ANONYMOUS_ERROR_EVENT, - @"msg": ex.reason + @"errorMessage": ex.reason }; [self sendJSEvent:AUTH_ERROR_EVENT @@ -137,14 +137,15 @@ @implementation FirestackAuth sendJSEvent:AUTH_CHANGED_EVENT props: @{ @"eventName": @"userTokenError", - @"msg": [error localizedFailureReason] + @"authenticated": @((BOOL)true), + @"errorMessage": [error localizedFailureReason] }]; } else { [self sendJSEvent:AUTH_CHANGED_EVENT props: @{ @"eventName": @"user", - @"authenticated": @(true), + @"authenticated": @((BOOL)true), @"user": userProps }]; } @@ -157,7 +158,7 @@ @implementation FirestackAuth [self sendJSEvent:AUTH_CHANGED_EVENT props:@{ @"eventName": @"no_user", - @"authenticated": @(false), + @"authenticated": @((BOOL)false), @"error": err }]; } diff --git a/ios/Firestack/FirestackCloudMessaging.h b/ios/Firestack/FirestackCloudMessaging.h index 3e7c98b..52cbb7e 100644 --- a/ios/Firestack/FirestackCloudMessaging.h +++ b/ios/Firestack/FirestackCloudMessaging.h @@ -14,6 +14,7 @@ #import "RCTBridgeModule.h" #import "RCTUtils.h" + @interface FirestackCloudMessaging : RCTEventEmitter { } @@ -22,4 +23,4 @@ @end -#endif \ No newline at end of file +#endif diff --git a/ios/Firestack/FirestackCloudMessaging.m b/ios/Firestack/FirestackCloudMessaging.m index 9cfa6b3..3d9f97a 100644 --- a/ios/Firestack/FirestackCloudMessaging.m +++ b/ios/Firestack/FirestackCloudMessaging.m @@ -17,23 +17,22 @@ // https://github.com/facebook/react-native/blob/master/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @implementation RCTConvert (UILocalNotification) - + (UILocalNotification *)UILocalNotification:(id)json { - NSDictionary *details = [self NSDictionary:json]; - UILocalNotification *notification = [UILocalNotification new]; - notification.fireDate = [RCTConvert NSDate:details[@"fireDate"]] ?: [NSDate date]; - notification.alertBody = [RCTConvert NSString:details[@"alertBody"]]; - notification.alertAction = [RCTConvert NSString:details[@"alertAction"]]; - notification.soundName = [RCTConvert NSString:details[@"soundName"]] ?: UILocalNotificationDefaultSoundName; - notification.userInfo = [RCTConvert NSDictionary:details[@"userInfo"]]; - notification.category = [RCTConvert NSString:details[@"category"]]; - if (details[@"applicationIconBadgeNumber"]) { - notification.applicationIconBadgeNumber = [RCTConvert NSInteger:details[@"applicationIconBadgeNumber"]]; - } - return notification; + NSDictionary *details = [self NSDictionary:json]; + + UILocalNotification* notification = [UILocalNotification new]; + notification.fireDate = [RCTConvert NSDate:details[@"fireDate"]]; + notification.alertBody = [RCTConvert NSString:details[@"alertBody"]]; + notification.alertTitle = [RCTConvert NSString:details[@"alertTitle"]]; + notification.alertAction = [RCTConvert NSString:details[@"alertAction"]]; + notification.soundName = [RCTConvert NSString:details[@"soundName"]] ?: UILocalNotificationDefaultSoundName; + notification.userInfo = [RCTConvert NSDictionary:details[@"userInfo"]] ?: @{}; + notification.category = [RCTConvert NSString:details[@"category"]]; + + + return notification; } - @end @implementation FirestackCloudMessaging @@ -41,141 +40,163 @@ @implementation FirestackCloudMessaging // https://github.com/facebook/react-native/blob/master/Libraries/PushNotificationIOS/RCTPushNotificationManager.m static NSDictionary *RCTFormatLocalNotification(UILocalNotification *notification) { - NSMutableDictionary *formattedLocalNotification = [NSMutableDictionary dictionary]; - if (notification.fireDate) { - NSDateFormatter *formatter = [NSDateFormatter new]; - [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"]; - NSString *fireDateString = [formatter stringFromDate:notification.fireDate]; - formattedLocalNotification[@"fireDate"] = fireDateString; - } - formattedLocalNotification[@"alertAction"] = RCTNullIfNil(notification.alertAction); - formattedLocalNotification[@"alertBody"] = RCTNullIfNil(notification.alertBody); - formattedLocalNotification[@"applicationIconBadgeNumber"] = @(notification.applicationIconBadgeNumber); - formattedLocalNotification[@"category"] = RCTNullIfNil(notification.category); - formattedLocalNotification[@"soundName"] = RCTNullIfNil(notification.soundName); - formattedLocalNotification[@"userInfo"] = RCTNullIfNil(RCTJSONClean(notification.userInfo)); - formattedLocalNotification[@"remote"] = @NO; - return formattedLocalNotification; + NSMutableDictionary *formattedLocalNotification = [NSMutableDictionary dictionary]; + if (notification.fireDate) { + NSDateFormatter *formatter = [NSDateFormatter new]; + [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"]; + NSString *fireDateString = [formatter stringFromDate:notification.fireDate]; + formattedLocalNotification[@"fireDate"] = fireDateString; + } + formattedLocalNotification[@"alertAction"] = RCTNullIfNil(notification.alertAction); + formattedLocalNotification[@"alertBody"] = RCTNullIfNil(notification.alertBody); + formattedLocalNotification[@"applicationIconBadgeNumber"] = @(notification.applicationIconBadgeNumber); + formattedLocalNotification[@"category"] = RCTNullIfNil(notification.category); + formattedLocalNotification[@"soundName"] = RCTNullIfNil(notification.soundName); + formattedLocalNotification[@"userInfo"] = RCTNullIfNil(RCTJSONClean(notification.userInfo)); + formattedLocalNotification[@"remote"] = @NO; + return formattedLocalNotification; } + - (void) dealloc { - [[NSNotificationCenter defaultCenter] removeObserver: self]; + [[NSNotificationCenter defaultCenter] removeObserver: self]; } -+ (void) setup:(UIApplication *) application +@synthesize bridge = _bridge; + +-(void) setBridge:(RCTBridge *)bridge { - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(connectToFirebase) - name: UIApplicationDidEnterBackgroundNotification - object: nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(disconnectFromFirebase) - name: UIApplicationDidBecomeActiveNotification - object: nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleRemoteNotificationReceived:) - name:MESSAGING_MESSAGE_RECEIVED_REMOTE - object: nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleLocalNotificationReceived:) - name:MESSAGING_MESSAGE_RECEIVED_LOCAL - object: nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleTokenRefresh) - name:kFIRInstanceIDTokenRefreshNotification - object: nil]; - - if (SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(@"9.0")) { - UIUserNotificationType allNotificationTypes = - (UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge); - UIUserNotificationSettings *settings = - [UIUserNotificationSettings settingsForTypes:allNotificationTypes categories:nil]; - [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; -} else { - // iOS 10 or later - #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - UNAuthorizationOptions authOptions = - UNAuthorizationOptionAlert - | UNAuthorizationOptionSound - | UNAuthorizationOptionBadge; - [[UNUserNotificationCenter currentNotificationCenter] - requestAuthorizationWithOptions:authOptions - completionHandler:^(BOOL granted, NSError * _Nullable error) { - } - ]; - - // For iOS 10 display notification (sent via APNS) - [[UNUserNotificationCenter currentNotificationCenter] setDelegate:self]; - // For iOS 10 data message (sent via FCM) - [[FIRMessaging messaging] setRemoteMessageDelegate:self]; - #endif + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(connectToFirebase) + name: UIApplicationDidBecomeActiveNotification + object: nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(disconnectFromFirebase) + name: UIApplicationDidEnterBackgroundNotification + object: nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleLocalNotificationReceived:) + name:MESSAGING_MESSAGE_RECEIVED_REMOTE + object: nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleLocalNotificationReceived:) + name:MESSAGING_MESSAGE_RECEIVED_LOCAL + object: nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleTokenRefresh) + name:kFIRInstanceIDTokenRefreshNotification + object: nil]; + + [[NSNotificationCenter defaultCenter]addObserver:self + selector:@selector(sendDataMessageFailure:) + name:FIRMessagingSendErrorNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(sendDataMessageSuccess:) + name:FIRMessagingSendSuccessNotification + object:nil]; } -[[UIApplication sharedApplication] registerForRemoteNotifications]; + +#pragma mark Request permissions +RCT_EXPORT_METHOD(requestPermissions:(RCTResponseSenderBlock) callback) +{ + if (SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(@"9.0")) { + UIUserNotificationType allNotificationTypes = + (UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge); + UIUserNotificationSettings *settings = + [UIUserNotificationSettings settingsForTypes:allNotificationTypes categories:nil]; + [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; + } else { + // iOS 10 or later +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + UNAuthorizationOptions authOptions = + UNAuthorizationOptionAlert + | UNAuthorizationOptionSound + | UNAuthorizationOptionBadge; + [[UNUserNotificationCenter currentNotificationCenter] + requestAuthorizationWithOptions:authOptions + completionHandler:^(BOOL granted, NSError * _Nullable error) { + } + ]; + + // For iOS 10 display notification (sent via APNS) + [[UNUserNotificationCenter currentNotificationCenter] setDelegate:self]; + // For iOS 10 data message (sent via FCM) + //This is hacky + dispatch_async(dispatch_get_main_queue(), ^{ + [[FIRMessaging messaging] setRemoteMessageDelegate:self]; + }); +#endif + } + + [[UIApplication sharedApplication] registerForRemoteNotifications]; } #pragma mark callbacks - (void) connectToFirebase { - [[FIRMessaging messaging] connectWithCompletion:^(NSError *error) { - NSDictionary *evt; - NSString *evtName; - if (error != nil) { - NSLog(@"Error connecting: %@", [error debugDescription]); - evtName = MESSAGING_SUBSYSTEM_ERROR; - evt = @{ - @"eventName": MESSAGING_SUBSYSTEM_ERROR, - @"msg": [error debugDescription] - }; - } else { - NSLog(@"Connected to Firebase messaging"); - evtName = MESSAGING_SUBSYSTEM_EVENT; - evt = @{ - @"result": @"connected" - }; - [self - sendJSEvent:evtName - props: evt]; - - } - }]; + [[FIRMessaging messaging] connectWithCompletion:^(NSError *error) { + NSDictionary *evt; + NSString *evtName; + if (error != nil) { + NSLog(@"Error connecting: %@", [error debugDescription]); + evtName = MESSAGING_SUBSYSTEM_ERROR; + evt = @{ + @"eventName": MESSAGING_SUBSYSTEM_ERROR, + @"msg": [error debugDescription] + }; + } else { + NSLog(@"Connected to Firebase messaging"); + evtName = MESSAGING_SUBSYSTEM_EVENT; + evt = @{ + @"result": @"connected" + }; + [self + sendJSEvent:evtName + props: evt]; + + } + }]; } - (void) disconnectFromFirebase { - [[FIRMessaging messaging] disconnect]; - NSLog(@"Disconnect from Firebase"); - [self + [[FIRMessaging messaging] disconnect]; + NSLog(@"Disconnect from Firebase"); + [self sendJSEvent:MESSAGING_SUBSYSTEM_EVENT props: @{ - @"status": @"disconnected" - }]; + @"status": @"disconnected" + }]; } - (void) handleRemoteNotificationReceived:(NSNotification *) n { - NSMutableDictionary *props = [[NSMutableDictionary alloc] initWithDictionary: n.userInfo]; - [self sendJSEvent:MESSAGING_MESSAGE_RECEIVED_REMOTE props: props]; + NSMutableDictionary *props = [[NSMutableDictionary alloc] initWithDictionary: n.userInfo]; + [self sendJSEvent:MESSAGING_MESSAGE_RECEIVED_REMOTE props: n]; } - (void) handleLocalNotificationReceived:(NSNotification *) n { - NSMutableDictionary *props = [[NSMutableDictionary alloc] initWithDictionary: n.userInfo]; - [self sendJSEvent:MESSAGING_MESSAGE_RECEIVED_LOCAL props: props]; + NSMutableDictionary *props = [[NSMutableDictionary alloc] initWithDictionary: n.userInfo]; + [self sendJSEvent:MESSAGING_MESSAGE_RECEIVED_LOCAL props: props]; } + - (void) handleTokenRefresh { - NSDictionary *props = @{ - @"status": @"token_refreshed", - @"token": [[FIRInstanceID instanceID] token] - }; - [self sendJSEvent:MESSAGING_TOKEN_REFRESH props: props]; + NSDictionary *props = @{ + @"status": @"token_refreshed", + @"token": [[FIRInstanceID instanceID] token] + }; + [self sendJSEvent:MESSAGING_TOKEN_REFRESH props: props]; } RCT_EXPORT_MODULE(FirestackCloudMessaging); @@ -183,67 +204,120 @@ - (void) handleTokenRefresh RCT_EXPORT_METHOD(getToken:(RCTResponseSenderBlock)callback) { - NSString *token = [[FIRInstanceID instanceID] token]; - callback(@[[NSNull null], @{ - @"status": @"success", - @"token": token - }]); + NSString *token = [[FIRInstanceID instanceID] token]; + callback(@[[NSNull null], @{ + @"status": @"success", + @"token": token + }]); } +RCT_EXPORT_METHOD(getInitialNotification:(RCTResponseSenderBlock)callback){ + NSDictionary *localUserInfo = _bridge.launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]; + if(localUserInfo){ + callback(@[[NSNull null], @{ + @"status": @"success", + @"launchedFrom": localUserInfo + }]); + } +} +//WORK ON THESE SOME MORE THIS IS UGLY -RCT_EXPORT_METHOD(sendLocal:(UILocalNotification *)notification - callback:(RCTResponseSenderBlock) callback) +RCT_EXPORT_METHOD(sendLocal:(NSDictionary *) notification + callback:(RCTResponseSenderBlock) callback) { - NSLog(@"sendLocal called with notification: %@", notification); - [RCTSharedApplication() presentLocalNotificationNow:notification]; + UILocalNotification* localNotification = [RCTConvert UILocalNotification:notification]; + NSLog(@"sendLocal called with notification: %@", notification); + [RCTSharedApplication() presentLocalNotificationNow:localNotification]; + } -RCT_EXPORT_METHOD(scheduleLocal:(UILocalNotification *)notification - callback:(RCTResponseSenderBlock) callback) +RCT_EXPORT_METHOD(scheduleLocal:(NSDictionary *) notification + callback:(RCTResponseSenderBlock) callback) { - [RCTSharedApplication() scheduleLocalNotification:notification]; + UILocalNotification* localNotification = [RCTConvert UILocalNotification:notification]; + NSLog(@"scheduleLocal called with notification: %@", notification); + [RCTSharedApplication() scheduleLocalNotification:localNotification]; } RCT_EXPORT_METHOD(cancelAllLocalNotifications) { - [RCTSharedApplication() cancelAllLocalNotifications]; + [RCTSharedApplication() cancelAllLocalNotifications]; } RCT_EXPORT_METHOD(cancelLocalNotifications:(NSDictionary *)userInfo) { - for (UILocalNotification *notification in [UIApplication sharedApplication].scheduledLocalNotifications) { - __block BOOL matchesAll = YES; - NSDictionary *notificationInfo = notification.userInfo; - // Note: we do this with a loop instead of just `isEqualToDictionary:` - // because we only require that all specified userInfo values match the - // notificationInfo values - notificationInfo may contain additional values - // which we don't care about. - [userInfo enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { - if (![notificationInfo[key] isEqual:obj]) { - matchesAll = NO; - *stop = YES; - } - }]; - if (matchesAll) { - [[UIApplication sharedApplication] cancelLocalNotification:notification]; + for (UILocalNotification *notification in [UIApplication sharedApplication].scheduledLocalNotifications) { + __block BOOL matchesAll = YES; + NSDictionary *notificationInfo = notification.userInfo; + // Note: we do this with a loop instead of just `isEqualToDictionary:` + // because we only require that all specified userInfo values match the + // notificationInfo values - notificationInfo may contain additional values + // which we don't care about. + [userInfo enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { + if (![notificationInfo[key] isEqual:obj]) { + matchesAll = NO; + *stop = YES; + } + }]; + if (matchesAll) { + [[UIApplication sharedApplication] cancelLocalNotification:notification]; + } } - } } -RCT_EXPORT_METHOD(sendRemote:(UILocalNotification *)notification - callback:(RCTResponseSenderBlock) callback) +//RCT_EXPORT_METHOD(sendRemote:(UILocalNotification *)notification +// callback:(RCTResponseSenderBlock) callback) +//{ +// +//} + + +//for upstream it makes sense, we could probably use this as a general function for all local,upstream etc. Lets revisit +RCT_EXPORT_METHOD(send:(NSString *) messageId + msg: (NSDictionary *) msg + ttl: (int) ttl + callback:(RCTResponseSenderBlock)callback) { - + + int64_t ttl64 = (int64_t) ttl; + //This is hacky + dispatch_async(dispatch_get_main_queue(), ^{ + NSString* senderId = [[[FIRApp defaultApp] options] GCMSenderID]; + + senderId = [NSString stringWithFormat:@"%@@gcm.googleapis.com", senderId]; + [[FIRMessaging messaging] sendMessage:msg + to:senderId + withMessageID:messageId + timeToLive:ttl]; + }); } - -RCT_EXPORT_METHOD(send:(NSString *) senderId - messageId:(NSString *) messageId - messageType:(NSString *) messageType - msg: (NSString *) msg - callback:(RCTResponseSenderBlock)callback) +RCT_EXPORT_METHOD(sendMessage:(NSDictionary *) details + type: (NSString *) type + callback:(RCTResponseSenderBlock)callback) { + +// int64_t ttl64 = (int64_t) ttl; +// //This is hacky +// dispatch_async(dispatch_get_main_queue(), ^{ +// NSString* senderId = [[[FIRApp defaultApp] options] GCMSenderID]; +// +// senderId = [NSString stringWithFormat:@"%@@gcm.googleapis.com", senderId]; +// [[FIRMessaging messaging] sendMessage:msg +// to:senderId +// withMessageID:messageId +// timeToLive:ttl]; +// }); +} +//send upstream always returns sendDataMessageFailure - pretty sure have to setup the server side component of this to actually see it work +- (void)sendDataMessageFailure:(NSNotification *)notification { + NSString *messageID = (NSString *)notification.userInfo[@"messageId"]; + NSDictionary *userInfo = notification.userInfo; // contains error info etc + +} +- (void)sendDataMessageSuccess:(NSNotification *)notification { + NSString *messageID = (NSString *)notification.userInfo[@"messageId"]; } RCT_EXPORT_METHOD(listenForTokenRefresh:(RCTResponseSenderBlock)callback) @@ -253,41 +327,41 @@ - (void) handleTokenRefresh {} RCT_EXPORT_METHOD(subscribeToTopic:(NSString *) topic - callback:(RCTResponseSenderBlock)callback) + callback:(RCTResponseSenderBlock)callback) { - [[FIRMessaging messaging] subscribeToTopic:topic]; - callback(@[[NSNull null], @{ - @"result": @"success", - @"topic": topic - }]); + [[FIRMessaging messaging] subscribeToTopic:topic]; + callback(@[[NSNull null], @{ + @"result": @"success", + @"topic": topic + }]); } RCT_EXPORT_METHOD(unsubscribeFromTopic:(NSString *) topic - callback: (RCTResponseSenderBlock)callback) + callback: (RCTResponseSenderBlock)callback) { - [[FIRMessaging messaging] unsubscribeFromTopic:topic]; - callback(@[[NSNull null], @{ - @"result": @"success", - @"topic": topic - }]); + [[FIRMessaging messaging] unsubscribeFromTopic:topic]; + callback(@[[NSNull null], @{ + @"result": @"success", + @"topic": topic + }]); } RCT_EXPORT_METHOD(setBadge:(NSInteger) number - callback:(RCTResponseSenderBlock) callback) + callback:(RCTResponseSenderBlock) callback) { - RCTSharedApplication().applicationIconBadgeNumber = number; - callback(@[[NSNull null], @{ - @"result": @"success", - @"number": @(number) - }]); + RCTSharedApplication().applicationIconBadgeNumber = number; + callback(@[[NSNull null], @{ + @"result": @"success", + @"number": @(number) + }]); } RCT_EXPORT_METHOD(getBadge:(RCTResponseSenderBlock) callback) { - callback(@[[NSNull null], @{ - @"result": @"success", - @"number": @(RCTSharedApplication().applicationIconBadgeNumber) - }]); + callback(@[[NSNull null], @{ + @"result": @"success", + @"number": @(RCTSharedApplication().applicationIconBadgeNumber) + }]); } RCT_EXPORT_METHOD(listenForReceiveNotification:(RCTResponseSenderBlock)callback) @@ -305,11 +379,11 @@ - (void) handleTokenRefresh // Not sure how to get away from this... yet - (NSArray *)supportedEvents { return @[ - MESSAGING_SUBSYSTEM_EVENT, - MESSAGING_SUBSYSTEM_ERROR, - MESSAGING_TOKEN_REFRESH, - MESSAGING_MESSAGE_RECEIVED_LOCAL, - MESSAGING_MESSAGE_RECEIVED_REMOTE]; + MESSAGING_SUBSYSTEM_EVENT, + MESSAGING_SUBSYSTEM_ERROR, + MESSAGING_TOKEN_REFRESH, + MESSAGING_MESSAGE_RECEIVED_LOCAL, + MESSAGING_MESSAGE_RECEIVED_REMOTE]; } - (void) sendJSEvent:(NSString *)title @@ -318,9 +392,9 @@ - (void) sendJSEvent:(NSString *)title @try { [self sendEventWithName:title body:@{ - @"eventName": title, - @"body": props - }]; + @"eventName": title, + @"body": props + }]; } @catch (NSException *err) { NSLog(@"An error occurred in sendJSEvent: %@", [err debugDescription]); diff --git a/ios/Firestack/FirestackStorage.m b/ios/Firestack/FirestackStorage.m index d88a19a..2da5377 100644 --- a/ios/Firestack/FirestackStorage.m +++ b/ios/Firestack/FirestackStorage.m @@ -25,7 +25,12 @@ - (dispatch_queue_t)methodQueue path:(NSString *) path callback:(RCTResponseSenderBlock) callback) { - FIRStorageReference *storageRef = [[FIRStorage storage] referenceForURL:storageUrl]; + FIRStorageReference *storageRef; + if (storageUrl == nil ) { + storageRef = [[FIRStorage storage] reference]; + } else { + storageRef = [[FIRStorage storage] referenceForURL:storageUrl]; + } FIRStorageReference *fileRef = [storageRef child:path]; [fileRef downloadURLWithCompletion:^(NSURL * _Nullable URL, NSError * _Nullable error) { if (error != nil) { @@ -52,14 +57,13 @@ - (dispatch_queue_t)methodQueue metadata:(NSDictionary *)metadata callback:(RCTResponseSenderBlock) callback) { + FIRStorageReference *storageRef; if (urlStr == nil) { - NSError *err = [[NSError alloc] init]; - [err setValue:@"Storage configuration error" forKey:@"name"]; - [err setValue:@"Call setStorageUrl() first" forKey:@"description"]; - return callback(@[err]); + storageRef = [[FIRStorage storage] reference]; + } else { + storageRef = [[FIRStorage storage] referenceForURL:urlStr]; } - FIRStorageReference *storageRef = [[FIRStorage storage] referenceForURL:urlStr]; FIRStorageReference *uploadRef = [storageRef child:name]; FIRStorageMetadata *firmetadata = [[FIRStorageMetadata alloc] initWithDictionary:metadata]; @@ -157,6 +161,7 @@ - (void) addUploadObservers:(FIRStorageUploadTask *) uploadTask case FIRStorageErrorCodeUnknown: // Unknown error occurred, inspect the server response [errProps setValue:@"Unknown error" forKey:@"description"]; + NSLog(@"Unknown error: %@", snapshot.error); break; } diff --git a/lib/modules/cloudmessaging.js b/lib/modules/cloudmessaging.js new file mode 100644 index 0000000..73b390b --- /dev/null +++ b/lib/modules/cloudmessaging.js @@ -0,0 +1,108 @@ +import {Platform, NativeModules, NativeEventEmitter} from 'react-native'; +const FirestackCloudMessaging = NativeModules.FirestackCloudMessaging; +const FirestackCloudMessagingEvt = new NativeEventEmitter(FirestackCloudMessaging); + +import promisify from '../utils/promisify' +import { Base, ReferenceBase } from './base' + +const defaultPermissions = { + 'badge': 1, + 'sound': 2, + 'alert': 3 +} +export class CloudMessaging extends Base { + constructor(firestack, options = {}) { + super(firestack, options); + + this.requestedPermissions = Object.assign({}, defaultPermissions, options.permissions); + } + get namespace() { + return 'firestack:cloudMessaging' + } + getToken() { + this.log.info('getToken for cloudMessaging'); + return promisify('getToken', FirestackCloudMessaging)(); + } + + // Request FCM permissions + requestPermissions(requestedPermissions = {}) { + if (Platform.OS === 'ios') { + const mergedRequestedPermissions = Object.assign({}, + this.requestedPermissions, + requestedPermissions); + return promisify('requestPermissions', FirestackCloudMessaging)(mergedRequestedPermissions) + .then(perms => { + + return perms; + }); + } + } + + sendMessage(details:Object = {}, type:string='local') { + const methodName = `send${type == 'local' ? 'Local' : 'Remote'}` + this.log.info('sendMessage', methodName, details); + return promisify(methodName, FirestackCloudMessaging)(details); + } + scheduleMessage(details:Object = {}, type:string='local') { + const methodName = `schedule${type == 'local' ? 'Local' : 'Remote'}` + return promisify(methodName, FirestackCloudMessaging)(details); + } + // OLD + send(senderId, messageId, messageType, msg){ + return promisify('send', FirestackCloudMessaging)(senderId, messageId, messageType, msg); + } + // + listenForTokenRefresh(callback) { + this.log.info('Setting up listenForTokenRefresh callback'); + const sub = this._on('FirestackRefreshToken', callback, FirestackCloudMessagingEvt); + return promisify(() => sub, FirestackCloudMessaging)(sub); + } + unlistenForTokenRefresh() { + this.log.info('Unlistening for TokenRefresh'); + this._off('FirestackRefreshToken'); + } + subscribeToTopic(topic) { + this.log.info('subscribeToTopic ' + topic); + const finalTopic = `/topics/${topic}` + return promisify('subscribeToTopic', FirestackCloudMessaging)(finalTopic); + } + unsubscribeFromTopic(topic) { + this.log.info('unsubscribeFromTopic ' + topic); + const finalTopic = `/topics/${topic}` + return promisify('unsubscribeFromTopic', FirestackCloudMessaging)(finalTopic); + } + // New api + onRemoteMessage(callback) { + this.log.info('On remote message callback'); + const sub = this._on('messaging_remote_event_received', callback, FirestackCloudMessagingEvt); + return promisify(() => sub, FirestackCloudMessaging)(sub); + } + + onLocalMessage(callback) { + this.log.info('on local callback'); + const sub = this._on('messaging_local_event_received', callback, FirestackCloudMessagingEvt); + return promisify(() => sub, FirestackCloudMessaging)(sub); + } + + // Original API + listenForReceiveNotification(callback) { + this.log.info('Setting up listenForReceiveNotification callback'); + const sub = this._on('FirestackReceiveNotification', callback, FirestackCloudMessagingEvt); + return promisify(() => sub, FirestackCloudMessaging)(sub); + } + unlistenForReceiveNotification() { + this.log.info('Unlistening for ReceiveNotification'); + this._off('FirestackRefreshToken'); + } + listenForReceiveUpstreamSend(callback) { + this.log.info('Setting up send callback'); + const sub = this._on('FirestackUpstreamSend', callback, FirestackCloudMessagingEvt); + return promisify(() => sub, FirestackCloudMessaging)(sub); + } + unlistenForReceiveUpstreamSend() { + this.log.info('Unlistening for send'); + this._off('FirestackUpstreamSend'); + } +} + +export default CloudMessaging \ No newline at end of file diff --git a/lib/modules/messaging.js b/lib/modules/messaging.js index 877cf29..696e5e4 100644 --- a/lib/modules/messaging.js +++ b/lib/modules/messaging.js @@ -5,14 +5,42 @@ import promisify from '../utils/promisify'; const FirestackCloudMessaging = NativeModules.FirestackCloudMessaging; const FirestackCloudMessagingEvt = new NativeEventEmitter(FirestackCloudMessaging); +const defaultPermissions = { + 'badge': 1, + 'sound': 2, + 'alert': 3 +} + /** * @class Messaging */ export default class Messaging extends Base { constructor(firestack, options = {}) { super(firestack, options); - } - + //this.requestedPermissions = Object.assign({}, defaultPermissions, options.permissions); + + } + + // Request FCM permissions + // requestPermissions(requestedPermissions = {}) { + // // if (Platform.OS === 'ios') { + // const mergedRequestedPermissions = Object.assign({}, + // this.requestedPermissions, + // requestedPermissions); + // return promisify('requestPermissions', FirestackCloudMessaging)(mergedRequestedPermissions) + // .then(perms => { + + // return perms; + // }); + // // } + // } + + // Request FCM permissions + requestPermissions() { + this.log.info('requesting permissions'); + return promisify('requestPermissions', FirestackCloudMessaging)(); + // } + } /* * WEB API */ @@ -48,8 +76,14 @@ export default class Messaging extends Base { return promisify('getToken', FirestackCloudMessaging)(); } + getInitialNotification(){ + this.log.info('user launched app from notification'); + return promisify('getInitialNotification',FirestackCloudMessaging)(); + } + sendMessage(details: Object = {}, type: string = 'local') { const methodName = `send${type == 'local' ? 'Local' : 'Remote'}`; + this.log.info(methodName); this.log.info('sendMessage', methodName, details); return promisify(methodName, FirestackCloudMessaging)(details); } @@ -60,8 +94,8 @@ export default class Messaging extends Base { } // OLD - send(senderId, messageId, messageType, msg) { - return promisify('send', FirestackCloudMessaging)(senderId, messageId, messageType, msg); + send(messageId, msg,ttl) { + return promisify('send', FirestackCloudMessaging)(messageId, msg,ttl); } // @@ -111,7 +145,7 @@ export default class Messaging extends Base { console.warn('Firestack: listenForReceiveNotification is now deprecated, please use onMessage'); return this.onMessage(...args); } - + /** * @deprecated * @param args diff --git a/lib/utils/window-or-global.js b/lib/utils/window-or-global.js index 3228c06..7b64020 100644 --- a/lib/utils/window-or-global.js +++ b/lib/utils/window-or-global.js @@ -2,4 +2,4 @@ // https://github.com/purposeindustries/window-or-global module.exports = (typeof self === 'object' && self.self === self && self) || (typeof global === 'object' && global.global === global && global) || - this \ No newline at end of file + {} \ No newline at end of file