Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d2cfd0f
wip: add basic notifications
sdisalvo-crd Oct 21, 2025
a8068eb
feat: adding cold start and performance optimization
sdisalvo-crd Oct 22, 2025
2878268
fix: cold start won't switch profile
sdisalvo-crd Oct 22, 2025
2696037
wip: fix persistent notifications bug
sdisalvo-crd Oct 23, 2025
e7a970f
test(jest): fix unit test
sdisalvo-crd Oct 23, 2025
a51c650
fix: replaced timeout-based workaround with event-driven state machin…
sdisalvo-crd Oct 24, 2025
da06610
fix: update package-lock
sdisalvo-crd Oct 24, 2025
48f2c9a
fix: mock a basic IonIcon
sdisalvo-crd Oct 24, 2025
275df0f
fix: updated the lock file to match the current state
sdisalvo-crd Oct 24, 2025
459ff5d
Merge remote-tracking branch 'origin/develop' into feat/VT20-2085-Ini…
sdisalvo-crd Oct 24, 2025
38742fd
wip: troubleshooting profile switch
sdisalvo-crd Oct 24, 2025
256590e
fix: duplicate notification bug
sdisalvo-crd Oct 28, 2025
9295b5d
Merge origin/develop into feat/VT20-2085-Initial-basic-push-notificat…
sdisalvo-crd Oct 28, 2025
476f630
chore: bring back react-hooks/exhaustive-deps deleted by mistake
sdisalvo-crd Oct 28, 2025
cd2bc4f
fix: review PR comments
sdisalvo-crd Oct 29, 2025
5cb8b67
fix: add NSUserNotificationsUsageDescription
sdisalvo-crd Oct 30, 2025
1da36e1
chore: service relocation
sdisalvo-crd Oct 30, 2025
cf93441
chore: remove MultiSigExn
sdisalvo-crd Oct 31, 2025
77377c6
wip: simplify & refactor
sdisalvo-crd Oct 31, 2025
dee9452
chore: cleanup
sdisalvo-crd Oct 31, 2025
9e2403f
fix: remove dynamic imports in handleNotificationReceived
sdisalvo-crd Nov 4, 2025
c83bc09
fix: Android cold start and failing tests
sdisalvo-crd Nov 4, 2025
6864278
Merge remote-tracking branch 'origin/develop' into feat/VT20-2085-Ini…
sdisalvo-crd Nov 5, 2025
102c8c0
feat: add CapacitorLocalNotifications.release.xcconfig
jimcase Nov 5, 2025
5ecadac
wip: fix PR comments
sdisalvo-crd Nov 5, 2025
bc7ccf2
fix: handleNotificationNavigation
sdisalvo-crd Nov 5, 2025
c4abf9f
wip: fix PR comments
sdisalvo-crd Nov 5, 2025
4d124f6
fix: navigate first and then clear in the background
sdisalvo-crd Nov 5, 2025
605ccc2
wip: fix PR comments
sdisalvo-crd Nov 5, 2025
8447fa5
fix: PR comments & clearDeliveredNotificationsForProfile
sdisalvo-crd Nov 6, 2025
fe11c02
fix: failing test
sdisalvo-crd Nov 6, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/CapacitorLocalNotifications
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Capacitor" "${PODS_CONFIGURATION_BUILD_DIR}/CapacitorCordova"
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
OTHER_LDFLAGS = $(inherited) -framework "Capacitor"
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/../../../node_modules/@capacitor/local-notifications
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
STRIP_DEBUG_SYMBOLS = YES
DEPLOYMENT_POSTPROCESSING = YES
STRIP_INSTALLED_PRODUCT = YES
162 changes: 34 additions & 128 deletions src/native/pushNotifications/notificationService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ jest.mock("@capacitor/core", () => ({
},
}));

jest.mock("../../ui/utils/error", () => ({
showError: jest.fn(),
}));

