Skip to content

Commit 6cd0d55

Browse files
committed
👤 simplify user profile updates; refactor push notif registration
Before this change, we would make 2 calls to `profile/update` every time the UI is initialized: one for push notification registration and another to store device settings. We also had a lot of unecessary complexity in all these places (event listeners everywhere!) All we really need to do on UI init, iff onboarding is done: 1. call `profile/get` on the server to get the current user profile 2. perform push notif registration 3. get current device settings 4. based on the results of the above, determine what fields need to be updated and call `profile/update` on server if needed 1, 2, and 3 can be executed in any order/ in parallel (with Promise.all / Promise.allSettled). Added a file userProfile to handle this. The added benefit of this is that we have access to the value of the current profile. We can keep it as state in AppContext allowing it to be used anywhere in the UI (this can simplify how custom labels are retrieved + used; and it may be useful for users to know their "last synced ts", for example) When the profile is updated from the UI, we have to keep the the local state in sync so instead of directly calling commHelper updateUser directly, we expose `updateUserProfile` from AppContext In removing the complexity, I moved remoteNotifyHandler into pushNotifySettings Updated tests for all changes
1 parent e5d4c1e commit 6cd0d55

22 files changed

Lines changed: 543 additions & 680 deletions

www/__mocks__/cordovaMocks.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,14 @@ export const mockReminders = () => {
3232

3333
export const mockDevice = () => {
3434
window['device'] ||= {};
35+
window['device'].cordova ||= packageJson.dependencies['cordova-ios'];
36+
window['device'].model ||= 'iPhone 12';
3537
window['device'].platform ||= 'ios';
38+
window['device'].uuid ||= '123456';
3639
window['device'].version ||= '14.0.0';
40+
window['device'].manufacturer ||= 'Apple';
41+
window['device'].isVirtual ||= false;
42+
window['device'].serial ||= 'ABC1234567890';
3743
};
3844

3945
export const mockGetAppVersion = () => {
@@ -223,6 +229,7 @@ export const mockBEMDataCollection = () => {
223229
},
224230
};
225231
window['cordova'] ||= {};
232+
window['cordova'].plugins ||= {};
226233
window['cordova'].plugins.BEMDataCollection = mockBEMDataCollection;
227234
};
228235

www/__mocks__/pushNotificationMocks.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,38 @@
11
let notifSettings;
2-
let onList: any = {};
3-
let called = null;
2+
let listenerList: any = {};
3+
let finishedNotId = null;
44

