@@ -4,6 +4,13 @@ import fs from 'fs';
44jest . mock ( '@expo/config-plugins' , ( ) => ( {
55 withDangerousMod : ( config : any , [ _platform , callback ] : [ string , Function ] ) =>
66 callback ( config ) ,
7+ withAndroidManifest : ( config : any , callback : Function ) => callback ( config ) ,
8+ AndroidConfig : {
9+ Manifest : {
10+ getMainApplicationOrThrow : ( modResults : any ) =>
11+ modResults . manifest . application [ 0 ] ,
12+ } ,
13+ } ,
714} ) ) ;
815
916import { withAndroidPushNotifications } from '../src/expo-plugins/withAndroidPushNotifications' ;
@@ -16,6 +23,17 @@ function createMockConfig(packageName?: string) {
1623 modRequest : {
1724 projectRoot : '/mock/project' ,
1825 } ,
26+ modResults : {
27+ manifest : {
28+ application : [
29+ {
30+ $ : { 'android:name' : '.MainApplication' } ,
31+ activity : [ ] ,
32+ service : [ ] as any [ ] ,
33+ } ,
34+ ] ,
35+ } ,
36+ } ,
1937 } ;
2038}
2139
@@ -153,6 +171,14 @@ dependencies {
153171 config : any ,
154172 [ _platform , callback ] : [ string , Function ]
155173 ) => callback ( config ) ,
174+ withAndroidManifest : ( config : any , callback : Function ) =>
175+ callback ( config ) ,
176+ AndroidConfig : {
177+ Manifest : {
178+ getMainApplicationOrThrow : ( modResults : any ) =>
179+ modResults . manifest . application [ 0 ] ,
180+ } ,
181+ } ,
156182 } ) ) ;
157183
158184 jest . spyOn ( fs , 'mkdirSync' ) . mockReturnValue ( undefined ) ;
@@ -194,6 +220,14 @@ dependencies {
194220 config : any ,
195221 [ _platform , callback ] : [ string , Function ]
196222 ) => callback ( config ) ,
223+ withAndroidManifest : ( config : any , callback : Function ) =>
224+ callback ( config ) ,
225+ AndroidConfig : {
226+ Manifest : {
227+ getMainApplicationOrThrow : ( modResults : any ) =>
228+ modResults . manifest . application [ 0 ] ,
229+ } ,
230+ } ,
197231 } ) ) ;
198232
199233 jest . spyOn ( fs , 'mkdirSync' ) . mockReturnValue ( undefined ) ;
@@ -225,6 +259,100 @@ dependencies {
225259 } ) ;
226260 } ) ;
227261
262+ describe ( 'AndroidManifest service registration' , ( ) => {
263+ test ( 'adds service entry with correct attributes' , ( ) => {
264+ const config = createMockConfig ( 'com.example.myapp' ) ;
265+ withAndroidPushNotifications ( config as any , { } as any ) ;
266+
267+ const services = config . modResults . manifest . application [ 0 ] . service ;
268+ expect ( services ) . toHaveLength ( 1 ) ;
269+
270+ const service = services [ 0 ] ;
271+ expect ( service . $ [ 'android:name' ] ) . toBe (
272+ '.IntercomFirebaseMessagingService'
273+ ) ;
274+ expect ( service . $ [ 'android:exported' ] ) . toBe ( 'false' ) ;
275+ } ) ;
276+
277+ test ( 'registers MESSAGING_EVENT intent filter with priority' , ( ) => {
278+ const config = createMockConfig ( 'com.example.myapp' ) ;
279+ withAndroidPushNotifications ( config as any , { } as any ) ;
280+
281+ const service = config . modResults . manifest . application [ 0 ] . service [ 0 ] ;
282+ const intentFilter = service [ 'intent-filter' ] [ 0 ] ;
283+ const action = intentFilter . action [ 0 ] ;
284+
285+ expect ( action . $ [ 'android:name' ] ) . toBe (
286+ 'com.google.firebase.MESSAGING_EVENT'
287+ ) ;
288+ expect ( intentFilter . $ [ 'android:priority' ] ) . toBe ( '10' ) ;
289+ } ) ;
290+
291+ test ( 'preserves existing services when adding Intercom service' , ( ) => {
292+ const config = createMockConfig ( 'com.example.myapp' ) ;
293+
294+ config . modResults . manifest . application [ 0 ] . service . push ( {
295+ $ : {
296+ 'android:name' : '.SomeOtherService' ,
297+ 'android:exported' : 'false' ,
298+ } ,
299+ } as any ) ;
300+
301+ withAndroidPushNotifications ( config as any , { } as any ) ;
302+
303+ const services = config . modResults . manifest . application [ 0 ] . service ;
304+ expect ( services ) . toHaveLength ( 2 ) ;
305+ expect ( services [ 0 ] . $ [ 'android:name' ] ) . toBe ( '.SomeOtherService' ) ;
306+ expect ( services [ 1 ] . $ [ 'android:name' ] ) . toBe (
307+ '.IntercomFirebaseMessagingService'
308+ ) ;
309+ } ) ;
310+
311+ test ( 'does not duplicate service on repeated runs (idempotency)' , ( ) => {
312+ const config = createMockConfig ( 'com.example.myapp' ) ;
313+
314+ withAndroidPushNotifications ( config as any , { } as any ) ;
315+ withAndroidPushNotifications ( config as any , { } as any ) ;
316+
317+ const services = config . modResults . manifest . application [ 0 ] . service ;
318+ expect ( services ) . toHaveLength ( 1 ) ;
319+ } ) ;
320+
321+ test ( 'skips registration and warns when another FCM service exists' , ( ) => {
322+ const config = createMockConfig ( 'com.example.myapp' ) ;
323+ const warnSpy = jest . spyOn ( console , 'warn' ) . mockImplementation ( ) ;
324+
325+ config . modResults . manifest . application [ 0 ] . service . push ( {
326+ '$' : {
327+ 'android:name' : '.ExistingFcmService' ,
328+ 'android:exported' : 'true' ,
329+ } ,
330+ 'intent-filter' : [
331+ {
332+ action : [
333+ {
334+ $ : {
335+ 'android:name' : 'com.google.firebase.MESSAGING_EVENT' ,
336+ } ,
337+ } ,
338+ ] ,
339+ } ,
340+ ] ,
341+ } as any ) ;
342+
343+ withAndroidPushNotifications ( config as any , { } as any ) ;
344+
345+ const services = config . modResults . manifest . application [ 0 ] . service ;
346+ expect ( services ) . toHaveLength ( 1 ) ;
347+ expect ( services [ 0 ] . $ [ 'android:name' ] ) . toBe ( '.ExistingFcmService' ) ;
348+ expect ( warnSpy ) . toHaveBeenCalledWith (
349+ expect . stringContaining ( 'existing FirebaseMessagingService' )
350+ ) ;
351+
352+ warnSpy . mockRestore ( ) ;
353+ } ) ;
354+ } ) ;
355+
228356 describe ( 'error handling' , ( ) => {
229357 test ( 'throws if android.package is not defined' , ( ) => {
230358 const config = createMockConfig ( ) ;
0 commit comments