From 2b5d385d340833ebd90908744d295229960e705d Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 25 Jan 2023 16:17:41 -0800 Subject: [PATCH 01/38] Install react-native-device-info --- ios/Podfile.lock | 6 ++++++ package-lock.json | 15 +++++++++++++++ package.json | 1 + 3 files changed, 22 insertions(+) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0637e1334ff3..dc45b4b7d650 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -575,6 +575,8 @@ PODS: - React-Core - RNDateTimePicker (3.5.2): - React-Core + - RNDeviceInfo (10.3.0): + - React-Core - RNFastImage (8.6.3): - React-Core - SDWebImage (~> 5.11.1) @@ -730,6 +732,7 @@ DEPENDENCIES: - "RNCClipboard (from `../node_modules/@react-native-community/clipboard`)" - "RNCPicker (from `../node_modules/@react-native-picker/picker`)" - "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)" + - RNDeviceInfo (from `../node_modules/react-native-device-info`) - RNFastImage (from `../node_modules/react-native-fast-image`) - "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)" - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" @@ -901,6 +904,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-picker/picker" RNDateTimePicker: :path: "../node_modules/@react-native-community/datetimepicker" + RNDeviceInfo: + :path: "../node_modules/react-native-device-info" RNFastImage: :path: "../node_modules/react-native-fast-image" RNFBAnalytics: @@ -1019,6 +1024,7 @@ SPEC CHECKSUMS: RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495 RNCPicker: 0b65be85fe7954fbb2062ef079e3d1cde252d888 RNDateTimePicker: 7658208086d86d09e1627b5c34ba0cf237c60140 + RNDeviceInfo: 4701f0bf2a06b34654745053db0ce4cb0c53ada7 RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 RNFBAnalytics: f76bfa164ac235b00505deb9fc1776634056898c RNFBApp: 729c0666395b1953198dc4a1ec6deb8fbe1c302e diff --git a/package-lock.json b/package-lock.json index e095176baa42..dd6b9c5b764e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,7 @@ "react-native-blob-util": "^0.16.2", "react-native-collapsible": "^1.6.0", "react-native-config": "^1.4.5", + "react-native-device-info": "^10.3.0", "react-native-document-picker": "^8.0.0", "react-native-fast-image": "^8.6.3", "react-native-gesture-handler": "2.6.0", @@ -35498,6 +35499,14 @@ } } }, + "node_modules/react-native-device-info": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.3.0.tgz", + "integrity": "sha512-/ziZN1sA1REbJTv5mQZ4tXggcTvSbct+u5kCaze8BmN//lbxcTvWsU6NQd4IihLt89VkbX+14IGc9sVApSxd/w==", + "peerDependencies": { + "react-native": "*" + } + }, "node_modules/react-native-document-picker": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/react-native-document-picker/-/react-native-document-picker-8.1.1.tgz", @@ -69955,6 +69964,12 @@ "integrity": "sha512-cSLdOfva2IPCxh6HjHN1IDVW9ratAvNnnAUx6ar2Byvr8KQU7++ysdFYPaoNVuJURuYoAKgvjab8ZcnwGZIO6Q==", "requires": {} }, + "react-native-device-info": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.3.0.tgz", + "integrity": "sha512-/ziZN1sA1REbJTv5mQZ4tXggcTvSbct+u5kCaze8BmN//lbxcTvWsU6NQd4IihLt89VkbX+14IGc9sVApSxd/w==", + "requires": {} + }, "react-native-document-picker": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/react-native-document-picker/-/react-native-document-picker-8.1.1.tgz", diff --git a/package.json b/package.json index 162bd7edfc03..bfcb0c59dcff 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "react-native-blob-util": "^0.16.2", "react-native-collapsible": "^1.6.0", "react-native-config": "^1.4.5", + "react-native-device-info": "^10.3.0", "react-native-document-picker": "^8.0.0", "react-native-fast-image": "^8.6.3", "react-native-gesture-handler": "2.6.0", From b6e2b21c1a08abb5b3028313f29dde2b52df86e7 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 25 Jan 2023 16:20:35 -0800 Subject: [PATCH 02/38] Upgrade urbanairship-react-native --- ios/Podfile.lock | 30 +++++++++++++++--------------- package-lock.json | 14 +++++++------- package.json | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index dc45b4b7d650..70733341159e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,18 +1,18 @@ PODS: - - Airship (16.8.0): - - Airship/Automation (= 16.8.0) - - Airship/Basement (= 16.8.0) - - Airship/Core (= 16.8.0) - - Airship/ExtendedActions (= 16.8.0) - - Airship/MessageCenter (= 16.8.0) - - Airship/Automation (16.8.0): + - Airship (16.10.7): + - Airship/Automation (= 16.10.7) + - Airship/Basement (= 16.10.7) + - Airship/Core (= 16.10.7) + - Airship/ExtendedActions (= 16.10.7) + - Airship/MessageCenter (= 16.10.7) + - Airship/Automation (16.10.7): - Airship/Core - - Airship/Basement (16.8.0) - - Airship/Core (16.8.0): + - Airship/Basement (16.10.7) + - Airship/Core (16.10.7): - Airship/Basement - - Airship/ExtendedActions (16.8.0): + - Airship/ExtendedActions (16.10.7): - Airship/Core - - Airship/MessageCenter (16.8.0): + - Airship/MessageCenter (16.10.7): - Airship/Core - boost (1.76.0) - CocoaAsyncSocket (7.6.5) @@ -641,8 +641,8 @@ PODS: - libwebp (~> 1.0) - SDWebImage/Core (~> 5.10) - SocketRocket (0.6.0) - - urbanairship-react-native (14.4.1): - - Airship (= 16.8.0) + - urbanairship-react-native (14.6.1): + - Airship (= 16.10.7) - React-Core - Yoga (1.14.0) - YogaKit (1.18.1): @@ -934,7 +934,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - Airship: 4657c3d5118441240e04674d9445cbd6e363c956 + Airship: fbff646723323c58e3871cd30488612ca373f597 boost: a7c83b31436843459a1961bfd74b96033dc77234 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 @@ -1039,7 +1039,7 @@ SPEC CHECKSUMS: SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 - urbanairship-react-native: 7e2e9a84c541b1d04798e51f7f390a2d5806eac0 + urbanairship-react-native: fe4d169332546a0efd348a009aa490dc36ff815e Yoga: f77f6497bccebdcbc8efb03dbf83eadfdec6d104 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a diff --git a/package-lock.json b/package-lock.json index dd6b9c5b764e..f2f82c1340bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,7 +90,7 @@ "semver": "^7.3.8", "shim-keyboard-event-key": "^1.0.3", "underscore": "^1.13.1", - "urbanairship-react-native": "^14.3.1" + "urbanairship-react-native": "^14.6.1" }, "devDependencies": { "@actions/core": "1.10.0", @@ -40994,9 +40994,9 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/urbanairship-react-native": { - "version": "14.4.1", - "resolved": "https://registry.npmjs.org/urbanairship-react-native/-/urbanairship-react-native-14.4.1.tgz", - "integrity": "sha512-7CFEszUR5DZdmx4YfipiDzbKEF72cBCaEh3gZHjwtGY7gsctKBx057+V5b5988vM+UghHbiCGE5fHgWs2fQMUQ==", + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/urbanairship-react-native/-/urbanairship-react-native-14.6.1.tgz", + "integrity": "sha512-ddL3ZWZnhwCja9oMpq7YHEyuqca1IH34MtMm24w1SePzGRhcVAvKOe/lncIB1FAK6QyjG0pkPT5mu3vE/DsPEw==", "peerDependencies": { "react": "*", "react-native": "*" @@ -74114,9 +74114,9 @@ } }, "urbanairship-react-native": { - "version": "14.4.1", - "resolved": "https://registry.npmjs.org/urbanairship-react-native/-/urbanairship-react-native-14.4.1.tgz", - "integrity": "sha512-7CFEszUR5DZdmx4YfipiDzbKEF72cBCaEh3gZHjwtGY7gsctKBx057+V5b5988vM+UghHbiCGE5fHgWs2fQMUQ==", + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/urbanairship-react-native/-/urbanairship-react-native-14.6.1.tgz", + "integrity": "sha512-ddL3ZWZnhwCja9oMpq7YHEyuqca1IH34MtMm24w1SePzGRhcVAvKOe/lncIB1FAK6QyjG0pkPT5mu3vE/DsPEw==", "requires": {} }, "uri-js": { diff --git a/package.json b/package.json index bfcb0c59dcff..356a248d495d 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "semver": "^7.3.8", "shim-keyboard-event-key": "^1.0.3", "underscore": "^1.13.1", - "urbanairship-react-native": "^14.3.1" + "urbanairship-react-native": "^14.6.1" }, "devDependencies": { "@actions/core": "1.10.0", From 4fc6f2632da301c5e13c6f64dda3be3a2382f512 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 25 Jan 2023 16:23:02 -0800 Subject: [PATCH 03/38] Add mock for react-native-device-info --- __mocks__/react-native-device-info.js | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 __mocks__/react-native-device-info.js diff --git a/__mocks__/react-native-device-info.js b/__mocks__/react-native-device-info.js new file mode 100644 index 000000000000..2ba5b6ef85b3 --- /dev/null +++ b/__mocks__/react-native-device-info.js @@ -0,0 +1,3 @@ +import MockDeviceInfo from 'react-native-device-info/jest/react-native-device-info-mock'; + +export default MockDeviceInfo; From efc4490cb5f948a66a1c14d94fd96e2cf1c16e79 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 25 Jan 2023 16:24:22 -0800 Subject: [PATCH 04/38] Update urbanairship-react-native mock --- __mocks__/urbanairship-react-native.js | 1 + 1 file changed, 1 insertion(+) diff --git a/__mocks__/urbanairship-react-native.js b/__mocks__/urbanairship-react-native.js index ba380b6d4a72..8eb5e9c14c08 100644 --- a/__mocks__/urbanairship-react-native.js +++ b/__mocks__/urbanairship-react-native.js @@ -21,6 +21,7 @@ const UrbanAirship = { removeAllListeners: jest.fn(), setBadgeNumber: jest.fn(), setForegroundPresentationOptions: jest.fn(), + getNotificationStatus: jest.fn(), }; export default UrbanAirship; From 599be59ed9dc0c61cace23ab0bd1ae78fcafa041 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 25 Jan 2023 16:25:43 -0800 Subject: [PATCH 05/38] Update CustomNotificationProvider --- .../chat/customairshipextender/CustomNotificationProvider.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java index 9b8f64d51a53..9e4cc0df8f43 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java @@ -45,6 +45,8 @@ public class CustomNotificationProvider extends ReactNotificationProvider { + private final Context context; + // Resize icons to 100 dp x 100 dp private static final int MAX_ICON_SIZE_DPS = 100; @@ -71,6 +73,7 @@ public class CustomNotificationProvider extends ReactNotificationProvider { public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConfigOptions configOptions) { super(context, configOptions); + this.context = context; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createAndRegisterNotificationChannel(context); } From cce0558d9ccc7f576b41bf5658b49bfe598202db Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 25 Jan 2023 16:35:34 -0800 Subject: [PATCH 06/38] Make PushNotification lib have same exports on web and native --- src/libs/Notification/PushNotification/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/Notification/PushNotification/index.js b/src/libs/Notification/PushNotification/index.js index c4100583442d..88136ff5dc72 100644 --- a/src/libs/Notification/PushNotification/index.js +++ b/src/libs/Notification/PushNotification/index.js @@ -2,6 +2,7 @@ import NotificationType from './NotificationType'; // Push notifications are only supported on mobile, so we'll just noop here export default { + init: () => {}, register: () => {}, deregister: () => {}, onReceived: () => {}, From 51116a7c4ae20f6d2b109f73d51cd9e5d3d3be65 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 25 Jan 2023 16:52:48 -0800 Subject: [PATCH 07/38] Create User actions to opt in and out of push notifications --- src/CONST.js | 1 + src/ONYXKEYS.js | 3 +++ src/libs/actions/User.js | 50 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/CONST.js b/src/CONST.js index 2d82c601f3cd..4a671996f2df 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -435,6 +435,7 @@ const CONST = { KYC_MIGRATION: 'expensify_migration_2020_04_28_RunKycVerifications', PREFERRED_EMOJI_SKIN_TONE: 'expensify_preferredEmojiSkinTone', FREQUENTLY_USED_EMOJIS: 'expensify_frequentlyUsedEmojis', + PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', }, DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, DEFAULT_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 8caa4b2997c4..0b80a856bebf 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -76,6 +76,9 @@ export default { // Contains the users's block expiration (if they have one) NVP_BLOCKED_FROM_CONCIERGE: 'private_blockedFromConcierge', + // Does this user have push notifications enabled? + NVP_PUSH_NOTIFICATIONS_ENABLED: 'nvp_pushNotificationsEnabled', + // Plaid data (access tokens, bank accounts ...) PLAID_DATA: 'plaidData', diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 09e7d6873084..1c4b1034149f 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -2,6 +2,7 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; import moment from 'moment'; +import DeviceInfo from 'react-native-device-info'; import ONYXKEYS from '../../ONYXKEYS'; import * as DeprecatedAPI from '../deprecatedAPI'; import * as API from '../API'; @@ -28,6 +29,12 @@ Onyx.connect({ }, }); +let isUserOptedInToPushNotifications = false; +Onyx.connect({ + key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, + callback: (val) => isUserOptedInToPushNotifications = val, +}); + /** * Changes a password for a given account * @@ -467,6 +474,47 @@ function generateStatementPDF(period) { }); } +/** + * A private helper function for optInToPushNotifications and optOutOfPushNotifications. + * + * @param {Boolean} isOptingIn + */ +function setPushNotificationOptInStatus(isOptingIn) { + const deviceID = DeviceInfo.getDeviceId(); + const commandName = isOptingIn ? 'OptInToPushNotifications' ? 'OptOutOfPushNotifications'; + const optimisticData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, + value: {[deviceID]: isOptingIn}, + }, + ]; + const failureData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, + value: {[deviceID]: isUserOptedInToPushNotifications}, + }, + ]; + API.write(commandName, {deviceID}, {optimisticData, failureData}); +} + +/** + * Record that user opted-in to push notifications on the current device. + * NOTE: This is purely for record-keeping purposes, and does not affect whether our server will attempt to send notifications to this user. + */ +function optInToPushNotifications() { + setPushNotificationOptInStatus(true); +} + +/** + * Record that user opted-out from push notifications on the current device. + * NOTE: This is purely for record-keeping purposes, and does not affect whether our server will attempt to send notifications to this user. + */ +function optOutOfPushNotifications() { + setPushNotificationOptInStatus(false); +} + export { updatePassword, closeAccount, @@ -487,4 +535,6 @@ export { deletePaypalMeAddress, addPaypalMeAddress, updateChatPriorityMode, + optInToPushNotifications, + optOutOfPushNotifications, }; From e158dbd7a4cc8bb266cef98c95e6fddc47cd5204 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 25 Jan 2023 16:53:14 -0800 Subject: [PATCH 08/38] Fix ternary --- src/libs/actions/User.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 1c4b1034149f..15acf375a847 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -481,7 +481,7 @@ function generateStatementPDF(period) { */ function setPushNotificationOptInStatus(isOptingIn) { const deviceID = DeviceInfo.getDeviceId(); - const commandName = isOptingIn ? 'OptInToPushNotifications' ? 'OptOutOfPushNotifications'; + const commandName = isOptingIn ? 'OptInToPushNotifications' : 'OptOutOfPushNotifications'; const optimisticData = [ { onyxMethod: CONST.ONYX.METHOD.MERGE, From 6ddd67cb9531cf49eb13879dc566e7fb49c59609 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 25 Jan 2023 16:56:26 -0800 Subject: [PATCH 09/38] Use only one user action instead of two --- src/libs/actions/User.js | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 15acf375a847..00118d0f957a 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -475,7 +475,8 @@ function generateStatementPDF(period) { } /** - * A private helper function for optInToPushNotifications and optOutOfPushNotifications. + * Record that user opted-in or opted-out of push notifications on the current device. + * NOTE: This is purely for record-keeping purposes, and does not affect whether our server will attempt to send notifications to this user. * * @param {Boolean} isOptingIn */ @@ -499,22 +500,6 @@ function setPushNotificationOptInStatus(isOptingIn) { API.write(commandName, {deviceID}, {optimisticData, failureData}); } -/** - * Record that user opted-in to push notifications on the current device. - * NOTE: This is purely for record-keeping purposes, and does not affect whether our server will attempt to send notifications to this user. - */ -function optInToPushNotifications() { - setPushNotificationOptInStatus(true); -} - -/** - * Record that user opted-out from push notifications on the current device. - * NOTE: This is purely for record-keeping purposes, and does not affect whether our server will attempt to send notifications to this user. - */ -function optOutOfPushNotifications() { - setPushNotificationOptInStatus(false); -} - export { updatePassword, closeAccount, @@ -535,6 +520,5 @@ export { deletePaypalMeAddress, addPaypalMeAddress, updateChatPriorityMode, - optInToPushNotifications, - optOutOfPushNotifications, + setPushNotificationOptInStatus, }; From 735e5a9e036dc79973ba35afd9f041a4a9f39609 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 25 Jan 2023 16:58:21 -0800 Subject: [PATCH 10/38] Hook up listeners in the PushNotification library to track push notification opt-in status --- .../PushNotification/index.native.js | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js index b6787858191d..b8c4d0627a8c 100644 --- a/src/libs/Notification/PushNotification/index.native.js +++ b/src/libs/Notification/PushNotification/index.native.js @@ -2,8 +2,17 @@ import _ from 'underscore'; import {AppState} from 'react-native'; import {UrbanAirship, EventType, iOS} from 'urbanairship-react-native'; import lodashGet from 'lodash/get'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../../ONYXKEYS'; import Log from '../../Log'; import NotificationType from './NotificationType'; +import * as User from '../../actions/User'; + +let isUserOptedInToPushNotifications = null; +Onyx.connect({ + key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, + callback: val => isUserOptedInToPushNotifications = val, +}); const notificationEventActionMap = {}; @@ -45,6 +54,22 @@ function pushNotificationEventCallback(eventType, notification) { action(payload); } +/** + * Check if a user is opted-in to push notifications and update the `pushNotificationsEnabled` NVP accordingly. + */ +function refreshNotificationOptInStatus() { + UrbanAirship.getNotificationStatus() + .then((notificationStatus) => { + const isOptedIn = notificationStatus.airshipOptIn && notificationStatus.systemEnabled; + if (isOptedIn === isUserOptedInToPushNotifications) { + return; + } + + Log.info('[PUSH_NOTIFICATION] Push notification opt-in status changed.', false, {isOptedIn}); + User.setPushNotificationOptInStatus(isOptedIn); + }); +} + /** * Register push notification callbacks. This is separate from namedUser registration because it needs to be executed * from a headless JS process, outside of any react lifecycle. @@ -55,6 +80,11 @@ function pushNotificationEventCallback(eventType, notification) { function init() { // Setup event listeners UrbanAirship.addListener(EventType.PushReceived, (notification) => { + // By default, refresh notification opt-in status to true if we receive a notification + if (!isUserOptedInToPushNotifications) { + User.setPushNotificationOptInStatus(true); + } + // If a push notification is received while the app is in foreground, // we'll assume pusher is connected so we'll ignore it and not fetch the same data twice. if (AppState.currentState === 'active') { @@ -71,6 +101,9 @@ function init() { pushNotificationEventCallback(EventType.NotificationResponse, event.notification); }); + // Keep track of which users have enabled push notifications via an NVP. + UrbanAirship.addListener(EventType.NotificationOptInStatus, refreshNotificationOptInStatus); + // This statement has effect on iOS only. // It enables the App to display push notifications when the App is in foreground. // By default, the push notifications are silenced on iOS if the App is in foreground. @@ -107,6 +140,9 @@ function register(accountID) { // Regardless of the user's opt-in status, we still want to receive silent push notifications. Log.info(`[PUSH_NOTIFICATIONS] Subscribing to notifications for account ID ${accountID}`); UrbanAirship.setNamedUser(accountID.toString()); + + // Refresh notification opt-in status NVP for the new user. + refreshNotificationOptInStatus(); } /** From 7e0a54a509f5ed5dbc7e047fa85bc7e2c6e77bea Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 25 Jan 2023 17:06:54 -0800 Subject: [PATCH 11/38] Fix JS style --- src/libs/actions/User.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 00118d0f957a..ffe8cb6ffec5 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -32,7 +32,7 @@ Onyx.connect({ let isUserOptedInToPushNotifications = false; Onyx.connect({ key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, - callback: (val) => isUserOptedInToPushNotifications = val, + callback: val => isUserOptedInToPushNotifications = val, }); /** From fa2fe5e7c7decd755582764138056154f439b04e Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 25 Jan 2023 17:34:37 -0800 Subject: [PATCH 12/38] Fix Onyx.connect callback to handle updated NVP shape --- src/libs/Notification/PushNotification/index.native.js | 10 ++++++++-- src/libs/actions/User.js | 8 +++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js index b8c4d0627a8c..af880f262ad7 100644 --- a/src/libs/Notification/PushNotification/index.native.js +++ b/src/libs/Notification/PushNotification/index.native.js @@ -8,10 +8,16 @@ import Log from '../../Log'; import NotificationType from './NotificationType'; import * as User from '../../actions/User'; -let isUserOptedInToPushNotifications = null; +let isUserOptedInToPushNotifications = false; Onyx.connect({ key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, - callback: val => isUserOptedInToPushNotifications = val, + callback: (val) => { + const mostRecentNVPValue = _.last(val); + if (!_.has(mostRecentNVPValue, 'isEnabled')) { + return; + } + isUserOptedInToPushNotifications = mostRecentNVPValue.isEnabled; + }, }); const notificationEventActionMap = {}; diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index ffe8cb6ffec5..0e7e477ec708 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -32,7 +32,13 @@ Onyx.connect({ let isUserOptedInToPushNotifications = false; Onyx.connect({ key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, - callback: val => isUserOptedInToPushNotifications = val, + callback: (val) => { + const mostRecentNVPValue = _.last(val); + if (!_.has(mostRecentNVPValue, 'isEnabled')) { + return; + } + isUserOptedInToPushNotifications = mostRecentNVPValue.isEnabled; + }, }); /** From 8941f1121045defd9f466086c29568bee9f7f2c9 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 14:07:42 -0800 Subject: [PATCH 13/38] Fix Onyx.connect to be keyed by deviceID --- src/libs/Notification/PushNotification/index.native.js | 6 +++++- src/libs/actions/User.js | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js index af880f262ad7..9368f0bbdd98 100644 --- a/src/libs/Notification/PushNotification/index.native.js +++ b/src/libs/Notification/PushNotification/index.native.js @@ -3,16 +3,20 @@ import {AppState} from 'react-native'; import {UrbanAirship, EventType, iOS} from 'urbanairship-react-native'; import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; +import DeviceInfo from 'react-native-device-info'; import ONYXKEYS from '../../../ONYXKEYS'; import Log from '../../Log'; import NotificationType from './NotificationType'; import * as User from '../../actions/User'; +const deviceID = DeviceInfo.getDeviceId(); + let isUserOptedInToPushNotifications = false; Onyx.connect({ key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, callback: (val) => { - const mostRecentNVPValue = _.last(val); + const pushNotificationOptInRecord = lodashGet(val, deviceID, []); + const mostRecentNVPValue = _.last(pushNotificationOptInRecord); if (!_.has(mostRecentNVPValue, 'isEnabled')) { return; } diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 0e7e477ec708..9be1b9cdaef7 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -21,6 +21,8 @@ import PusherUtils from '../PusherUtils'; import * as Report from './Report'; import * as ReportActionsUtils from '../ReportActionsUtils'; +const deviceID = DeviceInfo.getDeviceId(); + let currentUserAccountID = ''; Onyx.connect({ key: ONYXKEYS.SESSION, @@ -33,7 +35,8 @@ let isUserOptedInToPushNotifications = false; Onyx.connect({ key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, callback: (val) => { - const mostRecentNVPValue = _.last(val); + const pushNotificationOptInRecord = lodashGet(val, deviceID, []); + const mostRecentNVPValue = _.last(pushNotificationOptInRecord); if (!_.has(mostRecentNVPValue, 'isEnabled')) { return; } @@ -487,7 +490,6 @@ function generateStatementPDF(period) { * @param {Boolean} isOptingIn */ function setPushNotificationOptInStatus(isOptingIn) { - const deviceID = DeviceInfo.getDeviceId(); const commandName = isOptingIn ? 'OptInToPushNotifications' : 'OptOutOfPushNotifications'; const optimisticData = [ { From 01048e30ec00d8fd324f19f67a4f7d92d7e1e114 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 14:11:39 -0800 Subject: [PATCH 14/38] Consolidate Onyx connection into PushNotification lib --- .../PushNotification/index.native.js | 8 ++++++++ src/libs/actions/User.js | 16 ++-------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js index 9368f0bbdd98..4a711912a9a0 100644 --- a/src/libs/Notification/PushNotification/index.native.js +++ b/src/libs/Notification/PushNotification/index.native.js @@ -24,6 +24,13 @@ Onyx.connect({ }, }); +/** + * @returns {Boolean} + */ +function isUserOptedIn() { + return isUserOptedInToPushNotifications; +} + const notificationEventActionMap = {}; /** @@ -213,6 +220,7 @@ function clearNotifications() { } export default { + isUserOptedIn, init, register, deregister, diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 9be1b9cdaef7..4309f30931f4 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -20,6 +20,7 @@ import * as SequentialQueue from '../Network/SequentialQueue'; import PusherUtils from '../PusherUtils'; import * as Report from './Report'; import * as ReportActionsUtils from '../ReportActionsUtils'; +import PushNotification from '../Notification/PushNotification'; const deviceID = DeviceInfo.getDeviceId(); @@ -31,19 +32,6 @@ Onyx.connect({ }, }); -let isUserOptedInToPushNotifications = false; -Onyx.connect({ - key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, - callback: (val) => { - const pushNotificationOptInRecord = lodashGet(val, deviceID, []); - const mostRecentNVPValue = _.last(pushNotificationOptInRecord); - if (!_.has(mostRecentNVPValue, 'isEnabled')) { - return; - } - isUserOptedInToPushNotifications = mostRecentNVPValue.isEnabled; - }, -}); - /** * Changes a password for a given account * @@ -502,7 +490,7 @@ function setPushNotificationOptInStatus(isOptingIn) { { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, - value: {[deviceID]: isUserOptedInToPushNotifications}, + value: {[deviceID]: PushNotification.isUserOptedIn()}, }, ]; API.write(commandName, {deviceID}, {optimisticData, failureData}); From 6f89dc0493a0b642235ce64faa2dcc9b848db4f8 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 15:04:57 -0800 Subject: [PATCH 15/38] Fix require cycle --- .../PushNotification/index.native.js | 30 ++----------------- src/libs/actions/User.js | 22 ++++++++++++++ 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js index 4a711912a9a0..aab1da8beeab 100644 --- a/src/libs/Notification/PushNotification/index.native.js +++ b/src/libs/Notification/PushNotification/index.native.js @@ -2,35 +2,10 @@ import _ from 'underscore'; import {AppState} from 'react-native'; import {UrbanAirship, EventType, iOS} from 'urbanairship-react-native'; import lodashGet from 'lodash/get'; -import Onyx from 'react-native-onyx'; -import DeviceInfo from 'react-native-device-info'; -import ONYXKEYS from '../../../ONYXKEYS'; import Log from '../../Log'; import NotificationType from './NotificationType'; import * as User from '../../actions/User'; -const deviceID = DeviceInfo.getDeviceId(); - -let isUserOptedInToPushNotifications = false; -Onyx.connect({ - key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, - callback: (val) => { - const pushNotificationOptInRecord = lodashGet(val, deviceID, []); - const mostRecentNVPValue = _.last(pushNotificationOptInRecord); - if (!_.has(mostRecentNVPValue, 'isEnabled')) { - return; - } - isUserOptedInToPushNotifications = mostRecentNVPValue.isEnabled; - }, -}); - -/** - * @returns {Boolean} - */ -function isUserOptedIn() { - return isUserOptedInToPushNotifications; -} - const notificationEventActionMap = {}; /** @@ -78,7 +53,7 @@ function refreshNotificationOptInStatus() { UrbanAirship.getNotificationStatus() .then((notificationStatus) => { const isOptedIn = notificationStatus.airshipOptIn && notificationStatus.systemEnabled; - if (isOptedIn === isUserOptedInToPushNotifications) { + if (isOptedIn === User.isUserOptedIntoPushNotifications()) { return; } @@ -98,7 +73,7 @@ function init() { // Setup event listeners UrbanAirship.addListener(EventType.PushReceived, (notification) => { // By default, refresh notification opt-in status to true if we receive a notification - if (!isUserOptedInToPushNotifications) { + if (!User.isUserOptedIntoPushNotifications()) { User.setPushNotificationOptInStatus(true); } @@ -220,7 +195,6 @@ function clearNotifications() { } export default { - isUserOptedIn, init, register, deregister, diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 4309f30931f4..ac3d6d60da14 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -32,6 +32,27 @@ Onyx.connect({ }, }); +let isUserOptedInToPushNotifications = false; +Onyx.connect({ + key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, + callback: (val) => { + const pushNotificationOptInRecord = lodashGet(val, deviceID, []); + const mostRecentNVPValue = _.last(pushNotificationOptInRecord); + if (!_.has(mostRecentNVPValue, 'isEnabled')) { + return; + } + isUserOptedInToPushNotifications = mostRecentNVPValue.isEnabled; + }, +}); + +/** + * @returns {Boolean} + */ +function isUserOptedIntoPushNotifications() { + return isUserOptedInToPushNotifications; +} + + /** * Changes a password for a given account * @@ -517,4 +538,5 @@ export { addPaypalMeAddress, updateChatPriorityMode, setPushNotificationOptInStatus, + isUserOptedIntoPushNotifications, }; From 29f15e0383ca75757fc38952902d3464d543550b Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 15:43:07 -0800 Subject: [PATCH 16/38] Use Google Play-compliant deviceID alternative --- src/ONYXKEYS.js | 3 +++ src/libs/actions/App.js | 23 +++++++++++++++++++++ src/libs/actions/SignInRedirect.js | 1 + src/libs/getDeviceID/index.ios.js | 15 ++++++++++++++ src/libs/getDeviceID/index.js | 32 ++++++++++++++++++++++++++++++ src/setup/index.js | 3 +++ 6 files changed, 77 insertions(+) create mode 100644 src/libs/getDeviceID/index.ios.js create mode 100644 src/libs/getDeviceID/index.js diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 0b80a856bebf..99c2fede9fa2 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -15,6 +15,9 @@ export default { // which tab is the leader, and which ones are the followers ACTIVE_CLIENTS: 'activeClients', + // A unique ID for the device + DEVICE_ID: 'deviceID', + // Boolean flag set whenever the sidebar has loaded IS_SIDEBAR_LOADED: 'isSidebarLoaded', diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js index 874b45f1fc57..ace9a979568c 100644 --- a/src/libs/actions/App.js +++ b/src/libs/actions/App.js @@ -16,6 +16,7 @@ import ROUTES from '../../ROUTES'; import * as SessionUtils from '../SessionUtils'; import getCurrentUrl from '../Navigation/currentUrl'; import * as Session from './Session'; +import getDeviceID from '../getDeviceID'; let currentUserAccountID; let currentUserEmail = ''; @@ -270,6 +271,27 @@ function openProfile() { Navigation.navigate(ROUTES.SETTINGS_PROFILE); } +/** + * Saves a unique deviceID into Onyx. + */ +function setDeviceID() { + const connectionID = Onyx.connect({ + key: ONYXKEYS.DEVICE_ID, + callback: (deviceID) => { + Onyx.disconnect(connectionID); + if (deviceID) { + Log.info('Found existing deviceID', false, deviceID); + return; + } + + getDeviceID().then((uniqueID) => { + Log.info('Setting new deviceID', false, uniqueID); + Onyx.set(ONYXKEYS.DEVICE_ID, uniqueID); + }); + }, + }); +} + export { setLocale, setSidebarLoaded, @@ -277,4 +299,5 @@ export { openProfile, openApp, reconnectApp, + setDeviceID, }; diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index c7b1079c04f4..7abcd94337bb 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -29,6 +29,7 @@ function clearStorageAndRedirect(errorMessage) { const keysToPreserve = []; keysToPreserve.push(ONYXKEYS.NVP_PREFERRED_LOCALE); keysToPreserve.push(ONYXKEYS.ACTIVE_CLIENTS); + keysToPreserve.push(ONYXKEYS.DEVICE_ID); // After signing out, set ourselves as offline if we were offline before logging out and we are not forcing it. // If we are forcing offline, ignore it while signed out, otherwise it would require a refresh because there's no way to toggle the switch to go back online while signed out. diff --git a/src/libs/getDeviceID/index.ios.js b/src/libs/getDeviceID/index.ios.js new file mode 100644 index 000000000000..3146ca7bcd6e --- /dev/null +++ b/src/libs/getDeviceID/index.ios.js @@ -0,0 +1,15 @@ +import DeviceInfo from 'react-native-device-info'; + +const deviceID = DeviceInfo.getDeviceId(); + +/** + * Get the unique ID of the current device. This should remain the same even if the user uninstalls and reinstalls the app. + * + * @returns {Promise} + */ +function getDeviceID() { + return DeviceInfo.getUniqueId() + .then(uniqueID => `${deviceID}_${uniqueID}`); +} + +export default getDeviceID; diff --git a/src/libs/getDeviceID/index.js b/src/libs/getDeviceID/index.js new file mode 100644 index 000000000000..b5ff94ef6e53 --- /dev/null +++ b/src/libs/getDeviceID/index.js @@ -0,0 +1,32 @@ +import DeviceInfo from 'react-native-device-info'; +import Str from 'expensify-common'; + +const deviceID = DeviceInfo.getDeviceId(); +const uniqueID = Str.guid(deviceID); + +/** + * Get the "unique ID of the device". Note that the hardware ID provided by react-native-device-info for Android is considered private information, + * so using it without appropriate permissions would cause our app to be unlisted from the Google Play Store: + * + * - https://developer.android.com/training/articles/user-data-ids#kotlin + * = https://support.google.com/googleplay/android-developer/answer/10144311 + * + * Therefore, this deviceID is not truly unique but will be a new GUID each time the app runs. + * + * This GUID should stored in Onyx under ONYXKEYS.DEVICE_ID and preserved on logout, such that the deviceID will only change if: + * + * - The user uninstalls and reinstalls the app (Android/desktop) + * - The user opens the app on a different browser or in an incognito window (web) + * - The user manually clears Onyx data + * + * While this isn't perfect, it's the best we can do without violating Google Play's App Store guidelines. + * It's also just as good as any common web solution, such as this one (which is also reset under the same circumstances): + * https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId + * + * @returns {Promise} + */ +function getDeviceID() { + return Promise.resolve(uniqueID); +} + +export default getDeviceID; diff --git a/src/setup/index.js b/src/setup/index.js index 7cf509e1e3dd..e4ad7c235939 100644 --- a/src/setup/index.js +++ b/src/setup/index.js @@ -4,6 +4,7 @@ import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; import platformSetup from './platformSetup'; import * as Metrics from '../libs/Metrics'; +import * as App from '../libs/actions/App'; export default function () { /* @@ -40,6 +41,8 @@ export default function () { }, }); + App.setDeviceID(); + // Force app layout to work left to right because our design does not currently support devices using this mode I18nManager.allowRTL(false); I18nManager.forceRTL(false); From 973b551e21198081e01a36d5c62a8191ffd63818 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 16:13:15 -0800 Subject: [PATCH 17/38] Move Device to its own action --- .../PushNotification/permissionTracker.js | 0 src/libs/actions/App.js | 23 --------- .../Device/generateDeviceID}/index.ios.js | 4 +- .../Device/generateDeviceID}/index.js | 4 +- src/libs/actions/Device/index.js | 50 +++++++++++++++++++ src/setup/index.js | 4 +- 6 files changed, 56 insertions(+), 29 deletions(-) create mode 100644 src/libs/Notification/PushNotification/permissionTracker.js rename src/libs/{getDeviceID => actions/Device/generateDeviceID}/index.ios.js (84%) rename src/libs/{getDeviceID => actions/Device/generateDeviceID}/index.js (95%) create mode 100644 src/libs/actions/Device/index.js diff --git a/src/libs/Notification/PushNotification/permissionTracker.js b/src/libs/Notification/PushNotification/permissionTracker.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js index ace9a979568c..874b45f1fc57 100644 --- a/src/libs/actions/App.js +++ b/src/libs/actions/App.js @@ -16,7 +16,6 @@ import ROUTES from '../../ROUTES'; import * as SessionUtils from '../SessionUtils'; import getCurrentUrl from '../Navigation/currentUrl'; import * as Session from './Session'; -import getDeviceID from '../getDeviceID'; let currentUserAccountID; let currentUserEmail = ''; @@ -271,27 +270,6 @@ function openProfile() { Navigation.navigate(ROUTES.SETTINGS_PROFILE); } -/** - * Saves a unique deviceID into Onyx. - */ -function setDeviceID() { - const connectionID = Onyx.connect({ - key: ONYXKEYS.DEVICE_ID, - callback: (deviceID) => { - Onyx.disconnect(connectionID); - if (deviceID) { - Log.info('Found existing deviceID', false, deviceID); - return; - } - - getDeviceID().then((uniqueID) => { - Log.info('Setting new deviceID', false, uniqueID); - Onyx.set(ONYXKEYS.DEVICE_ID, uniqueID); - }); - }, - }); -} - export { setLocale, setSidebarLoaded, @@ -299,5 +277,4 @@ export { openProfile, openApp, reconnectApp, - setDeviceID, }; diff --git a/src/libs/getDeviceID/index.ios.js b/src/libs/actions/Device/generateDeviceID/index.ios.js similarity index 84% rename from src/libs/getDeviceID/index.ios.js rename to src/libs/actions/Device/generateDeviceID/index.ios.js index 3146ca7bcd6e..943971c5ba45 100644 --- a/src/libs/getDeviceID/index.ios.js +++ b/src/libs/actions/Device/generateDeviceID/index.ios.js @@ -7,9 +7,9 @@ const deviceID = DeviceInfo.getDeviceId(); * * @returns {Promise} */ -function getDeviceID() { +function generateDeviceID() { return DeviceInfo.getUniqueId() .then(uniqueID => `${deviceID}_${uniqueID}`); } -export default getDeviceID; +export default generateDeviceID; diff --git a/src/libs/getDeviceID/index.js b/src/libs/actions/Device/generateDeviceID/index.js similarity index 95% rename from src/libs/getDeviceID/index.js rename to src/libs/actions/Device/generateDeviceID/index.js index b5ff94ef6e53..738195f6d7ac 100644 --- a/src/libs/getDeviceID/index.js +++ b/src/libs/actions/Device/generateDeviceID/index.js @@ -25,8 +25,8 @@ const uniqueID = Str.guid(deviceID); * * @returns {Promise} */ -function getDeviceID() { +function generateDeviceID() { return Promise.resolve(uniqueID); } -export default getDeviceID; +export default generateDeviceID; diff --git a/src/libs/actions/Device/index.js b/src/libs/actions/Device/index.js new file mode 100644 index 000000000000..f56b5941c9d0 --- /dev/null +++ b/src/libs/actions/Device/index.js @@ -0,0 +1,50 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../../ONYXKEYS'; +import Log from '../../Log'; +import generateDeviceID from './generateDeviceID'; + +let deviceID; + +/** + * @returns {Promise} + */ +function getDeviceID() { + return new Promise((resolve) => { + if (deviceID) { + return resolve(deviceID); + } + + const connectionID = Onyx.connect({ + key: ONYXKEYS.DEVICE_ID, + callback: (ID) => { + Onyx.disconnect(connectionID); + deviceID = ID; + return resolve(ID); + }, + }); + }); +} + +/** + * Saves a unique deviceID into Onyx. + */ +function setDeviceID() { + getDeviceID() + .then((existingDeviceID) => { + if (!existingDeviceID) { + return Promise.resolve(); + } + throw new Error(existingDeviceID); + }) + .then(generateDeviceID) + .then((uniqueID) => { + Log.info('Got new deviceID', false, uniqueID); + Onyx.set(ONYXKEYS.DEVICE_ID, uniqueID); + }) + .catch(err => Log.info('Found existing deviceID', false, err.message)); +} + +export { + getDeviceID, + setDeviceID, +}; diff --git a/src/setup/index.js b/src/setup/index.js index e4ad7c235939..c2b380f6485b 100644 --- a/src/setup/index.js +++ b/src/setup/index.js @@ -4,7 +4,7 @@ import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; import platformSetup from './platformSetup'; import * as Metrics from '../libs/Metrics'; -import * as App from '../libs/actions/App'; +import * as Device from '../libs/actions/Device'; export default function () { /* @@ -41,7 +41,7 @@ export default function () { }, }); - App.setDeviceID(); + Device.setDeviceID(); // Force app layout to work left to right because our design does not currently support devices using this mode I18nManager.allowRTL(false); From 1513b20d52857f36c7fb73f152d0b4ef8a05032d Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 16:33:26 -0800 Subject: [PATCH 18/38] Fix require cycle by moving push notification tracking to separate module --- .../PushNotification/index.native.js | 43 ++++++++------ .../PushNotification/permissionTracker.js | 33 +++++++++++ src/libs/actions/User.js | 59 +++++++------------ 3 files changed, 79 insertions(+), 56 deletions(-) diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js index aab1da8beeab..3dcfb7f31e7d 100644 --- a/src/libs/Notification/PushNotification/index.native.js +++ b/src/libs/Notification/PushNotification/index.native.js @@ -4,6 +4,7 @@ import {UrbanAirship, EventType, iOS} from 'urbanairship-react-native'; import lodashGet from 'lodash/get'; import Log from '../../Log'; import NotificationType from './NotificationType'; +import PermissionTracker from './permissionTracker'; import * as User from '../../actions/User'; const notificationEventActionMap = {}; @@ -50,10 +51,16 @@ function pushNotificationEventCallback(eventType, notification) { * Check if a user is opted-in to push notifications and update the `pushNotificationsEnabled` NVP accordingly. */ function refreshNotificationOptInStatus() { - UrbanAirship.getNotificationStatus() - .then((notificationStatus) => { - const isOptedIn = notificationStatus.airshipOptIn && notificationStatus.systemEnabled; - if (isOptedIn === User.isUserOptedIntoPushNotifications()) { + Promise.all([ + UrbanAirship.getNotificationStatus(), + PermissionTracker.isUserOptedInToPushNotifications(), + ]) + .then(([ + notificationStatusFromAirship, + notificationStatusFromOnyx, + ]) => { + const isOptedIn = notificationStatusFromAirship.airshipOptIn && notificationStatusFromAirship.systemEnabled; + if (isOptedIn === notificationStatusFromOnyx) { return; } @@ -72,19 +79,21 @@ function refreshNotificationOptInStatus() { function init() { // Setup event listeners UrbanAirship.addListener(EventType.PushReceived, (notification) => { - // By default, refresh notification opt-in status to true if we receive a notification - if (!User.isUserOptedIntoPushNotifications()) { - User.setPushNotificationOptInStatus(true); - } - - // If a push notification is received while the app is in foreground, - // we'll assume pusher is connected so we'll ignore it and not fetch the same data twice. - if (AppState.currentState === 'active') { - Log.info('[PUSH_NOTIFICATION] Push received while app is in foreground, not executing any callback.'); - return; - } - - pushNotificationEventCallback(EventType.PushReceived, notification); + PermissionTracker.isUserOptedInToPushNotifications((isUserOptedIntoPushNotifications) => { + // By default, refresh notification opt-in status to true if we receive a notification + if (!isUserOptedIntoPushNotifications) { + User.setPushNotificationOptInStatus(true); + } + + // If a push notification is received while the app is in foreground, + // we'll assume pusher is connected so we'll ignore it and not fetch the same data twice. + if (AppState.currentState === 'active') { + Log.info('[PUSH_NOTIFICATION] Push received while app is in foreground, not executing any callback.'); + return; + } + + pushNotificationEventCallback(EventType.PushReceived, notification); + }); }); // Note: the NotificationResponse event has a nested PushReceived event, diff --git a/src/libs/Notification/PushNotification/permissionTracker.js b/src/libs/Notification/PushNotification/permissionTracker.js index e69de29bb2d1..5f2616b50364 100644 --- a/src/libs/Notification/PushNotification/permissionTracker.js +++ b/src/libs/Notification/PushNotification/permissionTracker.js @@ -0,0 +1,33 @@ +import _ from 'underscore'; +import lodashGet from 'lodash/get'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../../ONYXKEYS'; +import * as Device from '../../actions/Device'; + +let isUserOptedInToPushNotifications = false; +const getDeviceIDPromise = Device.getDeviceID() + .then((deviceID) => { + Onyx.connect({ + key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, + callback: (val) => { + const pushNotificationOptInRecord = lodashGet(val, deviceID, []); + const mostRecentNVPValue = _.last(pushNotificationOptInRecord); + if (!_.has(mostRecentNVPValue, 'isEnabled')) { + return; + } + isUserOptedInToPushNotifications = mostRecentNVPValue.isEnabled; + }, + }); + }); + +/** + * @returns {Promise} + */ +function isUserOptedIntoPushNotifications() { + return getDeviceIDPromise + .then(() => isUserOptedInToPushNotifications); +} + +export default { + isUserOptedInToPushNotifications, +}; diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index ac3d6d60da14..b16e5c7f9bc9 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -20,7 +20,7 @@ import * as SequentialQueue from '../Network/SequentialQueue'; import PusherUtils from '../PusherUtils'; import * as Report from './Report'; import * as ReportActionsUtils from '../ReportActionsUtils'; -import PushNotification from '../Notification/PushNotification'; +import PushNotificationPermissionTracker from '../Notification/PushNotification/permissionTracker'; const deviceID = DeviceInfo.getDeviceId(); @@ -32,27 +32,6 @@ Onyx.connect({ }, }); -let isUserOptedInToPushNotifications = false; -Onyx.connect({ - key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, - callback: (val) => { - const pushNotificationOptInRecord = lodashGet(val, deviceID, []); - const mostRecentNVPValue = _.last(pushNotificationOptInRecord); - if (!_.has(mostRecentNVPValue, 'isEnabled')) { - return; - } - isUserOptedInToPushNotifications = mostRecentNVPValue.isEnabled; - }, -}); - -/** - * @returns {Boolean} - */ -function isUserOptedIntoPushNotifications() { - return isUserOptedInToPushNotifications; -} - - /** * Changes a password for a given account * @@ -499,22 +478,25 @@ function generateStatementPDF(period) { * @param {Boolean} isOptingIn */ function setPushNotificationOptInStatus(isOptingIn) { - const commandName = isOptingIn ? 'OptInToPushNotifications' : 'OptOutOfPushNotifications'; - const optimisticData = [ - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, - value: {[deviceID]: isOptingIn}, - }, - ]; - const failureData = [ - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, - value: {[deviceID]: PushNotification.isUserOptedIn()}, - }, - ]; - API.write(commandName, {deviceID}, {optimisticData, failureData}); + PushNotificationPermissionTracker.isUserOptedInToPushNotifications() + .then((isUserOptedInToPushNotifications) => { + const commandName = isOptingIn ? 'OptInToPushNotifications' : 'OptOutOfPushNotifications'; + const optimisticData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, + value: {[deviceID]: isOptingIn}, + }, + ]; + const failureData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, + value: {[deviceID]: isUserOptedInToPushNotifications}, + }, + ]; + API.write(commandName, {deviceID}, {optimisticData, failureData}); + }); } export { @@ -538,5 +520,4 @@ export { addPaypalMeAddress, updateChatPriorityMode, setPushNotificationOptInStatus, - isUserOptedIntoPushNotifications, }; From ac8badf019d9a19d887d82bf089168ae953b30da Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 17:23:47 -0800 Subject: [PATCH 19/38] Fix permission tracker export --- src/libs/Notification/PushNotification/index.native.js | 4 ++-- src/libs/Notification/PushNotification/permissionTracker.js | 2 +- src/libs/actions/User.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js index 3dcfb7f31e7d..ded3f154687a 100644 --- a/src/libs/Notification/PushNotification/index.native.js +++ b/src/libs/Notification/PushNotification/index.native.js @@ -53,7 +53,7 @@ function pushNotificationEventCallback(eventType, notification) { function refreshNotificationOptInStatus() { Promise.all([ UrbanAirship.getNotificationStatus(), - PermissionTracker.isUserOptedInToPushNotifications(), + PermissionTracker.isUserOptedIntoPushNotifications(), ]) .then(([ notificationStatusFromAirship, @@ -79,7 +79,7 @@ function refreshNotificationOptInStatus() { function init() { // Setup event listeners UrbanAirship.addListener(EventType.PushReceived, (notification) => { - PermissionTracker.isUserOptedInToPushNotifications((isUserOptedIntoPushNotifications) => { + PermissionTracker.isUserOptedIntoPushNotifications((isUserOptedIntoPushNotifications) => { // By default, refresh notification opt-in status to true if we receive a notification if (!isUserOptedIntoPushNotifications) { User.setPushNotificationOptInStatus(true); diff --git a/src/libs/Notification/PushNotification/permissionTracker.js b/src/libs/Notification/PushNotification/permissionTracker.js index 5f2616b50364..ff9026e54857 100644 --- a/src/libs/Notification/PushNotification/permissionTracker.js +++ b/src/libs/Notification/PushNotification/permissionTracker.js @@ -29,5 +29,5 @@ function isUserOptedIntoPushNotifications() { } export default { - isUserOptedInToPushNotifications, + isUserOptedIntoPushNotifications, }; diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index b16e5c7f9bc9..2314091bf3dc 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -478,7 +478,7 @@ function generateStatementPDF(period) { * @param {Boolean} isOptingIn */ function setPushNotificationOptInStatus(isOptingIn) { - PushNotificationPermissionTracker.isUserOptedInToPushNotifications() + PushNotificationPermissionTracker.isUserOptedIntoPushNotifications() .then((isUserOptedInToPushNotifications) => { const commandName = isOptingIn ? 'OptInToPushNotifications' : 'OptOutOfPushNotifications'; const optimisticData = [ From 3b13de7e848e4bbc3862b856e6db69e3137a844f Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 17:45:21 -0800 Subject: [PATCH 20/38] Fix require cycle by moving setPushNotificationOptInStatus out of User --- .../PushNotification/index.native.js | 6 +-- src/libs/actions/PushNotification.js | 44 +++++++++++++++++++ src/libs/actions/User.js | 33 -------------- 3 files changed, 47 insertions(+), 36 deletions(-) create mode 100644 src/libs/actions/PushNotification.js diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js index ded3f154687a..9341e0bb9de1 100644 --- a/src/libs/Notification/PushNotification/index.native.js +++ b/src/libs/Notification/PushNotification/index.native.js @@ -4,8 +4,8 @@ import {UrbanAirship, EventType, iOS} from 'urbanairship-react-native'; import lodashGet from 'lodash/get'; import Log from '../../Log'; import NotificationType from './NotificationType'; +import * as PushNotification from '../../actions/PushNotification'; import PermissionTracker from './permissionTracker'; -import * as User from '../../actions/User'; const notificationEventActionMap = {}; @@ -65,7 +65,7 @@ function refreshNotificationOptInStatus() { } Log.info('[PUSH_NOTIFICATION] Push notification opt-in status changed.', false, {isOptedIn}); - User.setPushNotificationOptInStatus(isOptedIn); + PushNotification.setPushNotificationOptInStatus(isOptedIn); }); } @@ -82,7 +82,7 @@ function init() { PermissionTracker.isUserOptedIntoPushNotifications((isUserOptedIntoPushNotifications) => { // By default, refresh notification opt-in status to true if we receive a notification if (!isUserOptedIntoPushNotifications) { - User.setPushNotificationOptInStatus(true); + PushNotification.setPushNotificationOptInStatus(true); } // If a push notification is received while the app is in foreground, diff --git a/src/libs/actions/PushNotification.js b/src/libs/actions/PushNotification.js new file mode 100644 index 000000000000..4c6953d8ad03 --- /dev/null +++ b/src/libs/actions/PushNotification.js @@ -0,0 +1,44 @@ +import CONST from '../../CONST'; +import ONYXKEYS from '../../ONYXKEYS'; +import * as API from '../API'; +import * as Device from './Device'; +import PushNotificationPermissionTracker from '../Notification/PushNotification/permissionTracker'; + +/** + * Record that user opted-in or opted-out of push notifications on the current device. + * NOTE: This is purely for record-keeping purposes, and does not affect whether our server will attempt to send notifications to this user. + * + * @param {Boolean} isOptingIn + */ +function setPushNotificationOptInStatus(isOptingIn) { + Promise.all([ + Device.getDeviceID(), + PushNotificationPermissionTracker.isUserOptedIntoPushNotifications(), + ]) + .then(([ + deviceID, + isUserOptedInToPushNotifications, + ]) => { + const commandName = isOptingIn ? 'OptInToPushNotifications' : 'OptOutOfPushNotifications'; + const optimisticData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, + value: {[deviceID]: isOptingIn}, + }, + ]; + const failureData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, + value: {[deviceID]: isUserOptedInToPushNotifications}, + }, + ]; + API.write(commandName, {deviceID}, {optimisticData, failureData}); + }); +} + +export { + // eslint-disable-next-line import/prefer-default-export + setPushNotificationOptInStatus, +}; diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 2314091bf3dc..09e7d6873084 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -2,7 +2,6 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; import moment from 'moment'; -import DeviceInfo from 'react-native-device-info'; import ONYXKEYS from '../../ONYXKEYS'; import * as DeprecatedAPI from '../deprecatedAPI'; import * as API from '../API'; @@ -20,9 +19,6 @@ import * as SequentialQueue from '../Network/SequentialQueue'; import PusherUtils from '../PusherUtils'; import * as Report from './Report'; import * as ReportActionsUtils from '../ReportActionsUtils'; -import PushNotificationPermissionTracker from '../Notification/PushNotification/permissionTracker'; - -const deviceID = DeviceInfo.getDeviceId(); let currentUserAccountID = ''; Onyx.connect({ @@ -471,34 +467,6 @@ function generateStatementPDF(period) { }); } -/** - * Record that user opted-in or opted-out of push notifications on the current device. - * NOTE: This is purely for record-keeping purposes, and does not affect whether our server will attempt to send notifications to this user. - * - * @param {Boolean} isOptingIn - */ -function setPushNotificationOptInStatus(isOptingIn) { - PushNotificationPermissionTracker.isUserOptedIntoPushNotifications() - .then((isUserOptedInToPushNotifications) => { - const commandName = isOptingIn ? 'OptInToPushNotifications' : 'OptOutOfPushNotifications'; - const optimisticData = [ - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, - value: {[deviceID]: isOptingIn}, - }, - ]; - const failureData = [ - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, - value: {[deviceID]: isUserOptedInToPushNotifications}, - }, - ]; - API.write(commandName, {deviceID}, {optimisticData, failureData}); - }); -} - export { updatePassword, closeAccount, @@ -519,5 +487,4 @@ export { deletePaypalMeAddress, addPaypalMeAddress, updateChatPriorityMode, - setPushNotificationOptInStatus, }; From 6a7c2153414c9606f122862be444bdcf98a8a840 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 17:49:08 -0800 Subject: [PATCH 21/38] Remove awkward newline --- src/libs/Notification/PushNotification/permissionTracker.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/Notification/PushNotification/permissionTracker.js b/src/libs/Notification/PushNotification/permissionTracker.js index ff9026e54857..f9434bdc24fd 100644 --- a/src/libs/Notification/PushNotification/permissionTracker.js +++ b/src/libs/Notification/PushNotification/permissionTracker.js @@ -24,8 +24,7 @@ const getDeviceIDPromise = Device.getDeviceID() * @returns {Promise} */ function isUserOptedIntoPushNotifications() { - return getDeviceIDPromise - .then(() => isUserOptedInToPushNotifications); + return getDeviceIDPromise.then(() => isUserOptedInToPushNotifications); } export default { From 6621f1e3516557ad6e24bf052bacc7f1119625d4 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 17:51:32 -0800 Subject: [PATCH 22/38] Clarify comment --- src/libs/actions/Device/generateDeviceID/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/Device/generateDeviceID/index.js b/src/libs/actions/Device/generateDeviceID/index.js index 738195f6d7ac..9b2f3cfa7f98 100644 --- a/src/libs/actions/Device/generateDeviceID/index.js +++ b/src/libs/actions/Device/generateDeviceID/index.js @@ -13,14 +13,14 @@ const uniqueID = Str.guid(deviceID); * * Therefore, this deviceID is not truly unique but will be a new GUID each time the app runs. * - * This GUID should stored in Onyx under ONYXKEYS.DEVICE_ID and preserved on logout, such that the deviceID will only change if: + * This GUID is stored in Onyx under ONYXKEYS.DEVICE_ID and is preserved on logout, such that the deviceID will only change if: * - * - The user uninstalls and reinstalls the app (Android/desktop) - * - The user opens the app on a different browser or in an incognito window (web) + * - The user uninstalls and reinstalls the app (Android/desktop), OR + * - The user opens the app on a different browser or in an incognito window (web), OR * - The user manually clears Onyx data * - * While this isn't perfect, it's the best we can do without violating Google Play's App Store guidelines. - * It's also just as good as any common web solution, such as this one (which is also reset under the same circumstances): + * While this isn't perfect, it's the best we can do without violating the Google Play Store guidelines. + * It's also just as good as any obvious web solution, such as this one (which is also reset under the same circumstances): * https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId * * @returns {Promise} From 4f2ca7bfaf2ff043b0aa730dcdc8bdf7a89f082d Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 18:19:13 -0800 Subject: [PATCH 23/38] Simplify Onyx data to a boolean --- src/ONYXKEYS.js | 2 +- .../PushNotification/permissionTracker.js | 23 ++++--------------- src/libs/actions/PushNotification.js | 8 +++---- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 99c2fede9fa2..eaeadf401516 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -80,7 +80,7 @@ export default { NVP_BLOCKED_FROM_CONCIERGE: 'private_blockedFromConcierge', // Does this user have push notifications enabled? - NVP_PUSH_NOTIFICATIONS_ENABLED: 'nvp_pushNotificationsEnabled', + PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', // Plaid data (access tokens, bank accounts ...) PLAID_DATA: 'plaidData', diff --git a/src/libs/Notification/PushNotification/permissionTracker.js b/src/libs/Notification/PushNotification/permissionTracker.js index f9434bdc24fd..c4baa50f92f6 100644 --- a/src/libs/Notification/PushNotification/permissionTracker.js +++ b/src/libs/Notification/PushNotification/permissionTracker.js @@ -1,30 +1,17 @@ -import _ from 'underscore'; -import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../../ONYXKEYS'; -import * as Device from '../../actions/Device'; let isUserOptedInToPushNotifications = false; -const getDeviceIDPromise = Device.getDeviceID() - .then((deviceID) => { - Onyx.connect({ - key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, - callback: (val) => { - const pushNotificationOptInRecord = lodashGet(val, deviceID, []); - const mostRecentNVPValue = _.last(pushNotificationOptInRecord); - if (!_.has(mostRecentNVPValue, 'isEnabled')) { - return; - } - isUserOptedInToPushNotifications = mostRecentNVPValue.isEnabled; - }, - }); - }); +Onyx.connect({ + key: ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED, + callback: val => isUserOptedInToPushNotifications = val, +}); /** * @returns {Promise} */ function isUserOptedIntoPushNotifications() { - return getDeviceIDPromise.then(() => isUserOptedInToPushNotifications); + return isUserOptedInToPushNotifications; } export default { diff --git a/src/libs/actions/PushNotification.js b/src/libs/actions/PushNotification.js index 4c6953d8ad03..56f87dc57b1c 100644 --- a/src/libs/actions/PushNotification.js +++ b/src/libs/actions/PushNotification.js @@ -23,15 +23,15 @@ function setPushNotificationOptInStatus(isOptingIn) { const optimisticData = [ { onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, - value: {[deviceID]: isOptingIn}, + key: ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED, + value: isOptingIn, }, ]; const failureData = [ { onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.NVP_PUSH_NOTIFICATIONS_ENABLED, - value: {[deviceID]: isUserOptedInToPushNotifications}, + key: ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED, + value: isUserOptedInToPushNotifications, }, ]; API.write(commandName, {deviceID}, {optimisticData, failureData}); From 77f029d55fc851280a0e28268ac032daa50c846d Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 18:21:30 -0800 Subject: [PATCH 24/38] Fix use of PermissionsTracker --- .../PushNotification/index.native.js | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js index 9341e0bb9de1..7902c11d921a 100644 --- a/src/libs/Notification/PushNotification/index.native.js +++ b/src/libs/Notification/PushNotification/index.native.js @@ -79,21 +79,22 @@ function refreshNotificationOptInStatus() { function init() { // Setup event listeners UrbanAirship.addListener(EventType.PushReceived, (notification) => { - PermissionTracker.isUserOptedIntoPushNotifications((isUserOptedIntoPushNotifications) => { - // By default, refresh notification opt-in status to true if we receive a notification - if (!isUserOptedIntoPushNotifications) { - PushNotification.setPushNotificationOptInStatus(true); - } - - // If a push notification is received while the app is in foreground, - // we'll assume pusher is connected so we'll ignore it and not fetch the same data twice. - if (AppState.currentState === 'active') { - Log.info('[PUSH_NOTIFICATION] Push received while app is in foreground, not executing any callback.'); - return; - } - - pushNotificationEventCallback(EventType.PushReceived, notification); - }); + PermissionTracker.isUserOptedIntoPushNotifications() + .then((isUserOptedIntoPushNotifications) => { + // By default, refresh notification opt-in status to true if we receive a notification + if (!isUserOptedIntoPushNotifications) { + PushNotification.setPushNotificationOptInStatus(true); + } + + // If a push notification is received while the app is in foreground, + // we'll assume pusher is connected so we'll ignore it and not fetch the same data twice. + if (AppState.currentState === 'active') { + Log.info('[PUSH_NOTIFICATION] Push received while app is in foreground, not executing any callback.'); + return; + } + + pushNotificationEventCallback(EventType.PushReceived, notification); + }); }); // Note: the NotificationResponse event has a nested PushReceived event, From fb853a3c16032c8939250f125b8600a67a9817ef Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 18:26:38 -0800 Subject: [PATCH 25/38] Remove permissionTracker entirely --- .../PushNotification/index.native.js | 52 +++++++++---------- .../PushNotification/permissionTracker.js | 19 ------- src/libs/actions/PushNotification.js | 18 +++---- 3 files changed, 34 insertions(+), 55 deletions(-) delete mode 100644 src/libs/Notification/PushNotification/permissionTracker.js diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js index 7902c11d921a..fb69307fe45b 100644 --- a/src/libs/Notification/PushNotification/index.native.js +++ b/src/libs/Notification/PushNotification/index.native.js @@ -1,11 +1,18 @@ import _ from 'underscore'; import {AppState} from 'react-native'; +import Onyx from 'react-native-onyx'; import {UrbanAirship, EventType, iOS} from 'urbanairship-react-native'; import lodashGet from 'lodash/get'; import Log from '../../Log'; import NotificationType from './NotificationType'; import * as PushNotification from '../../actions/PushNotification'; -import PermissionTracker from './permissionTracker'; +import ONYXKEYS from '../../../ONYXKEYS'; + +let isUserOptedInToPushNotifications = false; +Onyx.connect({ + key: ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED, + callback: val => isUserOptedInToPushNotifications = val, +}); const notificationEventActionMap = {}; @@ -51,16 +58,10 @@ function pushNotificationEventCallback(eventType, notification) { * Check if a user is opted-in to push notifications and update the `pushNotificationsEnabled` NVP accordingly. */ function refreshNotificationOptInStatus() { - Promise.all([ - UrbanAirship.getNotificationStatus(), - PermissionTracker.isUserOptedIntoPushNotifications(), - ]) - .then(([ - notificationStatusFromAirship, - notificationStatusFromOnyx, - ]) => { - const isOptedIn = notificationStatusFromAirship.airshipOptIn && notificationStatusFromAirship.systemEnabled; - if (isOptedIn === notificationStatusFromOnyx) { + UrbanAirship.getNotificationStatus() + .then((notificationStatus) => { + const isOptedIn = notificationStatus.airshipOptIn && notificationStatus.systemEnabled; + if (isOptedIn === isUserOptedInToPushNotifications) { return; } @@ -79,22 +80,19 @@ function refreshNotificationOptInStatus() { function init() { // Setup event listeners UrbanAirship.addListener(EventType.PushReceived, (notification) => { - PermissionTracker.isUserOptedIntoPushNotifications() - .then((isUserOptedIntoPushNotifications) => { - // By default, refresh notification opt-in status to true if we receive a notification - if (!isUserOptedIntoPushNotifications) { - PushNotification.setPushNotificationOptInStatus(true); - } - - // If a push notification is received while the app is in foreground, - // we'll assume pusher is connected so we'll ignore it and not fetch the same data twice. - if (AppState.currentState === 'active') { - Log.info('[PUSH_NOTIFICATION] Push received while app is in foreground, not executing any callback.'); - return; - } - - pushNotificationEventCallback(EventType.PushReceived, notification); - }); + // By default, refresh notification opt-in status to true if we receive a notification + if (!isUserOptedInToPushNotifications) { + PushNotification.setPushNotificationOptInStatus(true); + } + + // If a push notification is received while the app is in foreground, + // we'll assume pusher is connected so we'll ignore it and not fetch the same data twice. + if (AppState.currentState === 'active') { + Log.info('[PUSH_NOTIFICATION] Push received while app is in foreground, not executing any callback.'); + return; + } + + pushNotificationEventCallback(EventType.PushReceived, notification); }); // Note: the NotificationResponse event has a nested PushReceived event, diff --git a/src/libs/Notification/PushNotification/permissionTracker.js b/src/libs/Notification/PushNotification/permissionTracker.js deleted file mode 100644 index c4baa50f92f6..000000000000 --- a/src/libs/Notification/PushNotification/permissionTracker.js +++ /dev/null @@ -1,19 +0,0 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '../../../ONYXKEYS'; - -let isUserOptedInToPushNotifications = false; -Onyx.connect({ - key: ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED, - callback: val => isUserOptedInToPushNotifications = val, -}); - -/** - * @returns {Promise} - */ -function isUserOptedIntoPushNotifications() { - return isUserOptedInToPushNotifications; -} - -export default { - isUserOptedIntoPushNotifications, -}; diff --git a/src/libs/actions/PushNotification.js b/src/libs/actions/PushNotification.js index 56f87dc57b1c..2bc86a878246 100644 --- a/src/libs/actions/PushNotification.js +++ b/src/libs/actions/PushNotification.js @@ -1,8 +1,14 @@ +import Onyx from 'react-native-onyx'; import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; import * as Device from './Device'; -import PushNotificationPermissionTracker from '../Notification/PushNotification/permissionTracker'; + +let isUserOptedInToPushNotifications = false; +Onyx.connect({ + key: ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED, + callback: val => isUserOptedInToPushNotifications = val, +}); /** * Record that user opted-in or opted-out of push notifications on the current device. @@ -11,14 +17,8 @@ import PushNotificationPermissionTracker from '../Notification/PushNotification/ * @param {Boolean} isOptingIn */ function setPushNotificationOptInStatus(isOptingIn) { - Promise.all([ - Device.getDeviceID(), - PushNotificationPermissionTracker.isUserOptedIntoPushNotifications(), - ]) - .then(([ - deviceID, - isUserOptedInToPushNotifications, - ]) => { + Device.getDeviceID() + .then((deviceID) => { const commandName = isOptingIn ? 'OptInToPushNotifications' : 'OptOutOfPushNotifications'; const optimisticData = [ { From 1470b019c9ebc555402700662b5b555affa4e835 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 18:32:31 -0800 Subject: [PATCH 26/38] Improve device comment again --- src/libs/actions/Device/generateDeviceID/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/Device/generateDeviceID/index.js b/src/libs/actions/Device/generateDeviceID/index.js index 9b2f3cfa7f98..4914dc3f0fe3 100644 --- a/src/libs/actions/Device/generateDeviceID/index.js +++ b/src/libs/actions/Device/generateDeviceID/index.js @@ -8,16 +8,16 @@ const uniqueID = Str.guid(deviceID); * Get the "unique ID of the device". Note that the hardware ID provided by react-native-device-info for Android is considered private information, * so using it without appropriate permissions would cause our app to be unlisted from the Google Play Store: * - * - https://developer.android.com/training/articles/user-data-ids#kotlin - * = https://support.google.com/googleplay/android-developer/answer/10144311 + * - https://developer.android.com/training/articles/user-data-ids#kotlin + * = https://support.google.com/googleplay/android-developer/answer/10144311 * - * Therefore, this deviceID is not truly unique but will be a new GUID each time the app runs. + * Therefore, this deviceID is not truly unique but will be a new GUID each time the app runs (we work around this limitation by saving it in Onyx) * * This GUID is stored in Onyx under ONYXKEYS.DEVICE_ID and is preserved on logout, such that the deviceID will only change if: * - * - The user uninstalls and reinstalls the app (Android/desktop), OR - * - The user opens the app on a different browser or in an incognito window (web), OR - * - The user manually clears Onyx data + * - The user uninstalls and reinstalls the app (Android/desktop), OR + * - The user opens the app on a different browser or in an incognito window (web), OR + * - The user manually clears Onyx data * * While this isn't perfect, it's the best we can do without violating the Google Play Store guidelines. * It's also just as good as any obvious web solution, such as this one (which is also reset under the same circumstances): From 1924a4b100c9ed15cb1e1aee077922bece164f6e Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 18:59:24 -0800 Subject: [PATCH 27/38] Add static deviceID for the desktop app --- desktop/ELECTRON_EVENTS.js | 9 +++++---- desktop/contextBridge.js | 16 ++++++++++++++++ desktop/main.js | 3 +++ desktop/package-lock.json | 13 ++++++++++++- desktop/package.json | 3 ++- .../Device/generateDeviceID/index.desktop.js | 12 ++++++++++++ .../actions/Device/generateDeviceID/index.js | 2 +- 7 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 src/libs/actions/Device/generateDeviceID/index.desktop.js diff --git a/desktop/ELECTRON_EVENTS.js b/desktop/ELECTRON_EVENTS.js index f0d9b865b01d..6a808bdb99aa 100644 --- a/desktop/ELECTRON_EVENTS.js +++ b/desktop/ELECTRON_EVENTS.js @@ -1,13 +1,14 @@ const ELECTRON_EVENTS = { + BLUR: 'blur', + FOCUS: 'focus', + LOCALE_UPDATED: 'locale-updated', + REQUEST_DEVICE_ID: 'requestDeviceID', + REQUEST_FOCUS_APP: 'requestFocusApp', REQUEST_UPDATE_BADGE_COUNT: 'requestUpdateBadgeCount', REQUEST_VISIBILITY: 'requestVisibility', - REQUEST_FOCUS_APP: 'requestFocusApp', SHOW_KEYBOARD_SHORTCUTS_MODAL: 'show-keyboard-shortcuts-modal', START_UPDATE: 'start-update', UPDATE_DOWNLOADED: 'update-downloaded', - FOCUS: 'focus', - BLUR: 'blur', - LOCALE_UPDATED: 'locale-updated', }; module.exports = ELECTRON_EVENTS; diff --git a/desktop/contextBridge.js b/desktop/contextBridge.js index f065c4caac21..5acd424142d1 100644 --- a/desktop/contextBridge.js +++ b/desktop/contextBridge.js @@ -6,6 +6,7 @@ const { const ELECTRON_EVENTS = require('./ELECTRON_EVENTS'); const WHITELIST_CHANNELS_RENDERER_TO_MAIN = [ + ELECTRON_EVENTS.REQUEST_DEVICE_ID, ELECTRON_EVENTS.REQUEST_FOCUS_APP, ELECTRON_EVENTS.REQUEST_UPDATE_BADGE_COUNT, ELECTRON_EVENTS.REQUEST_VISIBILITY, @@ -59,6 +60,21 @@ contextBridge.exposeInMainWorld('electron', { return ipcRenderer.sendSync(channel, data); }, + /** + * Wait for an event to be emitted by the main process and sent to the renderer process. + * + * @param {String} channel + * @param {*} args + * @returns {Promise} + */ + invoke: (channel, ...args) => { + if (!_.contains(WHITELIST_CHANNELS_RENDERER_TO_MAIN, channel)) { + throw new Error(getErrorMessage(channel)); + } + + return ipcRenderer.invoke(channel, ...args); + }, + /** * Set up a listener for events emitted from the main process and sent to the renderer process. * diff --git a/desktop/main.js b/desktop/main.js index d4bfd9dd2fe1..9381ff0e2dfe 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -12,6 +12,7 @@ const serve = require('electron-serve'); const contextMenu = require('electron-context-menu'); const {autoUpdater} = require('electron-updater'); const log = require('electron-log'); +const {machineId} = require('node-machine-id'); const ELECTRON_EVENTS = require('./ELECTRON_EVENTS'); const checkForUpdates = require('../src/libs/checkForUpdates'); const CONFIG = require('../src/CONFIG').default; @@ -282,6 +283,8 @@ const mainWindow = (() => { titleBarStyle: 'hidden', }); + ipcMain.handle(ELECTRON_EVENTS.REQUEST_DEVICE_ID, () => machineId()); + /* * The default origin of our Electron app is app://- instead of https://new.expensify.com or https://staging.new.expensify.com * This causes CORS errors because the referer and origin headers are wrong and the API responds with an Access-Control-Allow-Origin that doesn't match app://- diff --git a/desktop/package-lock.json b/desktop/package-lock.json index df18cde3a3d1..abc1299154ef 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -10,7 +10,8 @@ "electron-context-menu": "^2.3.0", "electron-log": "^4.4.7", "electron-serve": "^1.0.0", - "electron-updater": "^4.3.4" + "electron-updater": "^4.3.4", + "node-machine-id": "^1.1.12" } }, "node_modules/@types/semver": { @@ -307,6 +308,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -655,6 +661,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", diff --git a/desktop/package.json b/desktop/package.json index a31d0db4e5bd..45283a260970 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -7,7 +7,8 @@ "electron-context-menu": "^2.3.0", "electron-log": "^4.4.7", "electron-serve": "^1.0.0", - "electron-updater": "^4.3.4" + "electron-updater": "^4.3.4", + "node-machine-id": "^1.1.12" }, "author": "Expensify, Inc.", "license": "MIT", diff --git a/src/libs/actions/Device/generateDeviceID/index.desktop.js b/src/libs/actions/Device/generateDeviceID/index.desktop.js new file mode 100644 index 000000000000..26de25e326e8 --- /dev/null +++ b/src/libs/actions/Device/generateDeviceID/index.desktop.js @@ -0,0 +1,12 @@ +import ELECTRON_EVENTS from '../../../../../desktop/ELECTRON_EVENTS'; + +/** + * Get the unique ID of the current device. This should remain the same even if the user uninstalls and reinstalls the app. + * + * @returns {Promise} + */ +function generateDeviceID() { + return window.electron.invoke(ELECTRON_EVENTS.REQUEST_DEVICE_ID); +} + +export default generateDeviceID; diff --git a/src/libs/actions/Device/generateDeviceID/index.js b/src/libs/actions/Device/generateDeviceID/index.js index 4914dc3f0fe3..b1f0eb566832 100644 --- a/src/libs/actions/Device/generateDeviceID/index.js +++ b/src/libs/actions/Device/generateDeviceID/index.js @@ -15,7 +15,7 @@ const uniqueID = Str.guid(deviceID); * * This GUID is stored in Onyx under ONYXKEYS.DEVICE_ID and is preserved on logout, such that the deviceID will only change if: * - * - The user uninstalls and reinstalls the app (Android/desktop), OR + * - The user uninstalls and reinstalls the app (Android), OR * - The user opens the app on a different browser or in an incognito window (web), OR * - The user manually clears Onyx data * From ae22deef722d9cc6cd4c068326d2b854244bd4ce Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Jan 2023 19:13:58 -0800 Subject: [PATCH 28/38] Separate out Android and web deviceID implementations --- .../Device/generateDeviceID/index.android.js | 32 +++++++++++++++++++ .../actions/Device/generateDeviceID/index.js | 21 ++++-------- 2 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 src/libs/actions/Device/generateDeviceID/index.android.js diff --git a/src/libs/actions/Device/generateDeviceID/index.android.js b/src/libs/actions/Device/generateDeviceID/index.android.js new file mode 100644 index 000000000000..433f9171306e --- /dev/null +++ b/src/libs/actions/Device/generateDeviceID/index.android.js @@ -0,0 +1,32 @@ +import DeviceInfo from 'react-native-device-info'; +import Str from 'expensify-common'; + +const deviceID = DeviceInfo.getDeviceId(); +const uniqueID = Str.guid(deviceID); + +/** + * Get the "unique ID of the device". Note that the hardware ID provided by react-native-device-info for Android is considered private information, + * so using it without appropriate permissions could cause our app to be unlisted from the Google Play Store: + * + * - https://developer.android.com/training/articles/user-data-ids#kotlin + * = https://support.google.com/googleplay/android-developer/answer/10144311 + * + * Therefore, this deviceID is not truly unique, but will be a new GUID each time the app runs (we work around this limitation by saving it in Onyx). + * + * This GUID is stored in Onyx under ONYXKEYS.DEVICE_ID and is preserved on logout, such that the deviceID will only change if: + * + * - The user uninstalls and reinstalls the app (Android), OR + * - The user manually clears Onyx data + * + * While this isn't perfect, it's the best we can do without violating the Google Play Store guidelines, and it's probably good enough for most real-world users. + * Furthermore, the deviceID prefix is not unique to a specific device, but is likely to change from one type of device to another. + * + * Including this prefix will tell us with a reasonable degree of confidence if the user just uninstalled and reinstalled the app, or if they got a new device. + * + * @returns {Promise} + */ +function generateDeviceID() { + return Promise.resolve(uniqueID); +} + +export default generateDeviceID; diff --git a/src/libs/actions/Device/generateDeviceID/index.js b/src/libs/actions/Device/generateDeviceID/index.js index b1f0eb566832..f330520befdd 100644 --- a/src/libs/actions/Device/generateDeviceID/index.js +++ b/src/libs/actions/Device/generateDeviceID/index.js @@ -1,27 +1,18 @@ -import DeviceInfo from 'react-native-device-info'; import Str from 'expensify-common'; -const deviceID = DeviceInfo.getDeviceId(); -const uniqueID = Str.guid(deviceID); +const uniqueID = Str.guid(); /** - * Get the "unique ID of the device". Note that the hardware ID provided by react-native-device-info for Android is considered private information, - * so using it without appropriate permissions would cause our app to be unlisted from the Google Play Store: - * - * - https://developer.android.com/training/articles/user-data-ids#kotlin - * = https://support.google.com/googleplay/android-developer/answer/10144311 - * - * Therefore, this deviceID is not truly unique but will be a new GUID each time the app runs (we work around this limitation by saving it in Onyx) + * Get the "unique ID of the device". + * Note deviceID is not truly unique but will be a new GUID each time the app runs (we work around this limitation by saving it in Onyx) * * This GUID is stored in Onyx under ONYXKEYS.DEVICE_ID and is preserved on logout, such that the deviceID will only change if: * - * - The user uninstalls and reinstalls the app (Android), OR - * - The user opens the app on a different browser or in an incognito window (web), OR + * - The user opens the app on a different browser or in an incognito window, OR * - The user manually clears Onyx data * - * While this isn't perfect, it's the best we can do without violating the Google Play Store guidelines. - * It's also just as good as any obvious web solution, such as this one (which is also reset under the same circumstances): - * https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId + * While this isn't perfect, it's just as good as any other obvious web solution, such as this one https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId + * which is also different/reset under the same circumstances * * @returns {Promise} */ From 7287f42612ea7c2c08b8dfb208c9ab52b0da7f1e Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 27 Jan 2023 11:33:46 -0800 Subject: [PATCH 29/38] Improve comment on invoke --- desktop/contextBridge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/contextBridge.js b/desktop/contextBridge.js index 5acd424142d1..231e0072c6c9 100644 --- a/desktop/contextBridge.js +++ b/desktop/contextBridge.js @@ -61,7 +61,7 @@ contextBridge.exposeInMainWorld('electron', { }, /** - * Wait for an event to be emitted by the main process and sent to the renderer process. + * Execute a function in the main process and return a promise that resolves with its response. * * @param {String} channel * @param {*} args From 4baf080858e6f2983bf6e39b386b0547c6c83e0e Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 27 Jan 2023 11:37:10 -0800 Subject: [PATCH 30/38] Improve comment in generateDeviceID/index.android.js --- src/libs/actions/Device/generateDeviceID/index.android.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Device/generateDeviceID/index.android.js b/src/libs/actions/Device/generateDeviceID/index.android.js index 433f9171306e..f4967e148d0b 100644 --- a/src/libs/actions/Device/generateDeviceID/index.android.js +++ b/src/libs/actions/Device/generateDeviceID/index.android.js @@ -19,8 +19,8 @@ const uniqueID = Str.guid(deviceID); * - The user manually clears Onyx data * * While this isn't perfect, it's the best we can do without violating the Google Play Store guidelines, and it's probably good enough for most real-world users. - * Furthermore, the deviceID prefix is not unique to a specific device, but is likely to change from one type of device to another. * + * Furthermore, the deviceID prefix is not unique to a specific device, but is likely to change from one type of device to another. * Including this prefix will tell us with a reasonable degree of confidence if the user just uninstalled and reinstalled the app, or if they got a new device. * * @returns {Promise} From 137740d264be44a834a979c7a775870adde0ff3c Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 27 Jan 2023 11:56:38 -0800 Subject: [PATCH 31/38] Fix ReportTest --- __mocks__/urbanairship-react-native.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__mocks__/urbanairship-react-native.js b/__mocks__/urbanairship-react-native.js index 8eb5e9c14c08..5bc90f267bf2 100644 --- a/__mocks__/urbanairship-react-native.js +++ b/__mocks__/urbanairship-react-native.js @@ -21,7 +21,7 @@ const UrbanAirship = { removeAllListeners: jest.fn(), setBadgeNumber: jest.fn(), setForegroundPresentationOptions: jest.fn(), - getNotificationStatus: jest.fn(), + getNotificationStatus: () => Promise.resolve({airshipOptIn: false, systemEnabled: false}), }; export default UrbanAirship; From d9d8999576e688f7eadba98dbb31e01a8d335279 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 27 Jan 2023 12:01:37 -0800 Subject: [PATCH 32/38] Rename index.js to index.website.js --- .../Device/generateDeviceID/{index.js => index.website.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/libs/actions/Device/generateDeviceID/{index.js => index.website.js} (100%) diff --git a/src/libs/actions/Device/generateDeviceID/index.js b/src/libs/actions/Device/generateDeviceID/index.website.js similarity index 100% rename from src/libs/actions/Device/generateDeviceID/index.js rename to src/libs/actions/Device/generateDeviceID/index.website.js From 412e6c34908a5781402aa7ecbf1a28d139b0c2ba Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 27 Jan 2023 12:03:37 -0800 Subject: [PATCH 33/38] Fix expensify-common import --- src/libs/actions/Device/generateDeviceID/index.android.js | 2 +- src/libs/actions/Device/generateDeviceID/index.website.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Device/generateDeviceID/index.android.js b/src/libs/actions/Device/generateDeviceID/index.android.js index f4967e148d0b..f61b860bda7d 100644 --- a/src/libs/actions/Device/generateDeviceID/index.android.js +++ b/src/libs/actions/Device/generateDeviceID/index.android.js @@ -1,5 +1,5 @@ import DeviceInfo from 'react-native-device-info'; -import Str from 'expensify-common'; +import Str from 'expensify-common/lib/str'; const deviceID = DeviceInfo.getDeviceId(); const uniqueID = Str.guid(deviceID); diff --git a/src/libs/actions/Device/generateDeviceID/index.website.js b/src/libs/actions/Device/generateDeviceID/index.website.js index f330520befdd..b8abc4734134 100644 --- a/src/libs/actions/Device/generateDeviceID/index.website.js +++ b/src/libs/actions/Device/generateDeviceID/index.website.js @@ -1,4 +1,4 @@ -import Str from 'expensify-common'; +import Str from 'expensify-common/lib/str'; const uniqueID = Str.guid(); From 9bc055800d24e006f5c38ef5ae5bf0b65c38e3ea Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 27 Jan 2023 13:35:38 -0800 Subject: [PATCH 34/38] Fix NetworkTest --- tests/unit/NetworkTest.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js index 0a02132af77e..aae95c8f50cd 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.js @@ -20,6 +20,7 @@ import * as MainQueue from '../../src/libs/Network/MainQueue'; import * as Request from '../../src/libs/Request'; jest.useFakeTimers(); +jest.mock('../../src/libs/Log'); Onyx.init({ keys: ONYXKEYS, From 0b6b42529e05124872b41800e4f7d67d3c0b4a64 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 2 Feb 2023 23:03:14 -0800 Subject: [PATCH 35/38] Remove super bizzare unnecessary import --- src/libs/Notification/PushNotification/index.native.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js index 452fe774753f..2bc7b4ddc5ef 100644 --- a/src/libs/Notification/PushNotification/index.native.js +++ b/src/libs/Notification/PushNotification/index.native.js @@ -148,7 +148,7 @@ function register(accountID) { // When the user logged out and then logged in with a different account // while the app is still in background, we must resubscribe to the report // push notification in order to render the report click behaviour correctly - PushNotification.init(); + init(); Report.subscribeToReportCommentPushNotifications(); } From ba8728fac06702c5cddb2bb9ac923ac568af97a8 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 3 Feb 2023 10:30:11 -0800 Subject: [PATCH 36/38] Remove context --- .../customairshipextender/CustomNotificationProvider.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java index 72040f9dab22..36d3565f64d2 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java @@ -45,9 +45,6 @@ import java.util.concurrent.TimeUnit; public class CustomNotificationProvider extends ReactNotificationProvider { - - private final Context context; - // Resize icons to 100 dp x 100 dp private static final int MAX_ICON_SIZE_DPS = 100; @@ -74,7 +71,6 @@ public class CustomNotificationProvider extends ReactNotificationProvider { public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConfigOptions configOptions) { super(context, configOptions); - this.context = context; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createAndRegisterNotificationChannel(context); } From db124ad264e15314768c009b05937e51a3b6a43d Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 3 Feb 2023 10:40:36 -0800 Subject: [PATCH 37/38] Clarify comments around PUSH_NOTIFICATIONS_ENABLED --- src/ONYXKEYS.js | 2 +- src/libs/Notification/PushNotification/index.native.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 912aadceabf7..fbc386d34e26 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -79,7 +79,7 @@ export default { // Contains the users's block expiration (if they have one) NVP_BLOCKED_FROM_CONCIERGE: 'private_blockedFromConcierge', - // Does this user have push notifications enabled? + // Does this user have push notifications enabled for this device? PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', // Plaid data (access tokens, bank accounts ...) diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js index 2bc7b4ddc5ef..8e3c0283a01e 100644 --- a/src/libs/Notification/PushNotification/index.native.js +++ b/src/libs/Notification/PushNotification/index.native.js @@ -56,7 +56,7 @@ function pushNotificationEventCallback(eventType, notification) { } /** - * Check if a user is opted-in to push notifications and update the `pushNotificationsEnabled` NVP accordingly. + * Check if a user is opted-in to push notifications on this device and update the `pushNotificationsEnabled` NVP accordingly. */ function refreshNotificationOptInStatus() { UrbanAirship.getNotificationStatus() From 8fe9141b94ebeb692dbf1cce3ef79356d285f5f6 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 3 Feb 2023 10:41:28 -0800 Subject: [PATCH 38/38] Remove server-side assumption from comment --- src/libs/actions/PushNotification.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/PushNotification.js b/src/libs/actions/PushNotification.js index 2bc86a878246..168b4fb9e169 100644 --- a/src/libs/actions/PushNotification.js +++ b/src/libs/actions/PushNotification.js @@ -12,7 +12,6 @@ Onyx.connect({ /** * Record that user opted-in or opted-out of push notifications on the current device. - * NOTE: This is purely for record-keeping purposes, and does not affect whether our server will attempt to send notifications to this user. * * @param {Boolean} isOptingIn */