describe("NotificationService", () => {
beforeEach(() => {
jest.clearAllMocks();
(notificationService as any).permissionsGranted = true;
(notificationService as any).profileSwitcher = null;
(notificationService as any).navigator = null;
});

describe("requestPermissions", () => {
Expand Down Expand Up @@ -152,13 +155,19 @@ describe("NotificationService", () => {

describe("handleNotificationTap", () => {
let mockProfileSwitcher: jest.Mock;
let mockNavigator: jest.Mock;
let mockPushState: jest.SpyInstance;
let mockDispatchEvent: jest.SpyInstance;

beforeEach(() => {
mockProfileSwitcher = jest.fn();
mockNavigator = jest.fn();
mockPushState = jest.spyOn(window.history, "pushState");
mockDispatchEvent = jest.spyOn(window, "dispatchEvent");
(notificationService as any).profileSwitcher = mockProfileSwitcher;
(notificationService as any).navigator = mockNavigator;
});

afterEach(() => {
mockPushState.mockRestore();
mockDispatchEvent.mockRestore();
});

test("should switch profile and navigate on tap", async () => {
Expand All @@ -170,26 +179,15 @@ describe("NotificationService", () => {
},
};

(
LocalNotifications.getDeliveredNotifications as jest.Mock
).mockResolvedValue({
notifications: [
{ id: 1, extra: { profileId: "profile-abc" } },
{ id: 2, extra: { profileId: "profile-abc" } },
],
});
(LocalNotifications.cancel as jest.Mock).mockResolvedValue(undefined);

await (notificationService as any).handleNotificationTap(notification);

expect(mockProfileSwitcher).toHaveBeenCalledWith("profile-abc");
expect(LocalNotifications.cancel).toHaveBeenCalledWith({
notifications: [{ id: 1 }, { id: 2 }],
});
expect(mockNavigator).toHaveBeenCalledWith(
TabsRoutePath.NOTIFICATIONS,
"notif-123"
expect(mockPushState).toHaveBeenCalledWith(
null,
"",
TabsRoutePath.NOTIFICATIONS
);
expect(mockDispatchEvent).toHaveBeenCalled();
});

test("should queue notification if profileSwitcher not set", async () => {
Expand All @@ -208,12 +206,11 @@ describe("NotificationService", () => {
expect((notificationService as any).pendingNotification).toEqual(
notification
);
expect(mockNavigator).not.toHaveBeenCalled();
expect(mockPushState).not.toHaveBeenCalled();
expect(mockDispatchEvent).not.toHaveBeenCalled();
});

test("should queue notification if navigator not set", async () => {
(notificationService as any).navigator = null;

test("should queue notification if profileSwitcher not set", async () => {
const notification = {
id: 1,
extra: {
Expand All @@ -224,91 +221,12 @@ describe("NotificationService", () => {

await (notificationService as any).handleNotificationTap(notification);

expect((notificationService as any).pendingNotification).toEqual(
notification
);
expect(mockProfileSwitcher).not.toHaveBeenCalled();
});

test("should queue notification if profileId missing", async () => {
const notification = {
id: 1,
extra: {
notificationId: "notif-123",
},
};

await (notificationService as any).handleNotificationTap(notification);

expect((notificationService as any).pendingNotification).toEqual(
notification
);
});
});

describe("clearDeliveredNotificationsForProfile", () => {
test("should clear notifications for specific profile", async () => {
(
LocalNotifications.getDeliveredNotifications as jest.Mock
).mockResolvedValue({
notifications: [
{ id: 1, extra: { profileId: "profile-1" } },
{ id: 2, extra: { profileId: "profile-2" } },
{ id: 3, extra: { profileId: "profile-1" } },
],
});

await notificationService.clearDeliveredNotificationsForProfile(
"profile-1"
);

expect(LocalNotifications.cancel).toHaveBeenCalledWith({
notifications: [{ id: 1 }, { id: 3 }],
});
});

test("should not cancel if no notifications for profile", async () => {
(
LocalNotifications.getDeliveredNotifications as jest.Mock
).mockResolvedValue({
notifications: [{ id: 1, extra: { profileId: "profile-2" } }],
});

await notificationService.clearDeliveredNotificationsForProfile(
"profile-1"
);

expect(LocalNotifications.cancel).not.toHaveBeenCalled();
});
});

describe("cancelNotification", () => {
test("should cancel specific notification by ID", async () => {
await notificationService.cancelNotification("12345");

expect(LocalNotifications.cancel).toHaveBeenCalledWith({
notifications: [{ id: 12345 }],
});
});
});

describe("getActiveNotifications", () => {
test("should return pending notifications", async () => {
const mockNotifications = [
{ id: 1, extra: {} },
{ id: 2, extra: {} },
];
(LocalNotifications.getPending as jest.Mock).mockResolvedValue({
notifications: mockNotifications,
});

const result = await notificationService.getActiveNotifications();

expect(result).toEqual(mockNotifications);
expect(LocalNotifications.getPending).toHaveBeenCalled();
});
});

describe("getDeliveredNotifications", () => {
test("should return delivered notifications", async () => {
const mockNotifications = [
Expand Down Expand Up @@ -352,20 +270,14 @@ describe("NotificationService", () => {
});
});

describe("setProfileSwitcher and setNavigator", () => {
describe("setProfileSwitcher", () => {
test("should set profile switcher callback", () => {
const mockCallback = jest.fn();
notificationService.setProfileSwitcher(mockCallback);
expect((notificationService as any).profileSwitcher).toBe(mockCallback);
});

test("should set navigator callback", () => {
const mockCallback = jest.fn();
notificationService.setNavigator(mockCallback);
expect((notificationService as any).navigator).toBe(mockCallback);
});

test("should process pending notification when both handlers are set", async () => {
test("should process pending notification when profile switcher is set", async () => {
const notification = {
id: 1,
extra: {
Expand All @@ -375,37 +287,31 @@ describe("NotificationService", () => {
};

(notificationService as any).profileSwitcher = null;
(notificationService as any).navigator = null;
await (notificationService as any).handleNotificationTap(notification);

expect((notificationService as any).pendingNotification).toEqual(
notification
);

const mockProfileSwitcher = jest.fn();
const mockNavigator = jest.fn();
(
LocalNotifications.getDeliveredNotifications as jest.Mock
).mockResolvedValue({
notifications: [],
});
const mockPushState = jest.spyOn(window.history, "pushState");
const mockDispatchEvent = jest.spyOn(window, "dispatchEvent");

notificationService.setProfileSwitcher(mockProfileSwitcher);
expect((notificationService as any).pendingNotification).toEqual(
notification
);
expect(mockProfileSwitcher).not.toHaveBeenCalled();

notificationService.setNavigator(mockNavigator);

await new Promise((resolve) => setTimeout(resolve, 0));
await (notificationService as any).processPendingNotification();

expect((notificationService as any).pendingNotification).toBeNull();
expect(mockProfileSwitcher).toHaveBeenCalledWith("profile-abc");
expect(mockNavigator).toHaveBeenCalledWith(
TabsRoutePath.NOTIFICATIONS,
"notif-123"
expect(mockPushState).toHaveBeenCalledWith(
null,
"",
TabsRoutePath.NOTIFICATIONS
);
expect(mockDispatchEvent).toHaveBeenCalled();

mockPushState.mockRestore();
mockDispatchEvent.mockRestore();
});
});
});
Loading