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..a23ebcdd11 --- /dev/null +++ b/packages/yoroi-extension/app/UI/components/Switch/Switch.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import styled from '@emotion/styled'; +import { Switch as MuiSwitch } from '@mui/material'; + +const height = 31; +const width = 51; + +export const Switch: any = styled((props: any) => ( + +))(({ theme }) => ({ + width, + height, + padding: 0, + '& .MuiSwitch-switchBase': { + padding: 0, + margin: 3, + 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', + width: '25px', + height: '25px' + }, + '& .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..2e588a24b5 --- /dev/null +++ b/packages/yoroi-extension/app/UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings.tsx @@ -0,0 +1,81 @@ +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 { ampli } from '../../../../../../ampli'; + +const NotificationsSettings = ({ intl }) => { + const strings = useStrings(intl); + 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 initialNotifStatus() { + const notifEnabled = await getNotificationsSetting(true); + setNotificationsEnabled(notifEnabled); + } + + initialNotifStatus(); + }, []) + + // handle checkbox change event + const handleNotificationsChange = async (event) => { + const enabled = event.target.checked; + setNotificationsEnabled(enabled); + setNotificationsSetting(enabled); + ampli.settingsInAppNotificationsStatusUpdated({ + status: event.target.checked ? "enabled" : "disabled" + }) + } + + 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..0335d11ef6 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_ENABLED: networkForLocalStorage + '-NOTIFICATIONS_ENABLED_PER_WALLET', 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_ENABLED); + + setNotificationsSetting: string => Promise = allowed => setLocalItem(storageKeys.NOTIFICATIONS_ENABLED, allowed); + + unsetNotificationsSetting: void => Promise = () => removeLocalItem(storageKeys.NOTIFICATIONS_ENABLED); // ========== 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..4d615111f8 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,10 @@ 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'; +// $FlowIgnore: suppressing this error +import NotificationsSettings from '../../../UI/features/notifications/useCases/NotificationsSettings/NotificationsSettings' +import environment from '../../../environment'; @observer export default class WalletSettingsPage extends Component { @@ -31,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 ( @@ -70,8 +75,11 @@ export default class WalletSettingsPage extends Component { activeField={walletFieldBeingEdited} nameValidator={name => isValidWalletName(name)} /> + {notifFeatFlagEnabled && ( + + )} {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 34a1d9d7b6..96f16de246 100644 --- a/packages/yoroi-extension/app/i18n/locales/en-US.json +++ b/packages/yoroi-extension/app/i18n/locales/en-US.json @@ -341,6 +341,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",