Closed joshuapinter closed 7 years ago
I'm stuck with this at the moment but using remote notifications instead of local ones. I can't seem to capture an initial remote notification that starts up my app from a "cold start". Like everyone else in #2658, I can get my notification from my listener via PushNotificationIOS.addEventListener('notification', ...
working perfectly once the app is running, but trying to get a notification from a cold start doesn't seem to be possible.
@dwilt My solution works perfect for local notifications, I imagine it would work just as well with remote notifications. Have you tried the AppDelegate.m
modification I suggested above?
I did - but I can't seem to get it to work. I'm trying to get the prop value initialNotificationUserInfo
off my class and it doesn't seem to have a value. Of course, debugging from a cold start - not starting through Xcode where I have a console to put output - is making it much 10x harder.
@dwilt If you put a breakpoint right before rootView.appProperties = appProperties;
, what is the value of appProperties
? This will test to see if it's your Obj-c or your JS that is to blame here.
@joshuapinter how can I put a breakpoint there if the app is going to be started up cold by a push notification and not by using Xcode? Know what I mean?
Right. In Xcode you can set the Debugger to launch when the app launches (like from a notification). Just edit your Scheme and look for this:
And maybe instead of a breakpoint just NSLog
out appProperties
.
@joshuapinter
To reiterate what I've done: I closed the app completely, and edited the scheme of the project to "Wait for executable to be launched" (this is awesome by the way for testing these remote notifications). I logged into my Urban Airship account to dispatch a notification to my device. Boom, saw the notification come in, tapped on it, and it fired up the app, and paused at this breakpoint:
Here's my AppDelegate.m
:
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AppDelegate.h"
#import "RCTBundleURLProvider.h"
#import "RCTRootView.h"
#import <AirshipKit/AirshipKit.h>
#import "RCTPushNotificationManager.h"
@implementation AppDelegate
// Required to register for notifications
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
{
[RCTPushNotificationManager didRegisterUserNotificationSettings:notificationSettings];
}
// Required for the register event.
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
[RCTPushNotificationManager didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
// Required for the notification event.
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification
{
[RCTPushNotificationManager didReceiveRemoteNotification:notification];
}
// Required for the localNotification event.
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
{
[RCTPushNotificationManager didReceiveLocalNotification:notification];
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
NSLog(@"%@", error);
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSURL *jsCodeLocation;
// Set log level for debugging config loading (optional)
// It will be set to the value in the loaded config upon takeOff
[UAirship setLogLevel:UALogLevelTrace];
// Populate AirshipConfig.plist with your app's info from https://go.urbanairship.com
// or set runtime properties here.
UAConfig *config = [UAConfig defaultConfig];
// Call takeOff (which creates the UAirship singleton)
[UAirship takeOff:config];
// Print out the application configuration for debugging (optional)
UA_LDEBUG(@"Config:\n%@", [config description]);
// Set the icon badge to zero on startup (optional)
[[UAirship push] resetBadge];
// Set the notification types required for the app (optional). This value defaults
// to badge, alert and sound, so it's only necessary to set it if you want
// to add or remove types.
[UAirship push].userNotificationTypes = (UIUserNotificationTypeAlert |
UIUserNotificationTypeBadge |
UIUserNotificationTypeSound);
// User notifications will not be enabled until userPushNotificationsEnabled is
// set YES on UAPush. Onced enabled, the setting will be persisted and the user
// will be prompted to allow notifications. You should wait for a more appropriate
// time to enable push to increase the likelihood that the user will accept
// notifications. For troubleshooting, we will enable this at launch.
[UAirship push].userPushNotificationsEnabled = YES;
// Useful to see available font families
// for (NSString* family in [UIFont familyNames])
// {
// NSLog(@"%@", family);
// for (NSString* name in [UIFont fontNamesForFamilyName: family])
// {
// NSLog(@" %@", name);
// }
// }
// Create a Mutable Dictionary to hold the appProperties to pass to React Native.
NSMutableDictionary *appProperties = [NSMutableDictionary dictionary];
if (launchOptions != nil) {
// Get Local Notification used to launch application.
UILocalNotification *notification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
if (notification) {
// Instead of passing the entire Notification, we'll pass the userInfo,
// where a Record ID could be stored, for example.
NSDictionary *notificationUserInfo = [notification userInfo];
[ appProperties setObject:notificationUserInfo forKey:@"initialNotificationUserInfo" ];
}
}
#ifdef DEBUG
jsCodeLocation = [NSURL URLWithString:@"http://192.168.1.77:8081/index.ios.bundle?platform=ios&dev=true"];
#else
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
[[RCTBundleURLProvider sharedSettings] setDefaults];
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"GreatJonesStreet"
initialProperties:nil
launchOptions:launchOptions];
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
rootView.appProperties = appProperties;
NSLog(@"Hello World!");
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
return YES;
}
@end
I was using this for local notifications.
Try changing UIApplicationLaunchOptionsLocalNotificationKey
to UIApplicationLaunchOptionsRemoteNotificationKey
.
Got it. Whenever I change
// Get Local Notification used to launch application.
UILocalNotification *notification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
to
// Get Local Notification used to launch application.
UILocalNotification *notification = [launchOptions objectForKey: UIApplicationLaunchOptionsRemoteNotificationKey];
and run through the same flow, Xcode freezes here:
When I try to hit play, it just rebuilds it instead of getting to my breakpoint. It also seems like it starts up another instance of the project (notice on the left how there are two - that's odd?)
That being said, when I had it as UIApplicationLaunchOptionsLocalNotificationKey
, I inspected the LocalOptions
variable at that breakpoint (which seems just like an array) and noticed it's first object with a key of UIApplicationLaunchOptionsRemoteNotificationKey
:
Those values are indeed what I am passing down through my notification service. So at least we know they're there..
Nice sleuthing! That was gonna be my next suggestion.
So, now that you know it's in there, you know you can grab it.
I haven't dealt with Remote Notifications at all but you'll be able to get it from here. One thing to double check is that UILocalNotification
works as the class type. You might need to use the remote notification version of that as well.
Keep getting after it and make sure you post the successful code change here because I'll definitely be using remote notifications down the road.
Problem is, I'm a JS dude - not an Objective C guy. I'm fucked haha
That being said, thanks for all your help in getting me to this point man. Much appreciated. Would be happy to paypal you a beer.
Hahaha. I'm not an Obj-C guy as well. I'm a Ruby guy. I'll do some research here too. Let's get this solved.
@dwilt Try using this instead:
if (launchOptions != nil) {
// Get Local Notification used to launch application.
NSDictionary *notification = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
if (notification) {
[ appProperties setObject:notification forKey:@"initialNotification" ];
}
}
And see how that works.
And thanks for the offer but helping others is its own reward. Besides, I've gotten so much help in the past, I still have a deficit balance. ;)
Bingo! That worked. For some reason though, Xcode doesn't spit out the console stuff at all when using this delayed startup. Really fucking annoying. Had to stringify the object just to see it's value. Anyway, it's now on my root app component's props, as initialNotification
:
class GreatJonesStreet extends Component {
componentDidMount() {
var { initialNotification } = this.props;
// initialNotification === {
// aps: {
// alert: "Check out our new story: \"Dan's Magical Mystery Tour\"!",
// },
// ^d: "greatjonesstreet://story-detail",
// storyId: "2",
// storyTitle: "Dan's Magical Mystery Tour",
// _: "knsUgFJKlEeaqhNS-2a-UJg"
// }
if (initialNotification) {
AlertIOS.alert(JSON.stringify(initialNotification));
this._handleNotification(initialNotification);
}
PushNotificationIOS.addEventListener('notification', notification => {
var data = notification.getData();
if (data) {
this._handleNotification(data);
}
});
}
componentWillUnmount() {
PushNotificationIOS.removeEventListener('notification', this._notificationListener.bind(this));
}
_notificationListener(notification) {
var data = notification.getData();
if (data) {
this._handleNotification(data);
}
}
_handleNotification(data) {
var action = data['^d'] ? data['^d'].split('greatjonesstreet://')[1] : null;
switch (action) {
case 'story-detail':
var title = data.storyTitle,
id = parseFloat(data.storyId);
if (title && id) {
this._goToStory(id, title);
}
break;
default:
break;
}
}
_goToStory(id, title) {
Actions.StoryDetail({
id,
title
});
}
render() {
const store = configureStore(getInitialState());
//Connect w/ the Router
const ConnectedRouter = connect()(Router);
store.dispatch(setPlatform(platform));
store.dispatch(setVersion(VERSION));
// setup the router table with App selected as the initial component
return (
<Provider store={store}>
<ConnectedRouter hideNavBar={true}>
<Scene key="root">
<Scene key="Home" component={Home} initial={true}/>
<Scene key="CollectionDetail" component={CollectionDetail}/>
<Scene key="StoryDetail" component={StoryDetail}/>
</Scene>
</ConnectedRouter>
</Provider>
);
}
}
Something to note: The initialNotification
doesn't have the methods getData()
and getAlert()
like the notification object we get from the PushNotificationIOS.addEventListener
method. It's just a raw object with the data. What would be ideal is if it was the same kind of object so I could just pass both the initial and listener notification objects to the same function. Thanks so much @joshuapinter. Great team work. You've increased your karma balance! Now I'm more in debt :(
đź‘Ť Whoop whoop!
Awesome to hear. And, yeah, I totally agree that React Native should treat these like their other Notifications with getData()
, etc.
That's really what this issue is all about. I'll gladly make a pull request to get React Native properly fixed but I want to hear from somebody on their core team first to make sure they are on board.
@dwilt @joshuapinter this thread has helped me a lot but I'm curious how RCTPushNotificationManager
and UAirship
link up. I don't want to use Urban Airship's [UAirship push].userPushNotificationsEnabled = YES;
in AppDelegate.m
because I don't have control of when the request modal will show up.
Using PushNotificationIOS.requestPermissions()
is really what I want but I'm not sure how to get UAirship
to send deviceTokens in the callback. I don't know anything about it but is this "automatically" handled via method swizzling?
Any ideas?
When you say "the request modal" - which modal are you talking about? I have [UAirship push].userPushNotificationsEnabled = YES
set but I don't have any modal's showing.
I am also using PushNotificationIOS.requestPermissions()
in my app root file where I register my listeners for react router flux.
Also, which callback are you talking about - the one that would fire when a notification is received from PushNotificationIOS.addEventListener('notification'
? Give me a little more code and context so I can help
@dwilt according to Urban Airship, [UAirship push].userPushNotificationsEnabled = YES
shows an alert to get permissions (like this) when the app starts up. If you've already given permission, you won't see this again.
PushNotificationIOS.requestPermissions()
can also show that alert whenever you want instead of on app start. According to the RN Docs,
This method returns a promise that will resolve when the user accepts, rejects, or if the permissions were previously rejected. The promise resolves to the current state of the permission.
Got it - yes, I'm seeing this alert on initial load of my app too. I've got both of those settings set so I'm not sure which is doing it though. What's your question?
So, I don't want to show the notification at startup — I can show it when I want to via PushNotificationIOS.requestPermissions()
. I just don't know how to tie it up with Urban Airship.
Nevermind, apparently this IS handled with method swizzling!
Ok, glad you got it figured out. Sorry I couldn't be more of a help
@dwilt's approach worked for me, but I've noticed that the notification
object is shaped very differently depending on whether it's provided by one of the PushNotificationIOS events vs. this custom Obj-C pathway.
See the third panel here: prop names are different, object is structured differently. I've highlighted the custom JS payload that the RN app consumes.
@tomprogers yep, I mentioned that above too. Ran into the same thing and am normalizing the data before passing it into my reusable function :( Not ideal, but it works. What's frustrating is you don't get the getData()
, getMessage()
, etc methods that are exposed via RN when it's a cold start (coming straight from Obj-C)
@dwilt actually, I didn't figure it out, and I'm guessing it's due to the notification
object shape mentioned by @tomprogers above. How did you guys normalize the data?
Concerning requestPermissions()
, the RN Docs say
This method returns a promise that will resolve when the user accepts, rejects, or if the permissions were previously rejected. The promise resolves to the current state of the permission.
But when I try:
PushNotificationsIOS.requestPermissions().then((permissions) => console.log(permissions));
I never hit the console.log()
. What am I missing?
@chandlervdw so the requestPermissions.then
callback not firing is not something I have any experience with.
Regarding the normalizing the data, here's how I'm doing it in my app:
class GreatJonesStreet extends Component {
componentDidMount() {
var { initialNotification } = this.props;
if (initialNotification) {
// TODO: BS solution until this is resolved: https://github.com/aksonov/react-native-router-flux/issues/686
setTimeout(() => {
this._handleNotification(initialNotification.custom.a);
}, 1000);
}
PushNotificationIOS.addEventListener('notification', this._onNotification.bind(this));
}
_onNotification(notification) {
var data = notification.getData();
this._handleNotification(data.additionalData);
}
_handleNotification(notification = {}) {
switch(notification.action) {
case 'story-detail':
let { storyId, storyTitle } = notification;
storyId = parseFloat(storyId);
this._goToStory(storyId, storyTitle);
break;
case 'collection-detail':
let { collectionId, collectionTitle } = notification;
collectionId = parseFloat(collectionId);
this._goToCollection(collectionId, collectionTitle);
break;
default:
break;
}
}
_goToStory(id, title) {
Actions.StoryDetail({
id,
title
});
}
_goToCollection(id, title) {
Actions.CollectionDetail({
id,
title
});
}
render() {
const store = configureStore(getInitialState());
//Connect w/ the Router
const ConnectedRouter = connect()(Router);
store.dispatch(setPlatform(platform));
store.dispatch(setVersion(VERSION));
// setup the router table with App selected as the initial component
return (
<Provider store={store}>
<ConnectedRouter hideNavBar={true}>
<Scene key="root">
<Scene key="Home" component={Home} initial={true}/>
<Scene key="CollectionDetail" component={CollectionDetail}/>
<Scene key="StoryDetail" component={StoryDetail}/>
</Scene>
</ConnectedRouter>
</Provider>
);
}
}
I'm handling notifications from 2 areas:
initialNotification
object is available on the props
. This is the "cold start" scenario that @tomprogers referenced above. Because I'm using OneSignal the structure of their push notification is a bit odd and grabbing from the object what I need (the custom data from the push notification to reroute the user to a deeper page) via this._handleNotification(initialNotification.custom.a);
.PushNotificationIOS.addEventListener('notification', this._onNotification.bind(this));
. Then in _onNotification
, I'm using getData()
which is available here and only here and passing the data into _onNotification
: this._handleNotification(data.additionalData);
. Let me know if this makes sense. Again, not ideal, but it works.
Just wanted to leave a +1 for @dwilt & @joshuapinter . Helped me immensely!
Just wanted to come back here and note: I'm using OneSignal and it was a fuck ton easier to setup and it has the same objects passed to their onNotification
handler so you don't need custom code for each scenario.
Are you folks still seeing this issue? @dwilt @joshuapinter
@ericnakagawa we have since moved over to OneSignal for PushNotifications and it's been a lot easier since so I'm using their guides: https://github.com/geektimecoil/react-native-onesignal
Notice how they're using pendingNotifications
though: https://github.com/geektimecoil/react-native-onesignal#ios-usage
@ericnakagawa Will test again when I get a chance.
Closing this issue because it has been inactive for a while. If you think it should still be opened let us know why.
@hramos I think we should keep this open until React Native handles this properly in its core. We're essentially having to use a hack to handle cold start notifications.
Like I mentioned earlier I wouldn't mind creating a PR to handle this better but wanted to get some input from the React Native core team before spending the effort to do it.
Let me know what you think.
I think the best approach here would be to send a PR. We can loop in the core team then.
Handling Local Push Notifications when the app is in the
background
orinactive
state works just fine.When your app is not running (cold start) and is launched from a Local Push Notification, the practice (according to the current docs) is to use
PushNotificationIOS.getInitialNotification
. However, this always returnsnull
.I spent a long time trying to get this to work with
getInitialNotification
but nothing worked. I had to essentially capture the notification in thelaunchOptions
and then pass it in viaappProperties
.My solution is captured on this StackOverflow Answer but here is the crux of it.
AppDelegate.m
index.ios.js
Environment: