From f81a0d71a286fe952b63ab0ae37b007bd287f68d Mon Sep 17 00:00:00 2001 From: Patriciu Nista Date: Thu, 30 Jan 2025 21:30:58 +0100 Subject: [PATCH 1/7] implement components and logic --- .../app/UI/components/Switch/Switch.tsx | 51 +++++++++++++++++ .../notifications/common/hooks/useStrings.ts | 28 ++++++++++ .../NotificationsSettings.tsx | 56 +++++++++++++++++++ .../app/api/localStorage/index.js | 9 +++ .../settings/categories/WalletSettingsPage.js | 10 ++-- .../app/i18n/locales/en-US.json | 2 + 6 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 packages/yoroi-extension/app/UI/components/Switch/Switch.tsx create mode 100644 packages/yoroi-extension/app/UI/features/notifications/common/hooks/useStrings.ts create mode 100644 packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx diff --git a/packages/yoroi-extension/app/UI/components/Switch/Switch.tsx b/packages/yoroi-extension/app/UI/components/Switch/Switch.tsx new file mode 100644 index 0000000000..9e12a3ff0a --- /dev/null +++ b/packages/yoroi-extension/app/UI/components/Switch/Switch.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import styled from '@emotion/styled'; +import { Switch as MuiSwitch } from '@mui/material'; + +const height = 24; +const width = 44; + +export const Switch: any = styled((props: any) => ( + +))(({ theme }) => ({ + width, + height, + padding: 0, + '& .MuiSwitch-switchBase': { + padding: 0, + margin: 2, + transitionDuration: '300ms', + '&.Mui-checked': { + transform: 'translateX(20px)', + color: '#fff', + '& + .MuiSwitch-track': { + backgroundColor: theme.palette.primary[500], + opacity: 1, + border: 0, + }, + '&.Mui-disabled + .MuiSwitch-track': { + opacity: 0.5, + }, + }, + '&.Mui-focusVisible .MuiSwitch-thumb': { + border: '6px solid #fff', + }, + '&.Mui-disabled .MuiSwitch-thumb': { + color: theme.palette.grey[100], + }, + '&.Mui-disabled + .MuiSwitch-track': { + opacity: 0.7, + }, + }, + '& .MuiSwitch-thumb': { + boxSizing: 'border-box', + }, + '& .MuiSwitch-track': { + borderRadius: height / 2, + backgroundColor: theme.palette.grayscale[100], + opacity: 1, + transition: theme.transitions.create(['background-color'], { + duration: 500, + }), + }, +})); diff --git a/packages/yoroi-extension/app/UI/features/notifications/common/hooks/useStrings.ts b/packages/yoroi-extension/app/UI/features/notifications/common/hooks/useStrings.ts new file mode 100644 index 0000000000..9da5369c7d --- /dev/null +++ b/packages/yoroi-extension/app/UI/features/notifications/common/hooks/useStrings.ts @@ -0,0 +1,28 @@ +import React from 'react'; +import { defineMessages } from 'react-intl'; +import { useIntl } from '../../../../context/IntlProvider'; + +export const messages = Object.freeze( + defineMessages({ + notifSettingsTitle: { + id: 'notifications.settings.title', + defaultMessage: '!!!In-app notifications', + }, + notifSettingsDesc: { + id: 'notifications.settings.description', + defaultMessage: + '!!!Allow display of in-app notifications for key transactions', + }, + }) +); + +export const useStrings = (intl = null) => { + const { intl: contextIntl } = useIntl(); + + const i = intl || contextIntl; + + return React.useRef({ + notifSettingsTitle: i.formatMessage(messages.notifSettingsTitle), + notifSettingsDesc: i.formatMessage(messages.notifSettingsDesc), + }).current; +}; diff --git a/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx b/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx new file mode 100644 index 0000000000..5186232f0d --- /dev/null +++ b/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { Box, FormControlLabel, Typography } from '@mui/material'; +import { useStrings } from '../../common/hooks/useStrings'; +import { Switch } from '../../../../components/Switch/Switch'; +import LocalStorageApi from '../../../../../api/localStorage' + +const NotificationsSettings = ({ intl }) => { + const strings = useStrings(intl); + const [notificationsEnabled, setNotificationsEnabled] = React.useState(false) + + const lsApi = new LocalStorageApi(); + + React.useEffect(() => { + async function getNotifStatus() { + const notifEnabled = await lsApi.getNotificationsSetting(); + if (notifEnabled === "true") { + setNotificationsEnabled(true); + } + } + + getNotifStatus(); + }, []) + + const handleNotificationsChange = (event) => { + setNotificationsEnabled(prev => !prev); + lsApi.setNotificationsSetting(String(event.target.checked)); + } + + return ( + + + {strings.notifSettingsTitle} + + + + + } + labelPlacement="top" + sx={{ + mt: '16px', + marginLeft: '0px', + color: 'ds.text_gray_medium', + gap: '16px' + }} + /> + + ); +}; + +export default NotificationsSettings; \ No newline at end of file diff --git a/packages/yoroi-extension/app/api/localStorage/index.js b/packages/yoroi-extension/app/api/localStorage/index.js index 33a9fe25b8..904e186196 100644 --- a/packages/yoroi-extension/app/api/localStorage/index.js +++ b/packages/yoroi-extension/app/api/localStorage/index.js @@ -33,6 +33,7 @@ const storageKeys = { FLAGS: networkForLocalStorage + '-FLAGS', USER_THEME: networkForLocalStorage + '-USER-THEME', PORTFOLIO_FIAT_PAIR: networkForLocalStorage + '-PORTFOLIO_FIAT_PAIR', + NOTIFICATIONS_SETTING: networkForLocalStorage + '-NOTIFICATIONS_SETTING', BUY_SELL_DISCLAIMER: networkForLocalStorage + '-BUY_SELL_DISCLAIMER', // ========== CONNECTOR ========== // DAPP_CONNECTOR_WHITELIST: 'connector_whitelist', @@ -105,6 +106,14 @@ export default class LocalStorageApi { setSetPortfolioFiatPair: string => Promise = pair => setLocalItem(storageKeys.PORTFOLIO_FIAT_PAIR, pair); unsetPortfolioFiatPair: void => Promise = () => removeLocalItem(storageKeys.PORTFOLIO_FIAT_PAIR); + + // ========== Notifications Setting ========== // + + getNotificationsSetting: void => Promise = () => getLocalItem(storageKeys.NOTIFICATIONS_SETTING); + + setNotificationsSetting: string => Promise = allowed => setLocalItem(storageKeys.NOTIFICATIONS_SETTING, allowed); + + unsetNotificationsSetting: void => Promise = () => removeLocalItem(storageKeys.NOTIFICATIONS_SETTING); // ========== Buy/Sell Disclaimer ========== // getBuySellDisclaimer: void => Promise = () => getLocalItem(storageKeys.BUY_SELL_DISCLAIMER); diff --git a/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js b/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js index ac4f60a878..0839a9345f 100644 --- a/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js +++ b/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js @@ -1,6 +1,8 @@ // @flow -import { Component } from 'react'; import type { Node } from 'react'; +import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; +import type { StoresProps } from '../../../stores'; +import { Component } from 'react'; import { observer } from 'mobx-react'; import WalletNameSetting from '../../../components/wallet/settings/WalletNameSetting'; import NoWalletMessage from '../../wallet/NoWalletMessage'; @@ -15,9 +17,8 @@ import { isValidWalletName } from '../../../utils/validations'; import ChangeWalletPasswordDialogContainer from '../../wallet/dialogs/ChangeWalletPasswordDialogContainer'; import { Typography } from '@mui/material'; import { intlShape } from 'react-intl'; -import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; import globalMessages from '../../../i18n/global-messages'; -import type { StoresProps } from '../../../stores'; +import NotificationsSettings from '../../../UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings' @observer export default class WalletSettingsPage extends Component { @@ -70,8 +71,9 @@ export default class WalletSettingsPage extends Component { activeField={walletFieldBeingEdited} nameValidator={name => isValidWalletName(name)} /> + {selectedWallet.type === 'mnemonic' && ( - stores.uiDialogs.open({ dialog: ChangeWalletPasswordDialogContainer, diff --git a/packages/yoroi-extension/app/i18n/locales/en-US.json b/packages/yoroi-extension/app/i18n/locales/en-US.json index fc39b157a2..7d6517be1e 100644 --- a/packages/yoroi-extension/app/i18n/locales/en-US.json +++ b/packages/yoroi-extension/app/i18n/locales/en-US.json @@ -340,6 +340,8 @@ "settings.unitOfAccount.note": "Note: coin price is approximate and may not match the price of any given trading platform. Any transactions based on these price approximates are done at your own risk.", "settings.unitOfAccount.revamp.label": "Select currency", "settings.unitOfAccount.title": "Fiat pairing", + "notifications.settings.title": "In-app notifications", + "notifications.settings.description": "Allow display of in-app notifications for key transactions", "sidebar.assets": "Assets", "sidebar.faq": "Faq", "sidebar.feedback": "Feedback", From 3598c3f8bb15294c7d380ec61342a29a5c948d49 Mon Sep 17 00:00:00 2001 From: Patriciu Nista Date: Thu, 30 Jan 2025 21:33:50 +0100 Subject: [PATCH 2/7] fix flow errors --- .../app/containers/settings/categories/WalletSettingsPage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js b/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js index 0839a9345f..fc8529dd5d 100644 --- a/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js +++ b/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js @@ -18,6 +18,7 @@ import ChangeWalletPasswordDialogContainer from '../../wallet/dialogs/ChangeWall import { Typography } from '@mui/material'; import { intlShape } from 'react-intl'; import globalMessages from '../../../i18n/global-messages'; +// $FlowIgnore: suppressing this error import NotificationsSettings from '../../../UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings' @observer From 49d48c7be04252cd0defa97012f64b8a954f86c5 Mon Sep 17 00:00:00 2001 From: Patriciu Nista Date: Thu, 30 Jan 2025 21:55:45 +0100 Subject: [PATCH 3/7] wip ampli --- .../NotificationsSettings/NotificationsSettings.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx b/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx index 5186232f0d..e548aff591 100644 --- a/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx +++ b/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import { Box, FormControlLabel, Typography } from '@mui/material'; import { useStrings } from '../../common/hooks/useStrings'; import { Switch } from '../../../../components/Switch/Switch'; -import LocalStorageApi from '../../../../../api/localStorage' - +import LocalStorageApi from '../../../../../api/localStorage'; +// import { ampli } from '../../../../../../ampli'; const NotificationsSettings = ({ intl }) => { const strings = useStrings(intl); const [notificationsEnabled, setNotificationsEnabled] = React.useState(false) @@ -24,6 +24,9 @@ const NotificationsSettings = ({ intl }) => { const handleNotificationsChange = (event) => { setNotificationsEnabled(prev => !prev); lsApi.setNotificationsSetting(String(event.target.checked)); + + // TODO: pull amplitude metrics + // ampli.settingsInAppNotificationsStatusUpdated() } return ( From 21d66ff727270582cef2de52c2b7ce84d776ddbd Mon Sep 17 00:00:00 2001 From: Patriciu Nista Date: Mon, 3 Feb 2025 10:46:33 +0100 Subject: [PATCH 4/7] add analytics event --- .../NotificationsSettings/NotificationsSettings.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx b/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx index e548aff591..f50d442bf4 100644 --- a/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx +++ b/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx @@ -3,17 +3,18 @@ import { Box, FormControlLabel, Typography } from '@mui/material'; import { useStrings } from '../../common/hooks/useStrings'; import { Switch } from '../../../../components/Switch/Switch'; import LocalStorageApi from '../../../../../api/localStorage'; -// import { ampli } from '../../../../../../ampli'; +import { ampli } from '../../../../../../ampli'; + const NotificationsSettings = ({ intl }) => { const strings = useStrings(intl); - const [notificationsEnabled, setNotificationsEnabled] = React.useState(false) + const [notificationsEnabled, setNotificationsEnabled] = React.useState(true) const lsApi = new LocalStorageApi(); React.useEffect(() => { async function getNotifStatus() { const notifEnabled = await lsApi.getNotificationsSetting(); - if (notifEnabled === "true") { + if (notifEnabled === "true" && !notificationsEnabled) { setNotificationsEnabled(true); } } @@ -25,8 +26,9 @@ const NotificationsSettings = ({ intl }) => { setNotificationsEnabled(prev => !prev); lsApi.setNotificationsSetting(String(event.target.checked)); - // TODO: pull amplitude metrics - // ampli.settingsInAppNotificationsStatusUpdated() + ampli.settingsInAppNotificationsStatusUpdated({ + status: event.target.checked ? "enabled" : "disabled" + }) } return ( From 937a505b7f55160da7607865da809539a8617809 Mon Sep 17 00:00:00 2001 From: Patriciu Nista Date: Mon, 3 Feb 2025 11:41:15 +0100 Subject: [PATCH 5/7] YOEXT-1706 fix toggle styles --- .../yoroi-extension/app/UI/components/Switch/Switch.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/yoroi-extension/app/UI/components/Switch/Switch.tsx b/packages/yoroi-extension/app/UI/components/Switch/Switch.tsx index 9e12a3ff0a..a23ebcdd11 100644 --- a/packages/yoroi-extension/app/UI/components/Switch/Switch.tsx +++ b/packages/yoroi-extension/app/UI/components/Switch/Switch.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import styled from '@emotion/styled'; import { Switch as MuiSwitch } from '@mui/material'; -const height = 24; -const width = 44; +const height = 31; +const width = 51; export const Switch: any = styled((props: any) => ( @@ -13,7 +13,7 @@ export const Switch: any = styled((props: any) => ( padding: 0, '& .MuiSwitch-switchBase': { padding: 0, - margin: 2, + margin: 3, transitionDuration: '300ms', '&.Mui-checked': { transform: 'translateX(20px)', @@ -39,6 +39,8 @@ export const Switch: any = styled((props: any) => ( }, '& .MuiSwitch-thumb': { boxSizing: 'border-box', + width: '25px', + height: '25px' }, '& .MuiSwitch-track': { borderRadius: height / 2, From 068f64d5ce5fef1559d1451df7f9d5e1cf23ddcc Mon Sep 17 00:00:00 2001 From: Patriciu Nista Date: Mon, 3 Feb 2025 13:09:20 +0100 Subject: [PATCH 6/7] add setting per wallet --- .../NotificationsSettings.tsx | 41 ++++++++++++++----- .../app/api/localStorage/index.js | 8 ++-- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx b/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx index 5186232f0d..f2b6b55953 100644 --- a/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx +++ b/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx @@ -6,24 +6,45 @@ import LocalStorageApi from '../../../../../api/localStorage' const NotificationsSettings = ({ intl }) => { const strings = useStrings(intl); - const [notificationsEnabled, setNotificationsEnabled] = React.useState(false) + const [notificationsEnabled, setNotificationsEnabled] = React.useState(true); + const [selectedWalletId, setSelectedWalletId] = React.useState(""); const lsApi = new LocalStorageApi(); + async function getNotificationsSetting(checkCurrentWallet: boolean = false) { + const notifSettingsStr = await lsApi.getNotificationsSetting(); + const notifSettings = JSON.parse(notifSettingsStr || "{}"); + + if (checkCurrentWallet) { + const selectedWalletId = await lsApi.getSelectedWalletId(); + setSelectedWalletId(selectedWalletId); + + return notifSettings[selectedWalletId] !== undefined ? notifSettings[selectedWalletId] : true; + } + + return notifSettings; + } + + async function setNotificationsSetting(enabled: boolean) { + const notifSettings = await getNotificationsSetting(); + lsApi.setNotificationsSetting(JSON.stringify({ ...notifSettings, [selectedWalletId]: enabled })); + } + + // get initial state from localstorage React.useEffect(() => { - async function getNotifStatus() { - const notifEnabled = await lsApi.getNotificationsSetting(); - if (notifEnabled === "true") { - setNotificationsEnabled(true); - } + async function initialNotifStatus() { + const notifEnabled = await getNotificationsSetting(true); + setNotificationsEnabled(notifEnabled); } - getNotifStatus(); + initialNotifStatus(); }, []) - const handleNotificationsChange = (event) => { - setNotificationsEnabled(prev => !prev); - lsApi.setNotificationsSetting(String(event.target.checked)); + // handle checkbox change event + const handleNotificationsChange = async (event) => { + const enabled = event.target.checked; + setNotificationsEnabled(enabled); + setNotificationsSetting(enabled); } return ( diff --git a/packages/yoroi-extension/app/api/localStorage/index.js b/packages/yoroi-extension/app/api/localStorage/index.js index 904e186196..0335d11ef6 100644 --- a/packages/yoroi-extension/app/api/localStorage/index.js +++ b/packages/yoroi-extension/app/api/localStorage/index.js @@ -33,7 +33,7 @@ const storageKeys = { FLAGS: networkForLocalStorage + '-FLAGS', USER_THEME: networkForLocalStorage + '-USER-THEME', PORTFOLIO_FIAT_PAIR: networkForLocalStorage + '-PORTFOLIO_FIAT_PAIR', - NOTIFICATIONS_SETTING: networkForLocalStorage + '-NOTIFICATIONS_SETTING', + NOTIFICATIONS_ENABLED: networkForLocalStorage + '-NOTIFICATIONS_ENABLED_PER_WALLET', BUY_SELL_DISCLAIMER: networkForLocalStorage + '-BUY_SELL_DISCLAIMER', // ========== CONNECTOR ========== // DAPP_CONNECTOR_WHITELIST: 'connector_whitelist', @@ -109,11 +109,11 @@ export default class LocalStorageApi { // ========== Notifications Setting ========== // - getNotificationsSetting: void => Promise = () => getLocalItem(storageKeys.NOTIFICATIONS_SETTING); + getNotificationsSetting: void => Promise = () => getLocalItem(storageKeys.NOTIFICATIONS_ENABLED); - setNotificationsSetting: string => Promise = allowed => setLocalItem(storageKeys.NOTIFICATIONS_SETTING, allowed); + setNotificationsSetting: string => Promise = allowed => setLocalItem(storageKeys.NOTIFICATIONS_ENABLED, allowed); - unsetNotificationsSetting: void => Promise = () => removeLocalItem(storageKeys.NOTIFICATIONS_SETTING); + unsetNotificationsSetting: void => Promise = () => removeLocalItem(storageKeys.NOTIFICATIONS_ENABLED); // ========== Buy/Sell Disclaimer ========== // getBuySellDisclaimer: void => Promise = () => getLocalItem(storageKeys.BUY_SELL_DISCLAIMER); From 64d75223467d57db4716eca770b36785276ef9a6 Mon Sep 17 00:00:00 2001 From: Patriciu Nista Date: Tue, 4 Feb 2025 09:36:12 +0100 Subject: [PATCH 7/7] add feature flag for notifications --- .../containers/settings/categories/WalletSettingsPage.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js b/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js index fc8529dd5d..4d615111f8 100644 --- a/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js +++ b/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js @@ -20,6 +20,7 @@ import { intlShape } from 'react-intl'; import globalMessages from '../../../i18n/global-messages'; // $FlowIgnore: suppressing this error import NotificationsSettings from '../../../UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings' +import environment from '../../../environment'; @observer export default class WalletSettingsPage extends Component { @@ -33,6 +34,8 @@ export default class WalletSettingsPage extends Component { const { walletSettings } = stores; const { renameModelRequest, lastUpdatedWalletField, walletFieldBeingEdited } = walletSettings; + const notifFeatFlagEnabled = environment.isDev(); + const { selected: selectedWallet, selectedWalletName } = this.props.stores.wallets; if (selectedWallet == null) { return ( @@ -72,7 +75,9 @@ export default class WalletSettingsPage extends Component { activeField={walletFieldBeingEdited} nameValidator={name => isValidWalletName(name)} /> - + {notifFeatFlagEnabled && ( + + )} {selectedWallet.type === 'mnemonic' && (