diff --git a/__mocks__/urbanairship-react-native.js b/__mocks__/urbanairship-react-native.js index efbb5d87f92b..ba380b6d4a72 100644 --- a/__mocks__/urbanairship-react-native.js +++ b/__mocks__/urbanairship-react-native.js @@ -3,6 +3,14 @@ const EventType = { PushReceived: 'pushReceived', }; +const iOS = { + ForegroundPresentationOption: { + Alert: 'alert', + Sound: 'sound', + Badge: 'badge', + }, +}; + const UrbanAirship = { setUserNotificationsEnabled: jest.fn(), clearNotifications: jest.fn(), @@ -12,11 +20,13 @@ const UrbanAirship = { setNamedUser: jest.fn(), removeAllListeners: jest.fn(), setBadgeNumber: jest.fn(), + setForegroundPresentationOptions: jest.fn(), }; export default UrbanAirship; export { EventType, + iOS, UrbanAirship, }; diff --git a/config/webpack/webpack.desktop.js b/config/webpack/webpack.desktop.js index 298741eed498..86012e4bf738 100644 --- a/config/webpack/webpack.desktop.js +++ b/config/webpack/webpack.desktop.js @@ -50,6 +50,20 @@ module.exports = (env) => { __dirname: false, __filename: false, }, + module: { + rules: [ + { + test: /react-native-onyx/, + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-react'], + + }, + }, + }, + ], + }, }; return [mainProcessConfig, rendererConfig]; diff --git a/desktop/ELECTRON_EVENTS.js b/desktop/ELECTRON_EVENTS.js index 34643447d7da..f0d9b865b01d 100644 --- a/desktop/ELECTRON_EVENTS.js +++ b/desktop/ELECTRON_EVENTS.js @@ -7,6 +7,7 @@ const ELECTRON_EVENTS = { 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 c49aa5d9e9fb..f065c4caac21 100644 --- a/desktop/contextBridge.js +++ b/desktop/contextBridge.js @@ -10,6 +10,7 @@ const WHITELIST_CHANNELS_RENDERER_TO_MAIN = [ ELECTRON_EVENTS.REQUEST_UPDATE_BADGE_COUNT, ELECTRON_EVENTS.REQUEST_VISIBILITY, ELECTRON_EVENTS.START_UPDATE, + ELECTRON_EVENTS.LOCALE_UPDATED, ]; const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ diff --git a/desktop/main.js b/desktop/main.js index da5e13e91e22..3ca61aa318b4 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -15,6 +15,8 @@ const log = require('electron-log'); const ELECTRON_EVENTS = require('./ELECTRON_EVENTS'); const checkForUpdates = require('../src/libs/checkForUpdates'); const CONFIG = require('../src/CONFIG').default; +const CONST = require('../src/CONST'); +const Localize = require('../src/libs/Localize'); const port = process.env.PORT || 8080; @@ -73,6 +75,11 @@ for (let i = 0; i < process.argv.length; i++) { let hasUpdate = false; let downloadedVersion; +// Note that we have to subscribe to this separately and cannot use Localize.translateLocal, +// because the only way code can be shared between the main and renderer processes at runtime is via the context bridge +// So we track preferredLocale separately via ELECTRON_EVENTS.LOCALE_UPDATED +let preferredLocale = CONST.DEFAULT_LOCALE; + const quitAndInstallWithUpdate = () => { if (!downloadedVersion) { return; @@ -100,23 +107,23 @@ const manuallyCheckForUpdates = (menuItem, browserWindow) => { if (downloadPromise) { dialog.showMessageBox(browserWindow, { type: 'info', - message: 'Update Available', - detail: 'The new version will be available shortly. We’ll notify you when we’re ready to update.', - buttons: ['Sounds good'], + message: Localize.translate(preferredLocale, 'checkForUpdatesModal.available.title'), + detail: Localize.translate(preferredLocale, 'checkForUpdatesModal.available.message'), + buttons: [Localize.translate(preferredLocale, 'checkForUpdatesModal.available.soundsGood')], }); } else if (result && result.error) { dialog.showMessageBox(browserWindow, { type: 'error', - message: 'Update Check Failed', - detail: 'We couldn’t look for an update. Please check again in a bit!', - buttons: ['Okay'], + message: Localize.translate(preferredLocale, 'checkForUpdatesModal.error.title'), + detail: Localize.translate(preferredLocale, 'checkForUpdatesModal.error.message'), + buttons: [Localize.translate(preferredLocale, 'checkForUpdatesModal.notAvailable.okay')], }); } else { dialog.showMessageBox(browserWindow, { type: 'info', - message: 'Update Not Available', - detail: 'There is no update available as of now! Check again at a later time.', - buttons: ['Okay'], + message: Localize.translate(preferredLocale, 'checkForUpdatesModal.notAvailable.title'), + detail: Localize.translate(preferredLocale, 'checkForUpdatesModal.notAvailable.message'), + buttons: [Localize.translate(preferredLocale, 'checkForUpdatesModal.notAvailable.okay')], cancelId: 2, }); } @@ -141,34 +148,14 @@ const showKeyboardShortcutsModal = (browserWindow) => { browserWindow.webContents.send(ELECTRON_EVENTS.SHOW_KEYBOARD_SHORTCUTS_MODAL); }; -// Defines the system-level menu item to manually apply an update -// This menu item should become visible after an update is downloaded and ready to be applied -const updateAppMenuItem = new MenuItem({ - label: 'Update New Expensify', - visible: false, - click: quitAndInstallWithUpdate, -}); - -// System-level menu item to manually check for App updates -const checkForUpdateMenuItem = new MenuItem({ - label: 'Check For Updates', - visible: true, - click: manuallyCheckForUpdates, -}); - -// Defines the system-level menu item for opening keyboard shortcuts modal -const keyboardShortcutsMenu = new MenuItem({ - label: 'View Keyboard Shortcuts', - accelerator: 'CmdOrCtrl+I', -}); - // Actual auto-update listeners const electronUpdater = browserWindow => ({ init: () => { autoUpdater.on(ELECTRON_EVENTS.UPDATE_DOWNLOADED, (info) => { + const systemMenu = Menu.getApplicationMenu(); downloadedVersion = info.version; - updateAppMenuItem.visible = true; - checkForUpdateMenuItem.visible = false; + systemMenu.getMenuItemById(`updateAppMenuItem-${preferredLocale}`).visible = true; + systemMenu.getMenuItemById(`checkForUpdateMenuItem-${preferredLocale}`).visible = false; if (browserWindow.isVisible()) { browserWindow.webContents.send(ELECTRON_EVENTS.UPDATE_DOWNLOADED, info.version); } else { @@ -184,6 +171,61 @@ const electronUpdater = browserWindow => ({ }, }); +/* +* @param {Menu} systemMenu +*/ +const localizeMenuItems = (browserWindow, systemMenu) => { + // List the Expensify Chat instance under the Window menu, even when it's hidden + systemMenu.insert(4, new MenuItem({ + id: `historyMenuItem-${preferredLocale}`, + label: Localize.translate(preferredLocale, 'desktopApplicationMenu.history'), + submenu: [{ + id: `backMenuItem-${preferredLocale}`, + label: Localize.translate(preferredLocale, 'historyMenu.back'), + accelerator: process.platform === 'darwin' ? 'Cmd+[' : 'Shift+[', + click: () => { browserWindow.webContents.goBack(); }, + }, + { + id: `forwardMenuItem-${preferredLocale}`, + label: Localize.translate(preferredLocale, 'historyMenu.forward'), + accelerator: process.platform === 'darwin' ? 'Cmd+]' : 'Shift+]', + click: () => { browserWindow.webContents.goForward(); }, + }], + })); + + // Defines the system-level menu item to manually apply an update + // This menu item should become visible after an update is downloaded and ready to be applied + const updateAppMenuItem = new MenuItem({ + id: `updateAppMenuItem-${preferredLocale}`, + label: Localize.translate(preferredLocale, 'desktopApplicationMenu.updateExpensify'), + visible: false, + click: quitAndInstallWithUpdate, + }); + + // System-level menu item to manually check for App updates + const checkForUpdateMenuItem = new MenuItem({ + id: `checkForUpdateMenuItem-${preferredLocale}`, + label: Localize.translate(preferredLocale, 'desktopApplicationMenu.checkForUpdates'), + visible: true, + click: manuallyCheckForUpdates, + }); + + // Defines the system-level menu item for opening keyboard shortcuts modal + const keyboardShortcutsMenuItem = new MenuItem({ + id: `keyboardShortcutsMenuItem-${preferredLocale}`, + label: Localize.translate(preferredLocale, 'initialSettingsPage.aboutPage.viewKeyboardShortcuts'), + accelerator: 'CmdOrCtrl+I', + click: () => { + showKeyboardShortcutsModal(browserWindow); + }, + }); + + const appMenu = _.find(systemMenu.items, item => item.role === 'appmenu'); + appMenu.submenu.insert(1, updateAppMenuItem); + appMenu.submenu.insert(2, checkForUpdateMenuItem); + appMenu.submenu.insert(3, keyboardShortcutsMenuItem); +}; + const mainWindow = (() => { const loadURL = __DEV__ ? win => win.loadURL(`http://localhost:${port}`) @@ -256,27 +298,7 @@ const mainWindow = (() => { browserWindow.setTitle('New Expensify'); } - keyboardShortcutsMenu.click = () => { - showKeyboardShortcutsModal(browserWindow); - }; - - // List the Expensify Chat instance under the Window menu, even when it's hidden const systemMenu = Menu.getApplicationMenu(); - systemMenu.insert(4, new MenuItem({ - label: 'History', - submenu: [{ - role: 'back', - label: 'Back', - accelerator: process.platform === 'darwin' ? 'Cmd+[' : 'Shift+[', - click: () => { browserWindow.webContents.goBack(); }, - }, - { - role: 'forward', - label: 'Forward', - accelerator: process.platform === 'darwin' ? 'Cmd+]' : 'Shift+]', - click: () => { browserWindow.webContents.goForward(); }, - }], - })); // Register the custom Paste and Match Style command and place it near the default shortcut of the same role. const editMenu = _.find(systemMenu.items, item => item.role === 'editmenu'); @@ -285,10 +307,8 @@ const mainWindow = (() => { accelerator: 'CmdOrCtrl+Shift+V', })); - const appMenu = _.find(systemMenu.items, item => item.role === 'appmenu'); - appMenu.submenu.insert(1, updateAppMenuItem); - appMenu.submenu.insert(2, checkForUpdateMenuItem); - appMenu.submenu.insert(3, keyboardShortcutsMenu); + // Append custom context menu items using the preferred language of the user. + localizeMenuItems(browserWindow, systemMenu); // On mac, pressing cmd++ actually sends a cmd+=. cmd++ is generally the zoom in shortcut, but this is // not properly listened for by electron. Adding in an invisible cmd+= listener fixes this. @@ -374,6 +394,37 @@ const mainWindow = (() => { app.hide(); } + ipcMain.on(ELECTRON_EVENTS.LOCALE_UPDATED, (event, updatedLocale) => { + // Store the old locale so we can hide/remove these items after adding/showing the new ones. + const outdatedLocale = preferredLocale; + preferredLocale = updatedLocale; + + const currentHistoryMenuItem = systemMenu.getMenuItemById(`historyMenuItem-${outdatedLocale}`); + const currentUpdateAppMenuItem = systemMenu.getMenuItemById(`updateAppMenuItem-${outdatedLocale}`); + const currentCheckForUpdateMenuItem = systemMenu.getMenuItemById(`checkForUpdateMenuItem-${outdatedLocale}`); + const currentKeyboardShortcutsMenuItem = systemMenu.getMenuItemById(`keyboardShortcutsMenuItem-${outdatedLocale}`); + + // If we have previously added those languages, don't add new menu items, reshow them. + if (!systemMenu.getMenuItemById(`updateAppMenuItem-${updatedLocale}`)) { + // Update the labels and ids to use the translations. + localizeMenuItems(browserWindow, systemMenu); + } + + // Show the localized menu items if there were visible before we updated the locale. + systemMenu.getMenuItemById(`updateAppMenuItem-${updatedLocale}`).visible = currentUpdateAppMenuItem.visible; + systemMenu.getMenuItemById(`checkForUpdateMenuItem-${updatedLocale}`).visible = currentCheckForUpdateMenuItem.visible; + systemMenu.getMenuItemById(`keyboardShortcutsMenuItem-${updatedLocale}`).visible = currentKeyboardShortcutsMenuItem.visible; + systemMenu.getMenuItemById(`historyMenuItem-${updatedLocale}`).visible = currentHistoryMenuItem.visible; + + // Since we can't remove menu items, we hide the old ones. + currentUpdateAppMenuItem.visible = false; + currentCheckForUpdateMenuItem.visible = false; + currentKeyboardShortcutsMenuItem.visible = false; + currentHistoryMenuItem.visible = false; + + Menu.setApplicationMenu(systemMenu); + }); + ipcMain.on(ELECTRON_EVENTS.REQUEST_VISIBILITY, (event) => { // This is how synchronous messages work in Electron // eslint-disable-next-line no-param-reassign diff --git a/package-lock.json b/package-lock.json index 2c9854336b88..54b0c3234ef7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -131,7 +131,7 @@ "copy-webpack-plugin": "^6.4.1", "css-loader": "^5.2.4", "diff-so-fancy": "^1.3.0", - "electron": "^21.0.0", + "electron": "^21.2.2", "electron-builder": "23.5.0", "electron-notarize": "^1.2.1", "eslint": "^7.6.0", @@ -21398,9 +21398,9 @@ } }, "node_modules/electron": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-21.1.1.tgz", - "integrity": "sha512-EM2hvRJtiS3n54yx25Z0Qv54t3LGG+WjUHf1AOl+PKjQj+fmXnjIgVeIF9pM21kP1BTcyjrgvN6Sff0A45OB6A==", + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-21.2.2.tgz", + "integrity": "sha512-Q0j1tzLTM5JRjSJVAfDSONZgdtuyruHR1pc1y2IbMYQz62pVJWVWAvcJXzpty5iRh2HKzW9+B9WVlmfWNFA8ag==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -58969,9 +58969,9 @@ } }, "electron": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-21.1.1.tgz", - "integrity": "sha512-EM2hvRJtiS3n54yx25Z0Qv54t3LGG+WjUHf1AOl+PKjQj+fmXnjIgVeIF9pM21kP1BTcyjrgvN6Sff0A45OB6A==", + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-21.2.2.tgz", + "integrity": "sha512-Q0j1tzLTM5JRjSJVAfDSONZgdtuyruHR1pc1y2IbMYQz62pVJWVWAvcJXzpty5iRh2HKzW9+B9WVlmfWNFA8ag==", "dev": true, "requires": { "@electron/get": "^1.14.1", diff --git a/package.json b/package.json index f8d37ad9b841..47e984a63f4e 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,7 @@ "copy-webpack-plugin": "^6.4.1", "css-loader": "^5.2.4", "diff-so-fancy": "^1.3.0", - "electron": "^21.0.0", + "electron": "^21.2.2", "electron-builder": "23.5.0", "electron-notarize": "^1.2.1", "eslint": "^7.6.0", diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index a9f6fc360da4..4d47a0f405e8 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -272,7 +272,7 @@ class AvatarWithImagePicker extends React.Component { src={Expensicons.Camera} width={variables.iconSizeSmall} height={variables.iconSizeSmall} - fill={themeColors.iconReversed} + fill={themeColors.textLight} /> diff --git a/src/components/ConfirmationPage.js b/src/components/ConfirmationPage.js new file mode 100644 index 000000000000..1162c81506a9 --- /dev/null +++ b/src/components/ConfirmationPage.js @@ -0,0 +1,77 @@ +import React from 'react'; +import {Image, View} from 'react-native'; +import PropTypes from 'prop-types'; +import Text from './Text'; +import styles from '../styles/styles'; +import Button from './Button'; +import FixedFooter from './FixedFooter'; +import CONST from '../CONST'; + +const propTypes = { + /** The asset to render */ + illustration: PropTypes.string, + + /** Heading of the confirmation page */ + heading: PropTypes.string, + + /** Description of the confirmation page */ + description: PropTypes.string, + + /** The text for the button label */ + buttonText: PropTypes.string, + + /** A function that is called when the button is clicked on */ + onButtonPress: PropTypes.func, + + /** Whether we should show a confirmation button */ + shouldShowButton: PropTypes.bool, +}; + +const defaultProps = { + illustration: `${CONST.CLOUDFRONT_URL}/images/animations/animation__fireworks.gif`, + heading: '', + description: '', + buttonText: '', + onButtonPress: () => {}, + shouldShowButton: false, +}; + +const ConfirmationPage = props => ( + <> + + + + {props.heading} + + + {props.description} + + + {props.shouldShowButton && ( + +