55
export const mockPushNotification = () => {
66
window['PushNotification'] = {
77
init: (settings: Object) => {
88
notifSettings = settings;
9-
return {
9+
const push = {
1010
on: (event: string, callback: Function) => {
11-
onList[event] = callback;
11+
listenerList[event] = callback;
1212
},
1313
finish: (content: any, errorFcn: Function, notID: any) => {
14-
called = notID;
14+
finishedNotId = notID;
1515
},
1616
};
17+
setTimeout(() => {
18+
mockPushEvent('registration', {
19+
registrationId: 'foo123',
20+
registrationType: 'barABC',
21+
});
22+
}, 100);
23+
return push;
1724
},
1825
};
1926
};
2027

28+
export const getNotifSettings = () => notifSettings;
29+
export const getListenerList = () => listenerList;
30+
export const getFinishedNotId = () => finishedNotId;
31+
32+
export const mockPushEvent = (event: string, data: any) => listenerList[event]?.(data);
33+
2134
export function clearNotifMock() {
2235
notifSettings = {};
23-
onList = {};
24-
called = null;
36+
listenerList = {};
37+
finishedNotId = null;
2538
}
26-
export const getOnList = () => onList;
27-
export const getCalled = () => called;

www/__mocks__/setupJestEnv.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import {
1818
import { mockFileSystem } from './fileSystemMocks';
1919
import { mockPushNotification } from './pushNotificationMocks';
2020

21+
// init i18next so phone_lang is set correctly during tests
22+
import initializedI18next from '../js/i18nextInit';
23+
window['i18next'] = initializedI18next;
24+
2125
mockLogger();
2226
mockCordova();
2327
mockDevice();

www/__tests__/notifScheduler.test.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,13 @@ describe('updateScheduledNotifs', () => {
265265
callback(arg);
266266
});
267267
// call the function
268-
await updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise);
268+
await updateScheduledNotifs(
269+
reminderSchemes,
270+
isScheduling,
271+
setIsScheduling,
272+
scheduledPromise,
273+
updateUser,
274+
);
269275
const scheduledNotifs = await getScheduledNotifs(isScheduling, scheduledPromise);
270276

271277
expect(scheduledNotifs).toHaveLength(4);
@@ -295,7 +301,13 @@ describe('updateScheduledNotifs', () => {
295301
.spyOn(window['cordova'].plugins.notification.local, 'getScheduled')
296302
.mockImplementation((callback) => callback(mockNotifs));
297303
// call the function
298-
await updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise);
304+
await updateScheduledNotifs(
305+
reminderSchemes,
306+
isScheduling,
307+
setIsScheduling,
308+
scheduledPromise,
309+
updateUser,
310+
);
299311

300312
expect(logDebug).toHaveBeenCalledWith('Already scheduled, not scheduling again');
301313
});
@@ -314,7 +326,13 @@ describe('updateScheduledNotifs', () => {
314326
.spyOn(window['cordova'].plugins.notification.local, 'getScheduled')
315327
.mockImplementation((callback) => callback(mockNotifs));
316328
// call the function
317-
await updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise);
329+
await updateScheduledNotifs(
330+
reminderSchemes,
331+
isScheduling,
332+
setIsScheduling,
333+
scheduledPromise,
334+
updateUser,
335+
);
318336

319337
expect(logDebug).toHaveBeenCalledWith(
320338
'ERROR: Already scheduling notifications, not scheduling again',
@@ -329,7 +347,13 @@ describe('updateScheduledNotifs', () => {
329347
const setIsScheduling: Function = jest.fn((val: boolean) => (isScheduling = val));
330348
const scheduledPromise: Promise<any> = Promise.resolve();
331349
// call the function
332-
await updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise);
350+
await updateScheduledNotifs(
351+
reminderSchemes,
352+
isScheduling,
353+
setIsScheduling,
354+
scheduledPromise,
355+
updateUser,
356+
);
333357

334358
expect(logDebug).toHaveBeenCalledWith('Error: Reminder scheme not found');
335359
});
@@ -377,7 +401,13 @@ describe('getReminderPrefs', () => {
377401

378402
// call the function
379403
const { reminder_assignment, reminder_join_date, reminder_time_of_day } =
380-
await getReminderPrefs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise);
404+
await getReminderPrefs(
405+
reminderSchemes,
406+
isScheduling,
407+
setIsScheduling,
408+
scheduledPromise,
409+
updateUser,
410+
);
381411

382412
expect(logDebug).toHaveBeenCalledWith('User just joined, Initializing reminder prefs');
383413
expect(logDebug).toHaveBeenCalledWith('Added reminder prefs to client stats');
@@ -411,7 +441,13 @@ describe('getReminderPrefs', () => {
411441

412442
// call the function
413443
const { reminder_assignment, reminder_join_date, reminder_time_of_day } =
414-
await getReminderPrefs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise);
444+
await getReminderPrefs(
445+
reminderSchemes,
446+
isScheduling,
447+
setIsScheduling,
448+
scheduledPromise,
449+
updateUser,
450+
);
415451

416452
expect(reminder_assignment).toEqual(expectedResult.reminder_assignment);
417453
expect(reminder_join_date).toEqual(expectedResult.reminder_join_date);
@@ -459,6 +495,7 @@ describe('setReminderPrefs', () => {
459495
isScheduling,
460496
setIsScheduling,
461497
scheduledPromise,
498+
updateUser,
462499
).then(() => {
463500
// in the implementation in ProfileSettings.jsx,
464501
// refresNotificationSettings();
Lines changed: 55 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,66 @@
1-
import { DateTime } from 'luxon';
2-
import { EVENTS, publish } from '../js/customEventHandler';
3-
import { INTRO_DONE_KEY, readIntroDone } from '../js/onboarding/onboardingHelper';
4-
import { storageSet } from '../js/plugin/storage';
5-
import { initPushNotify } from '../js/splash/pushNotifySettings';
6-
import { clearNotifMock, getOnList, getCalled } from '../__mocks__/pushNotificationMocks';
7-
8-
global.fetch = (url: string) =>
9-
new Promise((rs, rj) => {
10-
setTimeout(() =>
11-
rs({
12-
json: () =>
13-
new Promise((rs, rj) => {
14-
let myJSON = {
15-
emSensorDataCollectionProtocol: {
16-
protocol_id: '2014-04-6267',
17-
approval_date: '2016-07-14',
18-
},
19-
};
20-
setTimeout(() => rs(myJSON), 100);
21-
}),
22-
}),
23-
);
24-
}) as any;
1+
import { initPushNotify, push } from '../js/splash/pushNotifySettings';
2+
import { clearNotifMock, getListenerList, mockPushEvent } from '../__mocks__/pushNotificationMocks';
3+
import { markConsented } from '../js/splash/startprefs';
4+
import { waitFor } from '@testing-library/react-native';
255

266
afterEach(() => {
277
clearNotifMock();
288
});
299

30-
it('intro done does nothing if not registered', () => {
31-
expect(getOnList()).toStrictEqual({});
32-
publish(EVENTS.INTRO_DONE_EVENT, 'test data');
33-
expect(getOnList()).toStrictEqual({});
34-
});
10+
describe('pushNotifySettings', () => {
11+
describe('initPushNotify', () => {
12+
it('does not set up listeners if consent not given', async () => {
13+
expect(getListenerList()).toStrictEqual({});
14+
await initPushNotify();
15+
expect(getListenerList()).toStrictEqual({});
16+
});
3517

36-
it('intro done initializes the push notifications', () => {
37-
expect(getOnList()).toStrictEqual({});
18+
it('sets up listeners if consent given', async () => {
19+
await markConsented();
20+
await initPushNotify();
21+
expect(getListenerList()).toStrictEqual(
22+
expect.objectContaining({
23+
notification: expect.any(Function),
24+
error: expect.any(Function),
25+
registration: expect.any(Function),
26+
}),
27+
);
28+
});
3829

39-
initPushNotify();
40-
publish(EVENTS.INTRO_DONE_EVENT, 'test data');
41-
expect(getOnList()).toStrictEqual(
42-
expect.objectContaining({
43-
notification: expect.any(Function),
44-
error: expect.any(Function),
45-
registration: expect.any(Function),
46-
}),
47-
);
48-
});
30+
it('handles visible notification', async () => {
31+
const InAppBrowserOpenSpy = jest.spyOn(window['cordova'].InAppBrowser, 'open');
32+
await initPushNotify();
33+
mockPushEvent('notification', {
34+
additionalData: {
35+
payload: { alert_type: 'website', spec: { url: 'https://foo.bar' } },
36+
},
37+
});
38+
expect(InAppBrowserOpenSpy).toHaveBeenCalledWith(
39+
'https://foo.bar',
40+
'_blank',
41+
'location=yes,clearcache=no,toolbar=yes,hideurlbar=yes',
42+
);
43+
});
4944

50-
it('cloud event does nothing if not registered', () => {
51-
expect(window['cordova'].platformId).toEqual('ios');
52-
publish(EVENTS.CLOUD_NOTIFICATION_EVENT, {
53-
additionalData: { 'content-available': 1, payload: { notId: 3 } },
54-
});
55-
expect(getCalled()).toBeNull();
56-
});
45+
it('handles silent notification', async () => {
46+
const BEMDataCollectionHandleSilentPushSpy = jest.spyOn(
47+
window['cordova'].plugins.BEMDataCollection,
48+
'handleSilentPush',
49+
);
50+
await initPushNotify();
51+
const pushFinishSpy = jest.spyOn(push, 'finish');
5752

58-
it('cloud event handles notification if registered', async () => {
59-
expect(window['cordova'].platformId).toEqual('ios');
60-
initPushNotify();
61-
publish(EVENTS.INTRO_DONE_EVENT, 'intro done');
62-
publish(EVENTS.CLOUD_NOTIFICATION_EVENT, {
63-
additionalData: { 'content-available': 1, payload: { notId: 3 } },
53+
const NOTIF_ID = 98765;
54+
mockPushEvent('notification', {
55+
additionalData: {
56+
'content-available': 1,
57+
payload: { notId: NOTIF_ID },
58+
},
59+
});
60+
await waitFor(() => {
61+
expect(pushFinishSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), NOTIF_ID);
62+
expect(BEMDataCollectionHandleSilentPushSpy).toHaveBeenCalled();
63+
});
64+
});
6465
});
65-
await new Promise((r) => setTimeout(r, 1000));
66-
expect(getCalled()).toEqual(3);
67-
});
68-
69-
it('consent event does nothing if not registered', () => {
70-
expect(getOnList()).toStrictEqual({});
71-
publish(EVENTS.CONSENTED_EVENT, 'test data');
72-
expect(getOnList()).toStrictEqual({});
73-
});
74-
75-
it('consent event registers if intro done', async () => {
76-
//make sure the mock is clear
77-
expect(getOnList()).toStrictEqual({});
78-
79-
//initialize the pushNotify, to subscribe to events
80-
initPushNotify();
81-
82-
//mark the intro as done
83-
const currDateTime = DateTime.now().toISO();
84-
let marked = await storageSet(INTRO_DONE_KEY, currDateTime);
85-
let introDone = await readIntroDone();
86-
expect(introDone).toBeTruthy();
87-
88-
//publish consent event and check results
89-
publish(EVENTS.CONSENTED_EVENT, 'test data');
90-
//have to wait a beat since event response is async
91-
await new Promise((r) => setTimeout(r, 1000));
92-
expect(getOnList()).toStrictEqual(
93-
expect.objectContaining({
94-
notification: expect.any(Function),
95-
error: expect.any(Function),
96-
registration: expect.any(Function),
97-
}),
98-
);
99-
});
100-
101-
it('consent event does not register if intro not done', () => {
102-
expect(getOnList()).toStrictEqual({});
103-
initPushNotify();
104-
publish(EVENTS.CONSENTED_EVENT, 'test data');
105-
expect(getOnList()).toStrictEqual({}); //nothing, intro not done
10666
});

0 commit comments

Comments
 (0)