diff --git a/web/cashtab/src/components/Common/EnhancedInputs.js b/web/cashtab/src/components/Common/EnhancedInputs.js --- a/web/cashtab/src/components/Common/EnhancedInputs.js +++ b/web/cashtab/src/components/Common/EnhancedInputs.js @@ -115,6 +115,7 @@ onMax, inputProps, selectProps, + activeFiatCode, ...otherProps }) => { const { Option } = Select; @@ -123,7 +124,10 @@ value: currency.ticker, label: currency.ticker, }, - { value: 'USD', label: 'USD' }, + { + value: activeFiatCode ? activeFiatCode : 'USD', + label: activeFiatCode ? activeFiatCode : 'USD', + }, ]; const currencyOptions = currencies.map(currency => { return ( @@ -246,6 +250,46 @@ ); }; +export const CurrencySelectDropdown = selectProps => { + const { Option } = Select; + + // Build select dropdown from currency.fiatCurrencies + const currencyMenuOptions = []; + const currencyKeys = Object.keys(currency.fiatCurrencies); + for (let i = 0; i < currencyKeys.length; i += 1) { + const currencyMenuOption = {}; + currencyMenuOption.value = + currency.fiatCurrencies[currencyKeys[i]].slug; + currencyMenuOption.label = `${ + currency.fiatCurrencies[currencyKeys[i]].name + } (${currency.fiatCurrencies[currencyKeys[i]].symbol})`; + currencyMenuOptions.push(currencyMenuOption); + } + const currencyOptions = currencyMenuOptions.map(currencyMenuOption => { + return ( + <Option + key={currencyMenuOption.value} + value={currencyMenuOption.value} + className="selectedCurrencyOption" + > + {currencyMenuOption.label} + </Option> + ); + }); + + return ( + <Select + className="select-after" + style={{ + width: '100%', + }} + {...selectProps} + > + {currencyOptions} + </Select> + ); +}; + export const AddressValidators = () => { const { BCH } = useBCH(); diff --git a/web/cashtab/src/components/Common/Ticker.js b/web/cashtab/src/components/Common/Ticker.js --- a/web/cashtab/src/components/Common/Ticker.js +++ b/web/cashtab/src/components/Common/Ticker.js @@ -24,6 +24,14 @@ useBlockchainWs: false, txHistoryCount: 5, hydrateUtxoBatchSize: 20, + defaultSettings: { fiatCurrency: 'usd' }, + settingsValidation: { fiatCurrency: ['usd', 'idr', 'krw', 'cny'] }, + fiatCurrencies: { + usd: { name: 'US Dollar', symbol: '$', slug: 'usd' }, + idr: { name: 'Indonesian Rupiah', symbol: 'Rp', slug: 'idr' }, + krw: { name: 'Korean Won', symbol: '₩', slug: 'krw' }, + cny: { name: 'Chinese Yuan', symbol: '元', slug: 'cny' }, + }, }; export function isValidCashPrefix(addressString) { diff --git a/web/cashtab/src/components/Configure/Configure.js b/web/cashtab/src/components/Configure/Configure.js --- a/web/cashtab/src/components/Configure/Configure.js +++ b/web/cashtab/src/components/Configure/Configure.js @@ -10,7 +10,10 @@ } from '@ant-design/icons'; import { WalletContext } from '@utils/context'; import { StyledCollapse } from '@components/Common/StyledCollapse'; -import { AntdFormWrapper } from '@components/Common/EnhancedInputs'; +import { + AntdFormWrapper, + CurrencySelectDropdown, +} from '@components/Common/EnhancedInputs'; import PrimaryButton, { SecondaryButton, SmartButton, @@ -20,6 +23,7 @@ CashLoadingIcon, ThemedCopyOutlined, ThemedWalletOutlined, + ThemedDollarOutlined, } from '@components/Common/CustomIcons'; import { ReactComponent as Trashcan } from '@assets/trashcan.svg'; import { ReactComponent as Edit } from '@assets/edit.svg'; @@ -172,6 +176,8 @@ deleteWallet, validateMnemonic, getSavedWallets, + cashtabSettings, + changeCashtabSettings, } = ContextValue; const [savedWallets, setSavedWallets] = useState([]); const [formData, setFormData] = useState({ @@ -582,6 +588,22 @@ </StyledCollapse> </> )} + <StyledSpacer /> + <h2> + <ThemedDollarOutlined /> Fiat Currency + </h2> + <AntdFormWrapper> + <CurrencySelectDropdown + defaultValue={ + cashtabSettings && cashtabSettings.fiatCurrency + ? cashtabSettings.fiatCurrency + : 'usd' + } + onChange={fiatCode => + changeCashtabSettings('fiatCurrency', fiatCode) + } + /> + </AntdFormWrapper> <StyledSpacer />[ <SettingsLink type="link" diff --git a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap --- a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap +++ b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap @@ -221,6 +221,123 @@ <div className="sc-kpOJdX eglhol" /> + <h2> + <span + aria-label="dollar" + className="anticon anticon-dollar sc-bxivhb ctLIog" + role="img" + > + <svg + aria-hidden="true" + data-icon="dollar" + fill="currentColor" + focusable="false" + height="1em" + viewBox="64 64 896 896" + width="1em" + > + <path + d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm47.7-395.2l-25.4-5.9V348.6c38 5.2 61.5 29 65.5 58.2.5 4 3.9 6.9 7.9 6.9h44.9c4.7 0 8.4-4.1 8-8.8-6.1-62.3-57.4-102.3-125.9-109.2V263c0-4.4-3.6-8-8-8h-28.1c-4.4 0-8 3.6-8 8v33c-70.8 6.9-126.2 46-126.2 119 0 67.6 49.8 100.2 102.1 112.7l24.7 6.3v142.7c-44.2-5.9-69-29.5-74.1-61.3-.6-3.8-4-6.6-7.9-6.6H363c-4.7 0-8.4 4-8 8.7 4.5 55 46.2 105.6 135.2 112.1V761c0 4.4 3.6 8 8 8h28.4c4.4 0 8-3.6 8-8.1l-.2-31.7c78.3-6.9 134.3-48.8 134.3-124-.1-69.4-44.2-100.4-109-116.4zm-68.6-16.2c-5.6-1.6-10.3-3.1-15-5-33.8-12.2-49.5-31.9-49.5-57.3 0-36.3 27.5-57 64.5-61.7v124zM534.3 677V543.3c3.1.9 5.9 1.6 8.8 2.2 47.3 14.4 63.2 34.4 63.2 65.1 0 39.1-29.4 62.6-72 66.4z" + /> + </svg> + </span> + Fiat Currency + </h2> + <div + className="sc-iwsKbI dmHKdG" + > + <div + className="ant-select select-after ant-select-single ant-select-show-arrow" + onBlur={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} + onMouseDown={[Function]} + style={ + Object { + "width": "100%", + } + } + > + <div + className="ant-select-selector" + onClick={[Function]} + onMouseDown={[Function]} + > + <span + className="ant-select-selection-search" + > + <input + aria-activedescendant="rc_select_TEST_OR_SSR_list_0" + aria-autocomplete="list" + aria-controls="rc_select_TEST_OR_SSR_list" + aria-haspopup="listbox" + aria-owns="rc_select_TEST_OR_SSR_list" + autoComplete="off" + className="ant-select-selection-search-input" + id="rc_select_TEST_OR_SSR" + onChange={[Function]} + onCompositionEnd={[Function]} + onCompositionStart={[Function]} + onKeyDown={[Function]} + onMouseDown={[Function]} + onPaste={[Function]} + readOnly={true} + role="combobox" + style={ + Object { + "opacity": 0, + } + } + type="search" + unselectable="on" + value="" + /> + </span> + <span + className="ant-select-selection-item" + title="US Dollar ($)" + > + US Dollar ($) + </span> + </div> + <span + aria-hidden={true} + className="ant-select-arrow" + onMouseDown={[Function]} + style={ + Object { + "WebkitUserSelect": "none", + "userSelect": "none", + } + } + unselectable="on" + > + <span + aria-label="down" + className="anticon anticon-down ant-select-suffix" + role="img" + > + <svg + aria-hidden="true" + data-icon="down" + fill="currentColor" + focusable="false" + height="1em" + viewBox="64 64 896 896" + width="1em" + > + <path + d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z" + /> + </svg> + </span> + </span> + </div> + </div> + <div + className="sc-kpOJdX eglhol" + /> [ <a className="sc-jzJRlG lcaLuD" @@ -420,6 +537,123 @@ <div className="sc-kpOJdX eglhol" /> + <h2> + <span + aria-label="dollar" + className="anticon anticon-dollar sc-bxivhb ctLIog" + role="img" + > + <svg + aria-hidden="true" + data-icon="dollar" + fill="currentColor" + focusable="false" + height="1em" + viewBox="64 64 896 896" + width="1em" + > + <path + d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm47.7-395.2l-25.4-5.9V348.6c38 5.2 61.5 29 65.5 58.2.5 4 3.9 6.9 7.9 6.9h44.9c4.7 0 8.4-4.1 8-8.8-6.1-62.3-57.4-102.3-125.9-109.2V263c0-4.4-3.6-8-8-8h-28.1c-4.4 0-8 3.6-8 8v33c-70.8 6.9-126.2 46-126.2 119 0 67.6 49.8 100.2 102.1 112.7l24.7 6.3v142.7c-44.2-5.9-69-29.5-74.1-61.3-.6-3.8-4-6.6-7.9-6.6H363c-4.7 0-8.4 4-8 8.7 4.5 55 46.2 105.6 135.2 112.1V761c0 4.4 3.6 8 8 8h28.4c4.4 0 8-3.6 8-8.1l-.2-31.7c78.3-6.9 134.3-48.8 134.3-124-.1-69.4-44.2-100.4-109-116.4zm-68.6-16.2c-5.6-1.6-10.3-3.1-15-5-33.8-12.2-49.5-31.9-49.5-57.3 0-36.3 27.5-57 64.5-61.7v124zM534.3 677V543.3c3.1.9 5.9 1.6 8.8 2.2 47.3 14.4 63.2 34.4 63.2 65.1 0 39.1-29.4 62.6-72 66.4z" + /> + </svg> + </span> + Fiat Currency + </h2> + <div + className="sc-iwsKbI dmHKdG" + > + <div + className="ant-select select-after ant-select-single ant-select-show-arrow" + onBlur={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} + onMouseDown={[Function]} + style={ + Object { + "width": "100%", + } + } + > + <div + className="ant-select-selector" + onClick={[Function]} + onMouseDown={[Function]} + > + <span + className="ant-select-selection-search" + > + <input + aria-activedescendant="rc_select_TEST_OR_SSR_list_0" + aria-autocomplete="list" + aria-controls="rc_select_TEST_OR_SSR_list" + aria-haspopup="listbox" + aria-owns="rc_select_TEST_OR_SSR_list" + autoComplete="off" + className="ant-select-selection-search-input" + id="rc_select_TEST_OR_SSR" + onChange={[Function]} + onCompositionEnd={[Function]} + onCompositionStart={[Function]} + onKeyDown={[Function]} + onMouseDown={[Function]} + onPaste={[Function]} + readOnly={true} + role="combobox" + style={ + Object { + "opacity": 0, + } + } + type="search" + unselectable="on" + value="" + /> + </span> + <span + className="ant-select-selection-item" + title="US Dollar ($)" + > + US Dollar ($) + </span> + </div> + <span + aria-hidden={true} + className="ant-select-arrow" + onMouseDown={[Function]} + style={ + Object { + "WebkitUserSelect": "none", + "userSelect": "none", + } + } + unselectable="on" + > + <span + aria-label="down" + className="anticon anticon-down ant-select-suffix" + role="img" + > + <svg + aria-hidden="true" + data-icon="down" + fill="currentColor" + focusable="false" + height="1em" + viewBox="64 64 896 896" + width="1em" + > + <path + d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z" + /> + </svg> + </span> + </span> + </div> + </div> + <div + className="sc-kpOJdX eglhol" + /> [ <a className="sc-jzJRlG lcaLuD" diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js --- a/web/cashtab/src/components/Send/Send.js +++ b/web/cashtab/src/components/Send/Send.js @@ -39,7 +39,13 @@ // If the wallet object from ContextValue has a `state key`, then check which keys are in the wallet object // Else set it as blank const ContextValue = React.useContext(WalletContext); - const { wallet, fiatPrice, slpBalancesAndUtxos, apiError } = ContextValue; + const { + wallet, + fiatPrice, + slpBalancesAndUtxos, + apiError, + cashtabSettings, + } = ContextValue; let balances; const paramsInWalletState = wallet.state ? Object.keys(wallet.state) : []; // If wallet.state includes balances and parsedTxHistory params, use these @@ -187,7 +193,7 @@ // Calculate the amount in BCH let bchValue = value; - if (selectedCurrency === 'USD') { + if (selectedCurrency !== 'XEC') { bchValue = fiatToCrypto(value, fiatPrice); } @@ -366,9 +372,18 @@ let fiatPriceString = ''; if (fiatPrice !== null && !isNaN(formData.value)) { if (selectedCurrency === currency.ticker) { - fiatPriceString = `$ ${(fiatPrice * Number(formData.value)).toFixed( - 2, - )} USD`; + fiatPriceString = `${ + cashtabSettings + ? `${ + currency.fiatCurrencies[cashtabSettings.fiatCurrency] + .symbol + } ` + : '$ ' + } ${(fiatPrice * Number(formData.value)).toFixed(2)} ${ + cashtabSettings && cashtabSettings.fiatCurrency + ? cashtabSettings.fiatCurrency.toUpperCase() + : 'USD' + }`; } else { fiatPriceString = `${ formData.value ? fiatToCrypto(formData.value, fiatPrice) : '0' @@ -376,7 +391,7 @@ } } - const priceApiError = fiatPrice === null && selectedCurrency === 'USD'; + const priceApiError = fiatPrice === null && selectedCurrency !== 'XEC'; return ( <> @@ -404,8 +419,19 @@ </BalanceHeader> {fiatPrice !== null && ( <BalanceHeaderFiat> - ${(balances.totalBalance * fiatPrice).toFixed(2)}{' '} - USD + {cashtabSettings + ? `${ + currency.fiatCurrencies[ + cashtabSettings.fiatCurrency + ].symbol + } ` + : '$ '} + {(balances.totalBalance * fiatPrice).toFixed(2)}{' '} + {cashtabSettings + ? `${currency.fiatCurrencies[ + cashtabSettings.fiatCurrency + ].slug.toUpperCase()} ` + : 'USD'} </BalanceHeaderFiat> )} </> @@ -448,6 +474,12 @@ }} ></FormItemWithQRCodeAddon> <SendBchInput + activeFiatCode={ + cashtabSettings && + cashtabSettings.fiatCurrency + ? cashtabSettings.fiatCurrency.toUpperCase() + : 'USD' + } validateStatus={ sendBchAmountError ? 'error' : '' } diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap --- a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap +++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap @@ -12,7 +12,7 @@ <div className="sc-jzJRlG fVAWpo" > - $ + $ NaN USD @@ -333,7 +333,7 @@ > = - $ NaN USD + $ NaN USD </div> <div style={ @@ -369,7 +369,7 @@ <div className="sc-jzJRlG fVAWpo" > - $ + $ NaN USD @@ -690,7 +690,7 @@ > = - $ NaN USD + $ NaN USD </div> <div style={ @@ -726,7 +726,7 @@ <div className="sc-jzJRlG fVAWpo" > - $ + $ NaN USD @@ -1047,7 +1047,7 @@ > = - $ NaN USD + $ NaN USD </div> <div style={ @@ -1083,7 +1083,7 @@ <div className="sc-jzJRlG fVAWpo" > - $ + $ NaN USD @@ -1404,7 +1404,7 @@ > = - $ NaN USD + $ NaN USD </div> <div style={ @@ -1754,7 +1754,7 @@ > = - $ NaN USD + $ NaN USD </div> <div style={ @@ -2103,7 +2103,7 @@ > = - $ NaN USD + $ NaN USD </div> <div style={ diff --git a/web/cashtab/src/components/Tokens/Tokens.js b/web/cashtab/src/components/Tokens/Tokens.js --- a/web/cashtab/src/components/Tokens/Tokens.js +++ b/web/cashtab/src/components/Tokens/Tokens.js @@ -34,9 +34,13 @@ wallet state parameters not stored in the wallet object are deprecated */ - const { loading, wallet, apiError, fiatPrice } = React.useContext( - WalletContext, - ); + const { + loading, + wallet, + apiError, + fiatPrice, + cashtabSettings, + } = React.useContext(WalletContext); // If wallet is unmigrated, do not show page until it has migrated // An invalid wallet will be validated/populated after the next API call, ETA 10s @@ -78,11 +82,22 @@ {fiatPrice !== null && !isNaN(balances.totalBalance) && ( <BalanceHeaderFiat> - $ + {cashtabSettings + ? `${ + currency.fiatCurrencies[ + cashtabSettings + .fiatCurrency + ].symbol + } ` + : '$ '} {( balances.totalBalance * fiatPrice ).toFixed(2)}{' '} - USD + {cashtabSettings + ? `${currency.fiatCurrencies[ + cashtabSettings.fiatCurrency + ].slug.toUpperCase()} ` + : 'USD'} </BalanceHeaderFiat> )} </> diff --git a/web/cashtab/src/components/Wallet/Tx.js b/web/cashtab/src/components/Wallet/Tx.js --- a/web/cashtab/src/components/Wallet/Tx.js +++ b/web/cashtab/src/components/Wallet/Tx.js @@ -141,7 +141,7 @@ } `; -const Tx = ({ data, fiatPrice }) => { +const Tx = ({ data, fiatPrice, fiatCurrency }) => { const txDate = typeof data.blocktime === 'undefined' ? new Date().toLocaleDateString() @@ -270,18 +270,22 @@ )} {currency.ticker} <br /> - {fiatPrice !== null && - !isNaN(data.amountSent) && ( - <TxFiatPrice> - - $ - {( - fromLegacyDecimals( - data.amountSent, - ) * fiatPrice - ).toFixed(2)}{' '} - USD - </TxFiatPrice> - )} + {fiatPrice !== null && !isNaN(data.amountSent) && ( + <TxFiatPrice> + -{' '} + { + currency.fiatCurrencies[ + fiatCurrency + ].symbol + } + {( + fromLegacyDecimals( + data.amountSent, + ) * fiatPrice + ).toFixed(2)}{' '} + {currency.fiatCurrencies.fiatCurrency} + </TxFiatPrice> + )} </> ) : ( <> @@ -294,13 +298,21 @@ {fiatPrice !== null && !isNaN(data.amountReceived) && ( <TxFiatPrice> - + $ + +{' '} + { + currency.fiatCurrencies[ + fiatCurrency + ].symbol + } {( fromLegacyDecimals( data.amountReceived, ) * fiatPrice ).toFixed(2)}{' '} - USD + { + currency.fiatCurrencies + .fiatCurrency + } </TxFiatPrice> )} </> diff --git a/web/cashtab/src/components/Wallet/TxHistory.js b/web/cashtab/src/components/Wallet/TxHistory.js --- a/web/cashtab/src/components/Wallet/TxHistory.js +++ b/web/cashtab/src/components/Wallet/TxHistory.js @@ -4,7 +4,7 @@ export const TxLink = styled.a``; -const TxHistory = ({ txs, fiatPrice }) => { +const TxHistory = ({ txs, fiatPrice, fiatCurrency }) => { return ( <div> {txs.map(tx => ( @@ -14,7 +14,11 @@ target="_blank" rel="noreferrer" > - <Tx data={tx} fiatPrice={fiatPrice} /> + <Tx + data={tx} + fiatPrice={fiatPrice} + fiatCurrency={fiatCurrency} + /> </TxLink> ))} </div> diff --git a/web/cashtab/src/components/Wallet/Wallet.js b/web/cashtab/src/components/Wallet/Wallet.js --- a/web/cashtab/src/components/Wallet/Wallet.js +++ b/web/cashtab/src/components/Wallet/Wallet.js @@ -179,7 +179,7 @@ const WalletInfo = () => { const ContextValue = React.useContext(WalletContext); - const { wallet, fiatPrice, apiError } = ContextValue; + const { wallet, fiatPrice, apiError, cashtabSettings } = ContextValue; let balances; let parsedTxHistory; let tokens; @@ -245,8 +245,19 @@ </BalanceHeader> {fiatPrice !== null && !isNaN(balances.totalBalance) && ( <BalanceHeaderFiat> - ${(balances.totalBalance * fiatPrice).toFixed(2)}{' '} - USD + {cashtabSettings + ? `${ + currency.fiatCurrencies[ + cashtabSettings.fiatCurrency + ].symbol + } ` + : '$ '} + {(balances.totalBalance * fiatPrice).toFixed(2)}{' '} + {cashtabSettings + ? `${currency.fiatCurrencies[ + cashtabSettings.fiatCurrency + ].slug.toUpperCase()} ` + : 'USD'} </BalanceHeaderFiat> )} </> @@ -356,6 +367,11 @@ <TxHistory txs={parsedTxHistory} fiatPrice={fiatPrice} + fiatCurrency={ + cashtabSettings && cashtabSettings.fiatCurrency + ? cashtabSettings.fiatCurrency + : 'usd' + } /> </TabPane> <TabPane active={activeTab === 'tokens'}> diff --git a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap --- a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap +++ b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap @@ -12,7 +12,7 @@ <div className="sc-gPEVay NMtXN" > - $ + $ NaN USD @@ -149,7 +149,7 @@ <div className="sc-gPEVay NMtXN" > - $ + $ NaN USD @@ -286,7 +286,7 @@ <div className="sc-gPEVay NMtXN" > - $ + $ NaN USD @@ -423,7 +423,7 @@ <div className="sc-gPEVay NMtXN" > - $ + $ NaN USD diff --git a/web/cashtab/src/hooks/useWallet.js b/web/cashtab/src/hooks/useWallet.js --- a/web/cashtab/src/hooks/useWallet.js +++ b/web/cashtab/src/hooks/useWallet.js @@ -12,6 +12,7 @@ loadStoredWallet, isValidStoredWallet, } from '@utils/cashMethods'; +import { isValidCashtabSettings } from '@utils/validation'; import localforage from 'localforage'; import { currency } from '@components/Common/Ticker'; import isEmpty from 'lodash.isempty'; @@ -19,9 +20,11 @@ const useWallet = () => { const [wallet, setWallet] = useState(false); + const [cashtabSettings, setCashtabSettings] = useState(false); const [fiatPrice, setFiatPrice] = useState(null); const [ws, setWs] = useState(null); const [apiError, setApiError] = useState(false); + const [checkFiatInterval, setCheckFiatInterval] = useState(null); const [walletState, setWalletState] = useState({ balances: {}, hydratedUtxoDetails: {}, @@ -799,6 +802,103 @@ await loadWalletFromStorageOnStartup(setWallet); }; + const loadCashtabSettings = async () => { + // get settings object from localforage + let localSettings; + try { + localSettings = await localforage.getItem('settings'); + // If there is no keyvalue pair in localforage with key 'settings' + if (localSettings === null) { + // Create one with the default settings from Ticker.js + localforage.setItem('settings', currency.defaultSettings); + // Set state to default settings + setCashtabSettings(currency.defaultSettings); + return currency.defaultSettings; + } + } catch (err) { + console.log(`Error getting cashtabSettings`, err); + // TODO If they do not exist, write them + // TODO add function to change them + setCashtabSettings(currency.defaultSettings); + return currency.defaultSettings; + } + // If you found an object in localforage at the settings key, make sure it's valid + if (isValidCashtabSettings(localSettings)) { + setCashtabSettings(localSettings); + return localSettings; + } + // if not valid, also set cashtabSettings to default + setCashtabSettings(currency.defaultSettings); + return currency.defaultSettings; + }; + + // With different currency selections possible, need unique intervals for price checks + // Must be able to end them and set new ones with new currencies + const initializeFiatPriceApi = async selectedFiatCurrency => { + // Update fiat price and confirm it is set to make sure ap keeps loading state until this is updated + await fetchBchPrice(selectedFiatCurrency); + // Set interval for updating the price with given currency + + const thisFiatInterval = setInterval(function () { + fetchBchPrice(selectedFiatCurrency); + }, 60000); + + // set interval in state + setCheckFiatInterval(thisFiatInterval); + }; + + const clearFiatPriceApi = fiatPriceApi => { + // Clear fiat price check interval of previously selected currency + clearInterval(fiatPriceApi); + }; + + const changeCashtabSettings = async (key, newValue) => { + // Set loading to true as you do not want to display the fiat price of the last currency + // loading = true will lock the UI until the fiat price has updated + setLoading(true); + // Get settings from localforage + let currentSettings; + let newSettings; + try { + currentSettings = await localforage.getItem('settings'); + } catch (err) { + console.log(`Error in changeCashtabSettings`, err); + // Set fiat price to null, which disables fiat sends throughout the app + setFiatPrice(null); + // Unlock the UI + setLoading(false); + return; + } + // Make sure function was called with valid params + if ( + Object.keys(currentSettings).includes(key) && + currency.settingsValidation[key].includes(newValue) + ) { + // Update settings + newSettings = currentSettings; + newSettings[key] = newValue; + } + // Set new settings in state so they are available in context throughout the app + setCashtabSettings(newSettings); + // If this settings change adjusted the fiat currency, update fiat price + if (key === 'fiatCurrency') { + clearFiatPriceApi(checkFiatInterval); + initializeFiatPriceApi(newValue); + } + // Write new settings in localforage + try { + await localforage.setItem('settings', newSettings); + } catch (err) { + console.log( + `Error writing newSettings object to localforage in changeCashtabSettings`, + err, + ); + console.log(`newSettings`, newSettings); + // do nothing. If this happens, the user will see default currency next time they load the app. + } + setLoading(false); + }; + // Parse for incoming BCH transactions // Only notify if websocket is not connected if ( @@ -931,11 +1031,6 @@ } } - // Update price every 1 min - useAsyncTimeout(async () => { - fetchBchPrice(); - }, 60000); - // Update wallet every 10s useAsyncTimeout(async () => { const wallet = await getWallet(); @@ -1223,11 +1318,11 @@ } }; - const fetchBchPrice = async () => { + const fetchBchPrice = async ( + fiatCode = cashtabSettings ? cashtabSettings.fiatCurrency : 'usd', + ) => { // Split this variable out in case coingecko changes const cryptoId = currency.coingeckoId; - // Keep currency as a variable as eventually it will be a user setting - const fiatCode = 'usd'; // Keep this in the code, because different URLs will have different outputs require different parsing const priceApiUrl = `https://api.coingecko.com/api/v3/simple/price?ids=${cryptoId}&vs_currencies=${fiatCode}&include_last_updated_at=true`; let bchPrice; @@ -1265,9 +1360,10 @@ } }; - useEffect(() => { + useEffect(async () => { handleUpdateWallet(setWallet); - fetchBchPrice(); + const initialSettings = await loadCashtabSettings(); + initializeFiatPriceApi(initialSettings.fiatCurrency); }, []); useEffect(() => { @@ -1297,6 +1393,8 @@ parsedTxHistory, loading, apiError, + cashtabSettings, + changeCashtabSettings, getActiveWalletFromLocalForage, getWallet, validateMnemonic, diff --git a/web/cashtab/src/utils/__tests__/validation.test.js b/web/cashtab/src/utils/__tests__/validation.test.js --- a/web/cashtab/src/utils/__tests__/validation.test.js +++ b/web/cashtab/src/utils/__tests__/validation.test.js @@ -7,6 +7,7 @@ isValidTokenInitialQty, isValidTokenDocumentUrl, isValidTokenStats, + isValidCashtabSettings, } from '../validation'; import { currency } from '@components/Common/Ticker.js'; import { fromSmallestDenomination } from '@utils/cashMethods'; @@ -213,4 +214,15 @@ it(`Recognizes a token stats object with missing required keys as invalid`, () => { expect(isValidTokenStats(noCovidStatsInvalid)).toBe(false); }); + it(`Recognizes a valid cashtab settings object`, () => { + expect(isValidCashtabSettings({ fiatCurrency: 'usd' })).toBe(true); + }); + it(`Rejects a cashtab settings object for an unsupported currency`, () => { + expect(isValidCashtabSettings({ fiatCurrency: 'jpy' })).toBe(false); + }); + it(`Rejects a corrupted cashtab settings object for an unsupported currency`, () => { + expect(isValidCashtabSettings({ fiatCurrencyWrongLabel: 'usd' })).toBe( + false, + ); + }); }); diff --git a/web/cashtab/src/utils/validation.js b/web/cashtab/src/utils/validation.js --- a/web/cashtab/src/utils/validation.js +++ b/web/cashtab/src/utils/validation.js @@ -13,8 +13,8 @@ let error = false; let testedAmount = new BigNumber(cashAmount); - if (selectedCurrency === 'USD') { - // Ensure no more than 8 decimal places + if (selectedCurrency !== currency.ticker) { + // Ensure no more than currency.cashDecimals decimal places testedAmount = new BigNumber(fiatToCrypto(cashAmount, fiatPrice)); } @@ -105,3 +105,17 @@ 'circulatingSupply' in tokenStats ); }; + +export const isValidCashtabSettings = settings => { + try { + const isValid = + typeof settings === 'object' && + Object.prototype.hasOwnProperty.call(settings, 'fiatCurrency') && + currency.settingsValidation.fiatCurrency.includes( + settings.fiatCurrency, + ); + return isValid; + } catch (err) { + return false; + } +};