@@ -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
@@ -151,6 +169,13 @@ dependencies {
151169 jest . mock ( '@expo/config-plugins' , ( ) => ( {
152170 withDangerousMod : ( config : any , [ _platform , callback ] : [ string , Function ] ) =>
153171 callback ( config ) ,
172+ withAndroidManifest : ( config : any , callback : Function ) => callback ( config ) ,
173+ AndroidConfig : {
174+ Manifest : {
175+ getMainApplicationOrThrow : ( modResults : any ) =>
176+ modResults . manifest . application [ 0 ] ,
177+ } ,
178+ } ,
154179 } ) ) ;
155180
156181 jest . spyOn ( fs , 'mkdirSync' ) . mockReturnValue ( undefined ) ;
@@ -186,6 +211,13 @@ dependencies {
186211 jest . mock ( '@expo/config-plugins' , ( ) => ( {
187212 withDangerousMod : ( config : any , [ _platform , callback ] : [ string , Function ] ) =>
188213 callback ( config ) ,
214+ withAndroidManifest : ( config : any , callback : Function ) => callback ( config ) ,
215+ AndroidConfig : {
216+ Manifest : {
217+ getMainApplicationOrThrow : ( modResults : any ) =>
218+ modResults . manifest . application [ 0 ] ,
219+ } ,
220+ } ,
189221 } ) ) ;
190222
191223 jest . spyOn ( fs , 'mkdirSync' ) . mockReturnValue ( undefined ) ;
@@ -213,6 +245,100 @@ dependencies {
213245 } ) ;
214246 } ) ;
215247
248+ describe ( 'AndroidManifest service registration' , ( ) => {
249+ test ( 'adds service entry with correct attributes' , ( ) => {
250+ const config = createMockConfig ( 'com.example.myapp' ) ;
251+ withAndroidPushNotifications ( config as any , { } as any ) ;
252+
253+ const services = config . modResults . manifest . application [ 0 ] . service ;
254+ expect ( services ) . toHaveLength ( 1 ) ;
255+
256+ const service = services [ 0 ] ;
257+ expect ( service . $ [ 'android:name' ] ) . toBe (
258+ '.IntercomFirebaseMessagingService'
259+ ) ;
260+ expect ( service . $ [ 'android:exported' ] ) . toBe ( 'false' ) ;
261+ } ) ;
262+
263+ test ( 'registers MESSAGING_EVENT intent filter with priority' , ( ) => {
264+ const config = createMockConfig ( 'com.example.myapp' ) ;
265+ withAndroidPushNotifications ( config as any , { } as any ) ;
266+
267+ const service = config . modResults . manifest . application [ 0 ] . service [ 0 ] ;
268+ const intentFilter = service [ 'intent-filter' ] [ 0 ] ;
269+ const action = intentFilter . action [ 0 ] ;
270+
271+ expect ( action . $ [ 'android:name' ] ) . toBe (
272+ 'com.google.firebase.MESSAGING_EVENT'
273+ ) ;
274+ expect ( intentFilter . $ [ 'android:priority' ] ) . toBe ( '10' ) ;
275+ } ) ;
276+
277+ test ( 'preserves existing services when adding Intercom service' , ( ) => {
278+ const config = createMockConfig ( 'com.example.myapp' ) ;
279+
280+ config . modResults . manifest . application [ 0 ] . service . push ( {
281+ $ : {
282+ 'android:name' : '.SomeOtherService' ,
283+ 'android:exported' : 'false' ,
284+ } ,
285+ } as any ) ;
286+
287+ withAndroidPushNotifications ( config as any , { } as any ) ;
288+
289+ const services = config . modResults . manifest . application [ 0 ] . service ;
290+ expect ( services ) . toHaveLength ( 2 ) ;
291+ expect ( services [ 0 ] . $ [ 'android:name' ] ) . toBe ( '.SomeOtherService' ) ;
292+ expect ( services [ 1 ] . $ [ 'android:name' ] ) . toBe (
293+ '.IntercomFirebaseMessagingService'
294+ ) ;
295+ } ) ;
296+
297+ test ( 'does not duplicate service on repeated runs (idempotency)' , ( ) => {
298+ const config = createMockConfig ( 'com.example.myapp' ) ;
299+
300+ withAndroidPushNotifications ( config as any , { } as any ) ;
301+ withAndroidPushNotifications ( config as any , { } as any ) ;
302+
303+ const services = config . modResults . manifest . application [ 0 ] . service ;
304+ expect ( services ) . toHaveLength ( 1 ) ;
305+ } ) ;
306+
307+ test ( 'skips registration and warns when another FCM service exists' , ( ) => {
308+ const config = createMockConfig ( 'com.example.myapp' ) ;
309+ const warnSpy = jest . spyOn ( console , 'warn' ) . mockImplementation ( ) ;
310+
311+ config . modResults . manifest . application [ 0 ] . service . push ( {
312+ '$' : {
313+ 'android:name' : '.ExistingFcmService' ,
314+ 'android:exported' : 'true' ,
315+ } ,
316+ 'intent-filter' : [
317+ {
318+ action : [
319+ {
320+ $ : {
321+ 'android:name' : 'com.google.firebase.MESSAGING_EVENT' ,
322+ } ,
323+ } ,
324+ ] ,
325+ } ,
326+ ] ,
327+ } as any ) ;
328+
329+ withAndroidPushNotifications ( config as any , { } as any ) ;
330+
331+ const services = config . modResults . manifest . application [ 0 ] . service ;
332+ expect ( services ) . toHaveLength ( 1 ) ;
333+ expect ( services [ 0 ] . $ [ 'android:name' ] ) . toBe ( '.ExistingFcmService' ) ;
334+ expect ( warnSpy ) . toHaveBeenCalledWith (
335+ expect . stringContaining ( 'existing FirebaseMessagingService' )
336+ ) ;
337+
338+ warnSpy . mockRestore ( ) ;
339+ } ) ;
340+ } ) ;
341+
216342 describe ( 'error handling' , ( ) => {
217343 test ( 'throws if android.package is not defined' , ( ) => {
218344 const config = createMockConfig ( ) ;
0 commit comments