-
Notifications
You must be signed in to change notification settings - Fork 104
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: [IOCOM-1740] Push Notification Data (#6211)
## Short description This PR: - adds the system notification receiving permission status to redux, not persisted - enables the direct opening of the system notification receiving permission screen, instead of the general app settings ## List of changes proposed in this pull request Permission status on redux: - dedicated reducer `pushNotifications/store/reducers/permissions.ts` - updated by dispatching `pushNotifications/store/actions/permissions.ts` - part of a larger reducer `pushNotifications/store/reducers/index.ts` - this last one is persisted and its data have been extracted and migrated from the root persistor, that is why there is a migration in `ts/boot/configureStoreAndPersistor.ts` - also, since this dedicated reducer has now a custom persistor, its persistence data must be retained after a logout/login from the same user, in `ts/store/reducers/index.ts` - the saga that handles the permission retrieval and action dispatching is `pushNotifications/sagas/notificationPermissionsListener.ts` and it is forked (so it is always running) by the startupSaga in `ts/sagas/startup.ts`. - the permission update is done only if it is different from the one saved in redux. Such comparison and update is handled by common saga functions in `pushNotifications /sagas/common.ts` System Permissions opening: - `react-native-notification-utils`: library to open the system notification permissions screen - `openSystemNotificationSettingsScreen` in `pushNotifications/utils/index.ts` is the utility function that wraps the call to the library - `OnboardingNotificationsInfoScreenConsent` has been changed in order to call the function above Push Token Upload: - there may have been ad edge case where the push notification token was becoming available after the function to upload it to the backend had run. In order to prevent it the following has been done - `pushNotificationTokenUpload` in `pushNotifications/sagas/pushNotificationTokenUpload.ts` is the saga that uploads the token. - it is forked (and not just called) by the startupSaga in `ts/sagas/startup.ts` - its first task is to wait for the token to be available. It does so by waiting for `awaitForPushNotificationToken` saga to return it. Please note that a token is never regenerated while the application is running - after that, the saga body has been refactored to increase readability Refactorings Actions, reducers and sagas have been split in order to differentiate between: - `installation`: handles the token memorization - `pendingMessage`: handles message id coming from a push notification - `permissions`: handles the system permission status Embedded analytics events have been moved to their own file and enriched when they were missing standard category and type. ## How to test Using the io-dev-api-server, make sure that: - the permission in redux is always up-to date - the push notification token is always sent to backend when it is available - push instructions' CTA in the onboarding flow opens the system screen (using the io-dev-api-server, remove any reference to `optInNotificationPreferences` in `profile.ts`, make sure to start the application from the beginning and deny the push notification permission when asked) --------- Co-authored-by: Martino Cesari Tomba <[email protected]>
- Loading branch information
Showing
50 changed files
with
1,561 additions
and
754 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
176 changes: 176 additions & 0 deletions
176
ts/features/pushNotifications/analytics/__tests__/index.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
import { | ||
trackNewPushNotificationsTokenGenerated, | ||
trackNotificationInstallationTokenNotChanged, | ||
trackNotificationsOptInOpenSettings, | ||
trackNotificationsOptInPreviewStatus, | ||
trackNotificationsOptInReminderOnPermissionsOff, | ||
trackNotificationsOptInReminderStatus, | ||
trackNotificationsOptInSkipSystemPermissions, | ||
trackPushNotificationTokenUploadFailure, | ||
trackPushNotificationTokenUploadSucceeded | ||
} from ".."; | ||
import { PushNotificationsContentTypeEnum } from "../../../../../definitions/backend/PushNotificationsContentType"; | ||
import { ReminderStatusEnum } from "../../../../../definitions/backend/ReminderStatus"; | ||
import * as Mixpanel from "../../../../mixpanel"; | ||
|
||
describe("pushNotifications analytics", () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
jest.resetAllMocks(); | ||
}); | ||
it("'trackNotificationInstallationTokenNotChanged' should have expected event name and properties", () => { | ||
const mockMixpanelTrack = getMockMixpanelTrack(); | ||
void trackNotificationInstallationTokenNotChanged(); | ||
expect(mockMixpanelTrack.mock.calls.length).toBe(1); | ||
expect(mockMixpanelTrack.mock.calls[0].length).toBe(2); | ||
expect(mockMixpanelTrack.mock.calls[0][0]).toBe( | ||
"NOTIFICATIONS_INSTALLATION_TOKEN_NOT_CHANGED" | ||
); | ||
expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({ | ||
event_category: "TECH" | ||
}); | ||
}); | ||
it("'trackNotificationsOptInPreviewStatus' should have expected event name and properties for 'ANONYMOUS' content type", () => { | ||
const mockMixpanelTrack = getMockMixpanelTrack(); | ||
void trackNotificationsOptInPreviewStatus( | ||
PushNotificationsContentTypeEnum.ANONYMOUS | ||
); | ||
expect(mockMixpanelTrack.mock.calls.length).toBe(1); | ||
expect(mockMixpanelTrack.mock.calls[0].length).toBe(2); | ||
expect(mockMixpanelTrack.mock.calls[0][0]).toBe( | ||
"NOTIFICATIONS_OPTIN_PREVIEW_STATUS" | ||
); | ||
expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({ | ||
event_category: "UX", | ||
event_type: "action", | ||
enabled: false | ||
}); | ||
}); | ||
it("'trackNotificationsOptInPreviewStatus' should have expected event name and properties for 'FULL' content type", () => { | ||
const mockMixpanelTrack = getMockMixpanelTrack(); | ||
void trackNotificationsOptInPreviewStatus( | ||
PushNotificationsContentTypeEnum.FULL | ||
); | ||
expect(mockMixpanelTrack.mock.calls.length).toBe(1); | ||
expect(mockMixpanelTrack.mock.calls[0].length).toBe(2); | ||
expect(mockMixpanelTrack.mock.calls[0][0]).toBe( | ||
"NOTIFICATIONS_OPTIN_PREVIEW_STATUS" | ||
); | ||
expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({ | ||
event_category: "UX", | ||
event_type: "action", | ||
enabled: true | ||
}); | ||
}); | ||
it("'trackNotificationsOptInReminderStatus' should have expected event name and properties for 'DISABLED' reminder", () => { | ||
const mockMixpanelTrack = getMockMixpanelTrack(); | ||
void trackNotificationsOptInReminderStatus(ReminderStatusEnum.DISABLED); | ||
expect(mockMixpanelTrack.mock.calls.length).toBe(1); | ||
expect(mockMixpanelTrack.mock.calls[0].length).toBe(2); | ||
expect(mockMixpanelTrack.mock.calls[0][0]).toBe( | ||
"NOTIFICATIONS_OPTIN_REMINDER_STATUS" | ||
); | ||
expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({ | ||
event_category: "UX", | ||
event_type: "action", | ||
enabled: false | ||
}); | ||
}); | ||
it("'trackNotificationsOptInReminderStatus' should have expected event name and properties for 'ENABLED' reminder", () => { | ||
const mockMixpanelTrack = getMockMixpanelTrack(); | ||
void trackNotificationsOptInReminderStatus(ReminderStatusEnum.ENABLED); | ||
expect(mockMixpanelTrack.mock.calls.length).toBe(1); | ||
expect(mockMixpanelTrack.mock.calls[0].length).toBe(2); | ||
expect(mockMixpanelTrack.mock.calls[0][0]).toBe( | ||
"NOTIFICATIONS_OPTIN_REMINDER_STATUS" | ||
); | ||
expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({ | ||
event_category: "UX", | ||
event_type: "action", | ||
enabled: true | ||
}); | ||
}); | ||
it("'trackNotificationsOptInReminderOnPermissionsOff' should have expected event name and properties", () => { | ||
const mockMixpanelTrack = getMockMixpanelTrack(); | ||
void trackNotificationsOptInReminderOnPermissionsOff(); | ||
expect(mockMixpanelTrack.mock.calls.length).toBe(1); | ||
expect(mockMixpanelTrack.mock.calls[0].length).toBe(2); | ||
expect(mockMixpanelTrack.mock.calls[0][0]).toBe( | ||
"NOTIFICATIONS_OPTIN_REMINDER_ON_PERMISSIONS_OFF" | ||
); | ||
expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({ | ||
event_category: "UX", | ||
event_type: "control" | ||
}); | ||
}); | ||
it("'trackNotificationsOptInOpenSettings' should have expected event name and properties", () => { | ||
const mockMixpanelTrack = getMockMixpanelTrack(); | ||
void trackNotificationsOptInOpenSettings(); | ||
expect(mockMixpanelTrack.mock.calls.length).toBe(1); | ||
expect(mockMixpanelTrack.mock.calls[0].length).toBe(2); | ||
expect(mockMixpanelTrack.mock.calls[0][0]).toBe( | ||
"NOTIFICATIONS_OPTIN_OPEN_SETTINGS" | ||
); | ||
expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({ | ||
event_category: "UX", | ||
event_type: "action" | ||
}); | ||
}); | ||
it("'trackNotificationsOptInSkipSystemPermissions' should have expected event name and properties", () => { | ||
const mockMixpanelTrack = getMockMixpanelTrack(); | ||
void trackNotificationsOptInSkipSystemPermissions(); | ||
expect(mockMixpanelTrack.mock.calls.length).toBe(1); | ||
expect(mockMixpanelTrack.mock.calls[0].length).toBe(2); | ||
expect(mockMixpanelTrack.mock.calls[0][0]).toBe( | ||
"NOTIFICATIONS_OPTIN_SKIP_SYSTEM_PERMISSIONS" | ||
); | ||
expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({ | ||
event_category: "UX", | ||
event_type: "action" | ||
}); | ||
}); | ||
it("'trackNewPushNotificationsTokenGenerated' should have expected event name and properties", () => { | ||
const mockMixpanelTrack = getMockMixpanelTrack(); | ||
void trackNewPushNotificationsTokenGenerated(); | ||
expect(mockMixpanelTrack.mock.calls.length).toBe(1); | ||
expect(mockMixpanelTrack.mock.calls[0].length).toBe(2); | ||
expect(mockMixpanelTrack.mock.calls[0][0]).toBe( | ||
"NOTIFICATIONS_INSTALLATION_TOKEN_UPDATE" | ||
); | ||
expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({ | ||
event_category: "TECH" | ||
}); | ||
}); | ||
it("'trackPushNotificationTokenUploadSucceeded' should have expected event name and properties", () => { | ||
const mockMixpanelTrack = getMockMixpanelTrack(); | ||
void trackPushNotificationTokenUploadSucceeded(); | ||
expect(mockMixpanelTrack.mock.calls.length).toBe(1); | ||
expect(mockMixpanelTrack.mock.calls[0].length).toBe(2); | ||
expect(mockMixpanelTrack.mock.calls[0][0]).toBe( | ||
"NOTIFICATIONS_INSTALLATION_TOKEN_REGISTERED" | ||
); | ||
expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({ | ||
event_category: "TECH" | ||
}); | ||
}); | ||
it("'trackPushNotificationTokenUploadFailure' should have expected event name and properties", () => { | ||
const mockMixpanelTrack = getMockMixpanelTrack(); | ||
const reason = "The reason"; | ||
void trackPushNotificationTokenUploadFailure(reason); | ||
expect(mockMixpanelTrack.mock.calls.length).toBe(1); | ||
expect(mockMixpanelTrack.mock.calls[0].length).toBe(2); | ||
expect(mockMixpanelTrack.mock.calls[0][0]).toBe( | ||
"NOTIFICATIONS_INSTALLATION_UPDATE_FAILURE" | ||
); | ||
expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({ | ||
event_category: "KO", | ||
event_type: "error", | ||
reason | ||
}); | ||
}); | ||
}); | ||
|
||
const getMockMixpanelTrack = () => | ||
jest | ||
.spyOn(Mixpanel, "mixpanelTrack") | ||
.mockImplementation((_event, _properties) => undefined); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.