Skip to content

Commit 8d75b36

Browse files
committed
feat(messaging, ios): new setAPNSToken API / getToken works on M1 Simulator
1 parent 47c20ca commit 8d75b36

8 files changed

Lines changed: 333 additions & 23 deletions

File tree

packages/messaging/e2e/messaging.e2e.js

Lines changed: 184 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,21 @@
1515
*
1616
*/
1717

18-
describe('messaging() modular', function () {
18+
async function isSimulator() {
19+
return await DeviceInfo.isEmulator();
20+
}
21+
22+
async function isAPNSCapableSimulator() {
23+
supportedAbis = await DeviceInfo.supportedAbis(); // looking for an ARM Simulator implying M1 host
24+
iosVersionMajor = DeviceInfo.getSystemVersion().split('.')[0]; // looking for iOS16+
25+
macOSVersionMajor = require('os').release().split('.')[0]; // host macOS13+ has Darwin kernel 22+
26+
if (macOSVersionMajor >= 22 && supportedAbis.includes('ARM64E') && iosVersionMajor >= 16) {
27+
return true;
28+
}
29+
return false;
30+
}
31+
32+
describe('messaging()', function () {
1933
describe('firebase v8 compatibility', function () {
2034
describe('namespace', function () {
2135
it('accessible from firebase.app()', function () {
@@ -74,9 +88,10 @@ describe('messaging() modular', function () {
7488
});
7589
it('successfully unregisters on ios', async function () {
7690
if (device.getPlatform() === 'ios') {
77-
should.equal(firebase.messaging().isDeviceRegisteredForRemoteMessages, true);
7891
await firebase.messaging().unregisterDeviceForRemoteMessages();
7992
should.equal(firebase.messaging().isDeviceRegisteredForRemoteMessages, false);
93+
await firebase.messaging().registerDeviceForRemoteMessages();
94+
should.equal(firebase.messaging().isDeviceRegisteredForRemoteMessages, true);
8095
} else {
8196
this.skip();
8297
}
@@ -116,9 +131,84 @@ describe('messaging() modular', function () {
116131
this.skip();
117132
}
118133
});
119-
it('resolves null on ios if using simulator', async function () {
134+
it('resolves on ios with token on supported simulators', async function () {
135+
// Make sure we are registered for remote notifications, else no token
136+
await firebase.messaging().registerDeviceForRemoteMessages();
137+
120138
if (device.getPlatform() === 'ios') {
121-
should.equal(await firebase.messaging().getAPNSToken(), null);
139+
apnsToken = await firebase.messaging().getAPNSToken();
140+
141+
simulator = await isSimulator();
142+
aPNSCapableSimulator = await isAPNSCapableSimulator();
143+
144+
if (!simulator || (simulator && aPNSCapableSimulator)) {
145+
apnsToken.should.be.a.String();
146+
} else {
147+
// unsupported iOS Simulator returns null (typically,
148+
// can attempt-but-fail if M1 Simulator but not macOS13+/ios16+, rare combo)
149+
if (apnsToken !== null) {
150+
apnsToken.should.be.a.String();
151+
}
152+
}
153+
} else {
154+
this.skip();
155+
}
156+
});
157+
});
158+
159+
describe('setAPNSToken', function () {
160+
it('requires a token parameter', async function () {
161+
try {
162+
firebase.messaging().setAPNSToken();
163+
return Promise.reject(new Error('Did not throw Error.'));
164+
} catch (e) {
165+
e.message.should.containEql("'token' expected a string value");
166+
}
167+
try {
168+
firebase.messaging().setAPNSToken(123);
169+
return Promise.reject(new Error('Did not throw Error.'));
170+
} catch (e) {
171+
e.message.should.containEql("'token' expected a string value");
172+
return Promise.resolve();
173+
}
174+
});
175+
176+
it('verifies type parameter is valid if specified', async function () {
177+
try {
178+
firebase.messaging().setAPNSToken('typeparamtest', 123);
179+
return Promise.reject(new Error('Did not throw Error.'));
180+
} catch (e) {
181+
e.message.should.containEql("'type' expected one of 'prod', 'sandbox', or 'unknown'");
182+
}
183+
try {
184+
firebase.messaging().setAPNSToken('typeparamtest', 'bogus');
185+
return Promise.reject(new Error('Did not throw Error.'));
186+
} catch (e) {
187+
e.message.should.containEql("'type' expected one of 'prod', 'sandbox', or 'unknown'");
188+
}
189+
});
190+
191+
it('resolves on android', async function () {
192+
if (device.getPlatform() === 'android') {
193+
should.equal(await firebase.messaging().setAPNSToken('foo'), null);
194+
} else {
195+
this.skip();
196+
}
197+
});
198+
199+
it('correctly sets new token on ios', async function () {
200+
if (device.getPlatform() === 'ios') {
201+
originalAPNSToken = await firebase.messaging().getAPNSToken();
202+
// 74657374696E67746F6B656E is hex for "testingtoken"
203+
await firebase.messaging().setAPNSToken('74657374696E67746F6B656E', 'unknown');
204+
newAPNSToken = await firebase.messaging().getAPNSToken();
205+
newAPNSToken.should.eql('74657374696E67746F6B656E');
206+
newAPNSToken.should.not.eql(originalAPNSToken);
207+
if (originalAPNSToken !== null) {
208+
await firebase.messaging().setAPNSToken(originalAPNSToken);
209+
}
210+
} else {
211+
this.skip();
122212
}
123213
});
124214
});
@@ -367,7 +457,7 @@ describe('messaging() modular', function () {
367457
});
368458
});
369459

370-
describe('modular', function () {
460+
describe('firebase v9 modular API', function () {
371461
describe('getMessaging', function () {
372462
it('pass app as argument', function () {
373463
const { getMessaging } = messagingModular;
@@ -418,7 +508,7 @@ describe('messaging() modular', function () {
418508
});
419509
});
420510

421-
describe('isDeviceRegisteredForRemoteMessages', function () {
511+
describe('isDeviceRegisteredForRemoteMessages default state', function () {
422512
it('returns true on android', function () {
423513
const { getMessaging, isDeviceRegisteredForRemoteMessages } = messagingModular;
424514

@@ -430,7 +520,7 @@ describe('messaging() modular', function () {
430520
});
431521
});
432522

433-
describe('unregisterDeviceForRemoteMessages', function () {
523+
describe('remote message device register / unregister', function () {
434524
it('resolves on android, remains registered', async function () {
435525
const {
436526
getMessaging,
@@ -450,12 +540,14 @@ describe('messaging() modular', function () {
450540
getMessaging,
451541
unregisterDeviceForRemoteMessages,
452542
isDeviceRegisteredForRemoteMessages,
543+
registerDeviceForRemoteMessages,
453544
} = messagingModular;
454545

455546
if (device.getPlatform() === 'ios') {
456-
should.equal(isDeviceRegisteredForRemoteMessages(getMessaging()), true);
457547
await unregisterDeviceForRemoteMessages(getMessaging());
458548
should.equal(isDeviceRegisteredForRemoteMessages(getMessaging()), false);
549+
await registerDeviceForRemoteMessages(getMessaging());
550+
should.equal(isDeviceRegisteredForRemoteMessages(getMessaging()), true);
459551
} else {
460552
this.skip();
461553
}
@@ -499,10 +591,91 @@ describe('messaging() modular', function () {
499591
this.skip();
500592
}
501593
});
502-
it('resolves null on ios if using simulator', async function () {
503-
const { getMessaging, getAPNSToken } = messagingModular;
594+
it('resolves on ios with token on supported simulators', async function () {
595+
// Make sure we are registered for remote notifications, else no token
596+
const { getMessaging, getAPNSToken, registerDeviceForRemoteMessages } = messagingModular;
597+
await registerDeviceForRemoteMessages(getMessaging());
598+
504599
if (device.getPlatform() === 'ios') {
505-
should.equal(await getAPNSToken(getMessaging()), null);
600+
apnsToken = await getAPNSToken(getMessaging());
601+
602+
simulator = await isSimulator();
603+
aPNSCapableSimulator = await isAPNSCapableSimulator();
604+
605+
if (!simulator || (simulator && aPNSCapableSimulator)) {
606+
apnsToken.should.be.a.String();
607+
} else {
608+
// unsupported iOS Simulator returns null (typically,
609+
// can attempt-but-fail if M1 Simulator but not macOS13+/ios16+, rare combo)
610+
if (apnsToken !== null) {
611+
apnsToken.should.be.a.String();
612+
}
613+
}
614+
} else {
615+
this.skip();
616+
}
617+
});
618+
});
619+
620+
describe('setAPNSToken', function () {
621+
it('requires a token parameter', async function () {
622+
const { getMessaging, setAPNSToken } = messagingModular;
623+
try {
624+
setAPNSToken(getMessaging());
625+
return Promise.reject(new Error('Did not throw Error.'));
626+
} catch (e) {
627+
e.message.should.containEql("'token' expected a string value");
628+
}
629+
try {
630+
setAPNSToken(getMessaging(), 123);
631+
return Promise.reject(new Error('Did not throw Error.'));
632+
} catch (e) {
633+
e.message.should.containEql("'token' expected a string value");
634+
return Promise.resolve();
635+
}
636+
});
637+
638+
it('verifies type parameter is valid if specified', async function () {
639+
const { getMessaging, setAPNSToken } = messagingModular;
640+
try {
641+
setAPNSToken(getMessaging(), 'typeparamtest', 123);
642+
return Promise.reject(new Error('Did not throw Error.'));
643+
} catch (e) {
644+
e.message.should.containEql("'type' expected one of 'prod', 'sandbox', or 'unknown'");
645+
}
646+
try {
647+
setAPNSToken(getMessaging(), 'typeparamtest', 'bogus');
648+
return Promise.reject(new Error('Did not throw Error.'));
649+
} catch (e) {
650+
e.message.should.containEql("'type' expected one of 'prod', 'sandbox', or 'unknown'");
651+
}
652+
});
653+
654+
it('resolves on android', async function () {
655+
const { getMessaging, setAPNSToken } = messagingModular;
656+
if (device.getPlatform() === 'android') {
657+
should.equal(await setAPNSToken(getMessaging(), 'foo'), null);
658+
} else {
659+
this.skip();
660+
}
661+
});
662+
663+
it('correctly sets new token on ios', async function () {
664+
const { getMessaging, getAPNSToken, setAPNSToken } = messagingModular;
665+
if (device.getPlatform() === 'ios') {
666+
originalAPNSToken = await getAPNSToken(getMessaging());
667+
// 74657374696E67746F6B656E6D6F64756C6172 is hex for "testingtokenmodular"
668+
await firebase
669+
.messaging()
670+
.setAPNSToken('74657374696E67746F6B656E6D6F64756C6172', 'unknown');
671+
newAPNSToken = await firebase.messaging().getAPNSToken();
672+
newAPNSToken.should.eql('74657374696E67746F6B656E6D6F64756C6172');
673+
newAPNSToken.should.not.eql(originalAPNSToken);
674+
if (originalAPNSToken !== null) {
675+
await setAPNSToken(getMessaging(), originalAPNSToken);
676+
}
677+
} else {
678+
this.skip();
506679
}
507680
});
508681
});

packages/messaging/ios/RNFBMessaging/RNFBMessaging+NSNotificationCenter.m

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,12 @@ - (void)application_onDidFinishLaunchingNotification:(nonnull NSNotification *)n
8787
(RCTRootView *)[UIApplication sharedApplication].delegate.window.rootViewController.view;
8888
}
8989

90-
#if !(TARGET_IPHONE_SIMULATOR)
90+
// #if !(TARGET_IPHONE_SIMULATOR)
9191
if ([[RNFBJSON shared] getBooleanValue:@"messaging_ios_auto_register_for_remote_messages"
9292
defaultValue:YES]) {
9393
[[UIApplication sharedApplication] registerForRemoteNotifications];
9494
}
95-
#endif
95+
// #endif
9696

9797
if (notification.userInfo[UIApplicationLaunchOptionsRemoteNotificationKey]) {
9898
if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
@@ -108,7 +108,7 @@ - (void)application_onDidFinishLaunchingNotification:(nonnull NSNotification *)n
108108
}
109109
}
110110

111-
#if !(TARGET_IPHONE_SIMULATOR)
111+
// #if !(TARGET_IPHONE_SIMULATOR)
112112
// When an app launches in the background (BG mode) and is launched with the notification
113113
// launch option the app delegate method
114114
// application:didReceiveRemoteNotification:fetchCompletionHandler: will not get called unless
@@ -118,7 +118,7 @@ - (void)application_onDidFinishLaunchingNotification:(nonnull NSNotification *)n
118118
// `messaging_ios_auto_register_for_remote_messages` as this is most likely an app launching
119119
// as a result of a remote notification - so has been registered previously
120120
[[UIApplication sharedApplication] registerForRemoteNotifications];
121-
#endif
121+
// #endif
122122
} else {
123123
if (rctRootView != nil) {
124124
isHeadless = NO;

packages/messaging/ios/RNFBMessaging/RNFBMessagingModule.m

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ - (NSDictionary *)constantsToExport {
114114
: (NSString *)senderId
115115
: (RCTPromiseResolveBlock)resolve
116116
: (RCTPromiseRejectBlock)reject) {
117-
#if !(TARGET_IPHONE_SIMULATOR)
117+
// #if !(TARGET_IPHONE_SIMULATOR)
118118
if ([UIApplication sharedApplication].isRegisteredForRemoteNotifications == NO) {
119119
[RNFBSharedUtils
120120
rejectPromiseWithUserInfo:reject
@@ -126,7 +126,7 @@ - (NSDictionary *)constantsToExport {
126126
}];
127127
return;
128128
}
129-
#endif
129+
// #endif
130130

131131
[[FIRMessaging messaging]
132132
retrieveFCMTokenForSenderID:senderId
@@ -160,7 +160,17 @@ - (NSDictionary *)constantsToExport {
160160
if (apnsToken) {
161161
resolve([RNFBMessagingSerializer APNSTokenFromNSData:apnsToken]);
162162
} else {
163-
#if !(TARGET_IPHONE_SIMULATOR)
163+
#if TARGET_IPHONE_SIMULATOR
164+
#if !TARGET_CPU_ARM64
165+
DLog(@"RNFBMessaging getAPNSToken - Simulator without APNS support detected, with no token "
166+
@"set. Use setAPNSToken with an arbitrary string if needed for testing.")
167+
resolve([NSNull null]);
168+
return;
169+
#endif
170+
DLog(@"RNFBMessaging getAPNSToken - ARM64 Simulator detected, but no APNS token set. Assuming "
171+
@"APNS token is possible. macOS13+ / iOS16+ / M1 mac required for assumption to be valid. "
172+
@"Use setAPNSToken in testing if needed.");
173+
#endif
164174
if ([UIApplication sharedApplication].isRegisteredForRemoteNotifications == NO) {
165175
[RNFBSharedUtils
166176
rejectPromiseWithUserInfo:reject
@@ -172,11 +182,28 @@ - (NSDictionary *)constantsToExport {
172182
}];
173183
return;
174184
}
175-
#endif
176-
resolve([NSNull null]);
177185
}
178186
}
179187

188+
RCT_EXPORT_METHOD(setAPNSToken
189+
: (NSString *)token
190+
: (NSString *)type
191+
: (RCTPromiseResolveBlock)resolve
192+
: (RCTPromiseRejectBlock)reject) {
193+
// Default to unknown (determined by provisioning profile) type, but user may have passed type as
194+
// param
195+
FIRMessagingAPNSTokenType tokenType = FIRMessagingAPNSTokenTypeUnknown;
196+
if (type != nil && [@"prod" isEqualToString:type]) {
197+
tokenType = FIRMessagingAPNSTokenTypeProd;
198+
} else if (type != nil && [@"sandbox" isEqualToString:type]) {
199+
tokenType = FIRMessagingAPNSTokenTypeSandbox;
200+
}
201+
202+
[[FIRMessaging messaging] setAPNSToken:[RNFBMessagingSerializer APNSTokenDataFromNSString:token]
203+
type:tokenType];
204+
resolve([NSNull null]);
205+
}
206+
180207
RCT_EXPORT_METHOD(getIsHeadless : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) {
181208
RNFBMessagingNSNotificationCenter *notifCenter =
182209
[RNFBMessagingNSNotificationCenter sharedInstance];
@@ -267,12 +294,19 @@ - (NSDictionary *)constantsToExport {
267294
: (RCTPromiseResolveBlock)resolve
268295
: (RCTPromiseRejectBlock)reject) {
269296
#if TARGET_IPHONE_SIMULATOR
297+
#if !TARGET_CPU_ARM64
298+
// Do the registration on this unsupported simulator, but don't set up to wait for a token that
299+
// won't arrive
300+
[[UIApplication sharedApplication] registerForRemoteNotifications];
270301
resolve(@([RCTConvert BOOL:@(YES)]));
271302
return;
303+
#endif
304+
DLog(@"RNFBMessaging registerForRemoteNotifications ARM64 Simulator detected, attempting real "
305+
@"registration. macOS13+ / iOS16+ / M1 mac required or will timeout.")
272306
#endif
273307
#pragma clang diagnostic push
274308
#pragma clang diagnostic ignored "-Wunreachable-code"
275-
if (@available(iOS 10.0, *)) {
309+
if (@available(iOS 10.0, *)) {
276310
#pragma pop
277311
if ([UIApplication sharedApplication].isRegisteredForRemoteNotifications == YES) {
278312
resolve(@([RCTConvert BOOL:@(YES)]));
@@ -286,7 +320,8 @@ - (NSDictionary *)constantsToExport {
286320
dispatch_async(dispatch_get_main_queue(), ^{
287321
[[UIApplication sharedApplication] registerForRemoteNotifications];
288322
});
289-
} else {
323+
}
324+
else {
290325
[RNFBSharedUtils
291326
rejectPromiseWithUserInfo:reject
292327
userInfo:[@{

0 commit comments

Comments
 (0)