Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions __tests__/withAndroidPushNotifications.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import fs from 'fs';
jest.mock('@expo/config-plugins', () => ({
withDangerousMod: (config: any, [_platform, callback]: [string, Function]) =>
callback(config),
withAndroidManifest: (config: any, callback: Function) => callback(config),
AndroidConfig: {
Manifest: {
getMainApplicationOrThrow: (modResults: any) =>
modResults.manifest.application[0],
},
},
}));

import { withAndroidPushNotifications } from '../src/expo-plugins/withAndroidPushNotifications';
Expand All @@ -16,6 +23,17 @@ function createMockConfig(packageName?: string) {
modRequest: {
projectRoot: '/mock/project',
},
modResults: {
manifest: {
application: [
{
$: { 'android:name': '.MainApplication' },
activity: [],
service: [] as any[],
},
],
},
},
};
}

Expand Down Expand Up @@ -178,6 +196,100 @@ dependencies {
});
});

describe('AndroidManifest service registration', () => {
test('adds service entry with correct attributes', () => {
const config = createMockConfig('com.example.myapp');
withAndroidPushNotifications(config as any, {} as any);

const services = config.modResults.manifest.application[0].service;
expect(services).toHaveLength(1);

const service = services[0];
expect(service.$['android:name']).toBe(
'.IntercomFirebaseMessagingService'
);
expect(service.$['android:exported']).toBe('false');
});

test('registers MESSAGING_EVENT intent filter with priority', () => {
const config = createMockConfig('com.example.myapp');
withAndroidPushNotifications(config as any, {} as any);

const service = config.modResults.manifest.application[0].service[0];
const intentFilter = service['intent-filter'][0];
const action = intentFilter.action[0];

expect(action.$['android:name']).toBe(
'com.google.firebase.MESSAGING_EVENT'
);
expect(intentFilter.$['android:priority']).toBe('10');
});

test('preserves existing services when adding Intercom service', () => {
const config = createMockConfig('com.example.myapp');

config.modResults.manifest.application[0].service.push({
$: {
'android:name': '.SomeOtherService',
'android:exported': 'false',
},
} as any);

withAndroidPushNotifications(config as any, {} as any);

const services = config.modResults.manifest.application[0].service;
expect(services).toHaveLength(2);
expect(services[0].$['android:name']).toBe('.SomeOtherService');
expect(services[1].$['android:name']).toBe(
'.IntercomFirebaseMessagingService'
);
});

test('does not duplicate service on repeated runs (idempotency)', () => {
const config = createMockConfig('com.example.myapp');

withAndroidPushNotifications(config as any, {} as any);
withAndroidPushNotifications(config as any, {} as any);

const services = config.modResults.manifest.application[0].service;
expect(services).toHaveLength(1);
});

test('skips registration and warns when another FCM service exists', () => {
const config = createMockConfig('com.example.myapp');
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();

config.modResults.manifest.application[0].service.push({
'$': {
'android:name': '.ExistingFcmService',
'android:exported': 'true',
},
'intent-filter': [
{
action: [
{
$: {
'android:name': 'com.google.firebase.MESSAGING_EVENT',
},
},
],
},
],
} as any);

withAndroidPushNotifications(config as any, {} as any);

const services = config.modResults.manifest.application[0].service;
expect(services).toHaveLength(1);
expect(services[0].$['android:name']).toBe('.ExistingFcmService');
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('existing FirebaseMessagingService')
);

warnSpy.mockRestore();
});
});

describe('error handling', () => {
test('throws if android.package is not defined', () => {
const config = createMockConfig();
Expand Down
97 changes: 93 additions & 4 deletions src/expo-plugins/withAndroidPushNotifications.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import path from 'path';
import fs from 'fs';

import { type ConfigPlugin, withDangerousMod } from '@expo/config-plugins';
import {
type ConfigPlugin,
withDangerousMod,
withAndroidManifest,
AndroidConfig,
} from '@expo/config-plugins';
import type { IntercomPluginProps } from './@types';

const SERVICE_CLASS_NAME = 'IntercomFirebaseMessagingService';
Expand Down Expand Up @@ -40,9 +45,7 @@ class ${SERVICE_CLASS_NAME} : FirebaseMessagingService() {
* into the app's Android source directory, and ensures firebase-messaging
* is on the app module's compile classpath.
*/
export const withAndroidPushNotifications: ConfigPlugin<IntercomPluginProps> = (
_config
) =>
const writeFirebaseService: ConfigPlugin<IntercomPluginProps> = (_config) =>
withDangerousMod(_config, [
'android',
(config) => {
Expand Down Expand Up @@ -110,3 +113,89 @@ export const withAndroidPushNotifications: ConfigPlugin<IntercomPluginProps> = (
return config;
},
]);

/**
* Adds the FirebaseMessagingService entry to the AndroidManifest.xml
* so Android knows to route FCM events to our service.
*/
const registerServiceInManifest: ConfigPlugin<IntercomPluginProps> = (
_config
) =>
withAndroidManifest(_config, (config) => {
const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(
config.modResults
);

const packageName = config.android?.package;
if (!packageName) {
throw new Error(
'@intercom/intercom-react-native: android.package must be defined in your Expo config to use Android push notifications.'
);
}

const serviceName = `.${SERVICE_CLASS_NAME}`;

const existingService = mainApplication.service?.find(
(s) => s.$?.['android:name'] === serviceName
);

const hasExistingFcmService = mainApplication.service?.some(
(s) =>
s.$?.['android:name'] !== serviceName &&
s['intent-filter']?.some(
(f: any) =>
f.action?.some(
(a: any) =>
a.$?.['android:name'] === 'com.google.firebase.MESSAGING_EVENT'
)
)
);

if (hasExistingFcmService) {
console.warn(
'@intercom/intercom-react-native: An existing FirebaseMessagingService was found in AndroidManifest.xml. ' +
'Skipping automatic Intercom service registration to avoid conflicts. ' +
'You will need to route Intercom pushes manually using IntercomModule.isIntercomPush() and IntercomModule.handleRemotePushMessage().'
);
return config;
}

if (!existingService) {
if (!mainApplication.service) {
mainApplication.service = [];
}

mainApplication.service.push({
'$': {
'android:name': serviceName,
'android:exported': 'false' as any,
},
'intent-filter': [
{
$: {
'android:priority': '10',
} as any,
action: [
{
$: {
'android:name': 'com.google.firebase.MESSAGING_EVENT',
},
},
],
},
],
} as any);
}

return config;
});

export const withAndroidPushNotifications: ConfigPlugin<IntercomPluginProps> = (
config,
props
) => {
let newConfig = config;
newConfig = writeFirebaseService(newConfig, props);
newConfig = registerServiceInManifest(newConfig, props);
return newConfig;
};
6 changes: 4 additions & 2 deletions src/expo-plugins/withPushNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
findObjcFunctionCodeBlock,
insertContentsInsideObjcFunctionBlock,
} from '@expo/config-plugins/build/ios/codeMod';
import { withAndroidPushNotifications } from './withAndroidPushNotifications';

const appDelegate: ConfigPlugin<IntercomPluginProps> = (_config) =>
withAppDelegate(_config, (config) => {
Expand Down Expand Up @@ -59,7 +60,8 @@ export const withIntercomPushNotification: ConfigPlugin<IntercomPluginProps> = (
props
) => {
let newConfig = config;
newConfig = appDelegate(config, props);
newConfig = infoPlist(config, props);
newConfig = appDelegate(newConfig, props);
newConfig = infoPlist(newConfig, props);
newConfig = withAndroidPushNotifications(newConfig, props);
return newConfig;
};