Skip to content
Merged
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
30 changes: 26 additions & 4 deletions __mocks__/react-native.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
import { vi } from 'vitest';

export const createEmitterSubscriptionMock = (
eventName: string,
callback: (payload: unknown) => void,
) => ({
remove: vi.fn(),
emitter: {
addListener: vi.fn(),
removeAllListeners: vi.fn(),
listenerCount: vi.fn(() => 1),
emit: vi.fn(),
},
listener: () => callback,
context: undefined,
eventType: eventName,
key: 0,
subscriber: {
addSubscription: vi.fn(),
removeSubscription: vi.fn(),
removeAllSubscriptions: vi.fn(),
getSubscriptionsForType: vi.fn(),
},
});

const mockRNOneSignal = {
initialize: vi.fn(),
login: vi.fn(),
Expand Down Expand Up @@ -61,6 +84,7 @@ const mockRNOneSignal = {
addOutcome: vi.fn(),
addUniqueOutcome: vi.fn(),
addOutcomeWithValue: vi.fn(),
displayNotification: vi.fn(),
};

const mockPlatform = {
Expand All @@ -78,10 +102,8 @@ export { mockPlatform, mockRNOneSignal };
export class NativeEventEmitter {
constructor(_nativeModule: typeof mockRNOneSignal) {}

addListener(_eventName: string, _callback: (payload: unknown) => void) {
return {
remove: vi.fn(),
};
addListener(eventName: string, callback: (payload: unknown) => void) {
return createEmitterSubscriptionMock(eventName, callback);
}

removeListener(_eventName: string, _callback: (payload: unknown) => void) {
Expand Down
242 changes: 242 additions & 0 deletions src/OSNotification.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { NativeModules, Platform } from 'react-native';
import OSNotification, { type BaseNotificationData } from './OSNotification';

const mockRNOneSignal = NativeModules.OneSignal;
const mockPlatform = Platform;

describe('OSNotification', () => {
const baseNotificationData: BaseNotificationData = {
body: 'Test notification body',
sound: 'default',
title: 'Test Title',
launchURL: 'https://example.com',
rawPayload: { key: 'value' },
actionButtons: [{ id: 'btn1', text: 'Button 1' }],
additionalData: { custom: 'data' },
notificationId: 'test-notification-id',
};

beforeEach(() => {
mockPlatform.OS = 'ios';
mockRNOneSignal.displayNotification.mockClear();
});

describe('constructor', () => {
test('should initialize common properties', () => {
const notification = new OSNotification(baseNotificationData);

expect(notification.body).toBe('Test notification body');
expect(notification.sound).toBe('default');
expect(notification.title).toBe('Test Title');
expect(notification.launchURL).toBe('https://example.com');
expect(notification.rawPayload).toEqual({ key: 'value' });
expect(notification.actionButtons).toEqual([
{ id: 'btn1', text: 'Button 1' },
]);
expect(notification.additionalData).toEqual({ custom: 'data' });
expect(notification.notificationId).toBe('test-notification-id');
});

test('should initialize with optional common properties as undefined', () => {
const notificationData = {
body: 'Test body',
rawPayload: '',
notificationId: 'id-123',
};
const notification = new OSNotification(notificationData);

expect(notification.body).toBe('Test body');
expect(notification.sound).toBeUndefined();
expect(notification.title).toBeUndefined();
expect(notification.launchURL).toBeUndefined();
expect(notification.actionButtons).toBeUndefined();
expect(notification.additionalData).toBeUndefined();
});

describe('Android-specific properties', () => {
beforeEach(() => {
mockPlatform.OS = 'android';
});

test('should initialize Android-specific properties on Android platform', () => {
const androidData = {
...baseNotificationData,
groupKey: 'group-1',
groupMessage: 'group message',
ledColor: 'FFFF0000',
priority: 2,
smallIcon: 'icon_small',
largeIcon: 'icon_large',
bigPicture: 'image_url',
collapseId: 'collapse-1',
fromProjectNumber: '123456789',
smallIconAccentColor: 'FFFF0000',
lockScreenVisibility: '1',
androidNotificationId: 456,
};
const notification = new OSNotification(androidData);

expect(notification.groupKey).toBe('group-1');
expect(notification.groupMessage).toBe('group message');
expect(notification.ledColor).toBe('FFFF0000');
expect(notification.priority).toBe(2);
expect(notification.smallIcon).toBe('icon_small');
expect(notification.largeIcon).toBe('icon_large');
expect(notification.bigPicture).toBe('image_url');
expect(notification.collapseId).toBe('collapse-1');
expect(notification.fromProjectNumber).toBe('123456789');
expect(notification.smallIconAccentColor).toBe('FFFF0000');
expect(notification.lockScreenVisibility).toBe('1');
expect(notification.androidNotificationId).toBe(456);
});

test('should not set iOS-specific properties on Android', () => {
const notification = new OSNotification(baseNotificationData);

expect(notification.badge).toBeUndefined();
expect(notification.badgeIncrement).toBeUndefined();
expect(notification.category).toBeUndefined();
expect(notification.threadId).toBeUndefined();
expect(notification.subtitle).toBeUndefined();
expect(notification.templateId).toBeUndefined();
expect(notification.templateName).toBeUndefined();
expect(notification.attachments).toBeUndefined();
expect(notification.mutableContent).toBeUndefined();
expect(notification.contentAvailable).toBeUndefined();
expect(notification.relevanceScore).toBeUndefined();
expect(notification.interruptionLevel).toBeUndefined();
});
});

describe('iOS-specific properties', () => {
beforeEach(() => {
mockPlatform.OS = 'ios';
});

test('should initialize iOS-specific properties on iOS platform', () => {
const iosData = {
...baseNotificationData,
badge: '1',
badgeIncrement: '5',
category: 'NOTIFICATION_CATEGORY',
threadId: 'thread-123',
subtitle: 'Test Subtitle',
templateId: 'template-1',
templateName: 'template-name',
attachments: { key: 'attachment-value' },
mutableContent: true,
contentAvailable: '1',
relevanceScore: 0.9,
interruptionLevel: 'timeSensitive',
};
const notification = new OSNotification(iosData);

expect(notification.badge).toBe('1');
expect(notification.badgeIncrement).toBe('5');
expect(notification.category).toBe('NOTIFICATION_CATEGORY');
expect(notification.threadId).toBe('thread-123');
expect(notification.subtitle).toBe('Test Subtitle');
expect(notification.templateId).toBe('template-1');
expect(notification.templateName).toBe('template-name');
expect(notification.attachments).toEqual({ key: 'attachment-value' });
expect(notification.mutableContent).toBe(true);
expect(notification.contentAvailable).toBe('1');
expect(notification.relevanceScore).toBe(0.9);
expect(notification.interruptionLevel).toBe('timeSensitive');
});

test('should not set Android-specific properties on iOS', () => {
const notification = new OSNotification(baseNotificationData);

expect(notification.groupKey).toBeUndefined();
expect(notification.groupMessage).toBeUndefined();
expect(notification.ledColor).toBeUndefined();
expect(notification.priority).toBeUndefined();
expect(notification.smallIcon).toBeUndefined();
expect(notification.largeIcon).toBeUndefined();
expect(notification.bigPicture).toBeUndefined();
expect(notification.collapseId).toBeUndefined();
expect(notification.fromProjectNumber).toBeUndefined();
expect(notification.smallIconAccentColor).toBeUndefined();
expect(notification.lockScreenVisibility).toBeUndefined();
expect(notification.androidNotificationId).toBeUndefined();
});
});
});

describe('display', () => {
test('should call native displayNotification with notificationId', () => {
const notification = new OSNotification(baseNotificationData);
notification.display();

expect(mockRNOneSignal.displayNotification).toHaveBeenCalledWith(
'test-notification-id',
);
});

test('should display notification with different notificationId', () => {
const notificationID = 'custom-id-789';
const notification = new OSNotification({
...baseNotificationData,
notificationId: notificationID,
});
notification.display();

expect(mockRNOneSignal.displayNotification).toHaveBeenCalledWith(
notificationID,
);
});

test('should return undefined', () => {
const notification = new OSNotification(baseNotificationData);
const result = notification.display();

expect(result).toBeUndefined();
});
});

describe('rawPayload types', () => {
test('should accept object as rawPayload', () => {
const notificationData = {
...baseNotificationData,
rawPayload: { key: 'value', nested: { key: 'value' } },
};
const notification = new OSNotification(notificationData);

expect(notification.rawPayload).toEqual({
key: 'value',
nested: { key: 'value' },
});
});

test('should accept string as rawPayload', () => {
const notificationData = {
...baseNotificationData,
rawPayload: '{"key":"value"}',
};
const notification = new OSNotification(notificationData);

expect(notification.rawPayload).toBe('{"key":"value"}');
});

test('should accept empty object as rawPayload', () => {
const notificationData = {
...baseNotificationData,
rawPayload: {},
};
const notification = new OSNotification(notificationData);

expect(notification.rawPayload).toEqual({});
});

test('should accept empty string as rawPayload', () => {
const notificationData = {
...baseNotificationData,
rawPayload: '',
};
const notification = new OSNotification(notificationData);

expect(notification.rawPayload).toBe('');
});
});
});
64 changes: 59 additions & 5 deletions src/OSNotification.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,49 @@
import { NativeModules, Platform } from 'react-native';
const RNOneSignal = NativeModules.OneSignal;

export interface BaseNotificationData {
body: string;
sound?: string;
title?: string;
launchURL?: string;
rawPayload: object | string; // platform bridges return different types
actionButtons?: object[];
additionalData?: object;
notificationId: string;
}

interface AndroidNotificationData extends BaseNotificationData {
groupKey?: string;
groupMessage?: string;
ledColor?: string;
priority?: number;
smallIcon?: string;
largeIcon?: string;
bigPicture?: string;
collapseId?: string;
fromProjectNumber?: string;
smallIconAccentColor?: string;
lockScreenVisibility?: string;
androidNotificationId?: number;
}

interface iOSNotificationData extends BaseNotificationData {
badge?: string;
badgeIncrement?: string;
category?: string;
threadId?: string;
subtitle?: string;
templateId?: string;
templateName?: string;
attachments?: object;
mutableContent?: boolean;
contentAvailable?: string;
relevanceScore?: number;
interruptionLevel?: string;
}

export type OSNotificationData = AndroidNotificationData | iOSNotificationData;

export default class OSNotification {
body: string;
sound?: string;
Expand Down Expand Up @@ -37,7 +80,7 @@ export default class OSNotification {
relevanceScore?: number;
interruptionLevel?: string;

constructor(receivedEvent: OSNotification) {
constructor(receivedEvent: OSNotificationData) {
this.body = receivedEvent.body;
this.sound = receivedEvent.sound;
this.title = receivedEvent.title;
Expand All @@ -47,7 +90,8 @@ export default class OSNotification {
this.additionalData = receivedEvent.additionalData;
this.notificationId = receivedEvent.notificationId;

if (Platform.OS === 'android') {
/* v8 ignore else -- @preserve */
if (isAndroidNotificationData(receivedEvent)) {
this.groupKey = receivedEvent.groupKey;
this.ledColor = receivedEvent.ledColor;
this.priority = receivedEvent.priority;
Expand All @@ -60,9 +104,7 @@ export default class OSNotification {
this.smallIconAccentColor = receivedEvent.smallIconAccentColor;
this.lockScreenVisibility = receivedEvent.lockScreenVisibility;
this.androidNotificationId = receivedEvent.androidNotificationId;
}

if (Platform.OS === 'ios') {
} else if (isiOSNotificationData(receivedEvent)) {
this.badge = receivedEvent.badge;
this.category = receivedEvent.category;
this.threadId = receivedEvent.threadId;
Expand All @@ -83,3 +125,15 @@ export default class OSNotification {
return;
}
}

const isAndroidNotificationData = (
_data: AndroidNotificationData | iOSNotificationData,
): _data is AndroidNotificationData => {
return Platform.OS === 'android';
};

const isiOSNotificationData = (
_data: AndroidNotificationData | iOSNotificationData,
): _data is iOSNotificationData => {
return Platform.OS === 'ios';
};
Loading
Loading