From 26033d0c7793cc36ecbfdc74d4074c9c2d6d57e9 Mon Sep 17 00:00:00 2001 From: Patrick Tajima Date: Thu, 10 Mar 2022 17:00:37 -0800 Subject: [PATCH] feat: Add new 'import w/ link' UI --- packages/frontend/src/components/Routing.js | 3 +- .../accounts/import/ImportAccountWithLink.js | 169 ++++++++++++++++++ .../routes/ImportAccountWithLinkWrapper.js | 81 +++++++++ .../frontend/src/translations/en.global.json | 18 ++ .../frontend/src/translations/pt.global.json | 14 ++ .../frontend/src/translations/ru.global.json | 14 ++ .../frontend/src/translations/tr.global.json | 14 ++ .../frontend/src/translations/vi.global.json | 14 ++ .../src/translations/zh-hans.global.json | 14 ++ .../src/translations/zh-hant.global.json | 14 ++ packages/frontend/src/utils/helper-api.js | 11 ++ packages/frontend/src/utils/wallet.js | 5 +- 12 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 packages/frontend/src/components/accounts/import/ImportAccountWithLink.js create mode 100644 packages/frontend/src/routes/ImportAccountWithLinkWrapper.js diff --git a/packages/frontend/src/components/Routing.js b/packages/frontend/src/components/Routing.js index 191f9635c9..193ad92c2c 100644 --- a/packages/frontend/src/components/Routing.js +++ b/packages/frontend/src/components/Routing.js @@ -20,6 +20,7 @@ import { handleClearAlert } from '../redux/reducers/status'; import { selectAccountSlice } from '../redux/slices/account'; import { actions as tokenFiatValueActions } from '../redux/slices/tokenFiatValues'; import { CreateImplicitAccountWrapper } from '../routes/CreateImplicitAccountWrapper'; +import { ImportAccountWithLinkWrapper } from '../routes/ImportAccountWithLinkWrapper'; import { LoginWrapper } from '../routes/LoginWrapper'; import { SetupLedgerNewAccountWrapper } from '../routes/SetupLedgerNewAccountWrapper'; import { SetupPassphraseNewAccountWrapper } from '../routes/SetupPassphraseNewAccountWrapper'; @@ -471,7 +472,7 @@ class Routing extends Component { div { + display: flex; + align-items: center; + overflow: hidden; + width: 100%; + + > div { + text-overflow: ellipsis; + overflow: hidden; + max-width: 70%; + white-space: nowrap; + } + } + } + } +`; + +export default ({ + accountsBySeedPhrase, + onClickAccount, + importingAccount +}) => { + + const numberOfAccountsFound = accountsBySeedPhrase.length; + + const getAccountsStatus = () => { + if (numberOfAccountsFound === 1 && accountsBySeedPhrase[0].imported === true) { + return

; + } + + if (numberOfAccountsFound !== 1) { + return

; + } + + return

; + }; + + return ( + +

+ + {getAccountsStatus()} + +

+   + + + + +   + +

+
+ {numberOfAccountsFound !== 1 + ? + : + } +
+ { + numberOfAccountsFound > 0 && ( +
+
+ {accountsBySeedPhrase.map((account) => ( +
+
+ +
{account.accountId}
+
+ account.imported) && !account.imported ? 'gray-blue' : 'blue'} + disabled={!!importingAccount} + sending={importingAccount === account.accountId} + sendingString='importing' + onClick={() => { + onClickAccount({ + accountId: account.accountId, + action: account.imported ? 'select' : 'import' + }); + }} + > + {account.imported + ? + : + } + +
+ ))} +
+
+ ) + } +
+ ); +}; diff --git a/packages/frontend/src/routes/ImportAccountWithLinkWrapper.js b/packages/frontend/src/routes/ImportAccountWithLinkWrapper.js new file mode 100644 index 0000000000..469fa05320 --- /dev/null +++ b/packages/frontend/src/routes/ImportAccountWithLinkWrapper.js @@ -0,0 +1,81 @@ +import React, { useEffect, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { useParams } from 'react-router-dom'; + +import ImportAccountWithLink from '../components/accounts/import/ImportAccountWithLink'; +import { Mixpanel } from '../mixpanel/index'; +import { + switchAccount, + redirectTo, + recoverAccountSeedPhrase, + refreshAccount, + clearAccountState +} from '../redux/actions/account'; +import { showCustomAlert } from '../redux/actions/status'; +import { selectAccountId } from '../redux/slices/account'; +import { selectAvailableAccounts } from '../redux/slices/availableAccounts'; +import { getAccountIdsBySeedPhrase } from '../utils/helper-api'; + +export function ImportAccountWithLinkWrapper() { + const dispatch = useDispatch(); + const { seedPhrase } = useParams(); + const activeAccountId = useSelector(selectAccountId); + const availableAccounts = useSelector(selectAvailableAccounts); + const [accountIdsBySeedPhrase, setAccountIdsBySeedPhrase] = useState([]); + const [importingAccount, setImportingAccount] = useState(false); + + useEffect(() => { + const handleGetAccountsBySeedPhrase = async () => { + const accountIdsBySeedPhrase = await getAccountIdsBySeedPhrase(seedPhrase); + setAccountIdsBySeedPhrase(accountIdsBySeedPhrase); + }; + handleGetAccountsBySeedPhrase(); + }, []); + + let accountsBySeedPhrase = []; + + for (let accountId of accountIdsBySeedPhrase) { + let account = {}; + account.accountId = accountId; + account.imported = availableAccounts.includes(accountId); + accountsBySeedPhrase.push(account); + } + + return ( + { + if (action === 'import') { + await Mixpanel.withTracking('IE Recover with link', + async () => { + const shouldCreateFullAccessKey = false; + setImportingAccount(accountId); + await dispatch(recoverAccountSeedPhrase(seedPhrase, accountId, shouldCreateFullAccessKey)); + dispatch(refreshAccount()); + dispatch(redirectTo('/')); + dispatch(clearAccountState()); + }, + (e) => { + dispatch(showCustomAlert({ + success: false, + messageCodeHeader: 'error', + messageCode: 'walletErrorCodes.recoverAccountLink.error', + errorMessage: e.message + })); + throw e; + }, + () => { + setImportingAccount(false); + } + ); + } else if (action === 'select') { + if (accountId !== activeAccountId) { + await dispatch(switchAccount({ accountId })); + } + await dispatch(redirectTo('/')); + } + }} + /> + ); +}; diff --git a/packages/frontend/src/translations/en.global.json b/packages/frontend/src/translations/en.global.json index 4967751cc5..5532c874b9 100644 --- a/packages/frontend/src/translations/en.global.json +++ b/packages/frontend/src/translations/en.global.json @@ -844,6 +844,20 @@ "snackbarCopySuccess": "Recover URL copied", "title": "Restore Account" }, + "importAccountWithLink": { + "accountFound": "1 Account Found", + "accountsFound": "${count} Accounts Found", + "accountImported": "Account Imported", + "alreadyImported": "The account secured by this link has been imported.", + "copyUrl": "copy the URL", + "continue": "and continue this process in your browser of choice.", + "foundAccount": "We found 1 account secured by this link.", + "foundAccounts": "We found ${count} accounts secured by this link.", + "import": "Import", + "importAccount": "Import Your Account", + "preferedBrowser": "If this isn't your preferred browser,", + "goToAccount": "Go to Account" + }, "recoveryMgmt": { "disableInputPlaceholder": "Enter your account ID to confirm", "disableNo": "No, keep", @@ -975,6 +989,7 @@ "switchAccounthNotAllowed": "This app doesn't allow account changes" }, "sending": "Sending", + "importing": "Importing", "sendMoney": { "account": { "title": "Send to" @@ -1555,6 +1570,9 @@ "recoverAccountSeedPhrase": { "errorInvalidSeedPhrase": "No accounts were found for this passphrase." }, + "recoverAccountLink": { + "error": "Import of your account failed. Please try again or contact support for assistance." + }, "recoveryMethods": { "lastMethod": "Cannot delete your last recovery method. Unless you have Ledger enabled, you need to keep at least one recovery method active to ensure recoverability of your account.", "setupMethod": "An error occurred. Please check your recovery method." diff --git a/packages/frontend/src/translations/pt.global.json b/packages/frontend/src/translations/pt.global.json index 141b7da7bd..fef869c13e 100644 --- a/packages/frontend/src/translations/pt.global.json +++ b/packages/frontend/src/translations/pt.global.json @@ -795,6 +795,20 @@ "snackbarCopySuccess": "URL de Recuperação copiada", "title": "Restaurar conta" }, + "importAccountWithLink": { + "accountFound": "1 Account Found", + "accountsFound": "${count} Accounts Found", + "accountImported": "Account Imported", + "alreadyImported": "The account secured by this link has been imported.", + "copyUrl": "copy the URL", + "continue": "and continue this process in your browser of choice.", + "foundAccount": "We found 1 account secured by this link.", + "foundAccounts": "We found ${count} accounts secured by this link.", + "import": "Import", + "importAccount": "Import Your Account", + "preferedBrowser": "If this isn't your preferred browser,", + "goToAccount": "Go to Account" + }, "recoveryMgmt": { "disableInputPlaceholder": "Insira o ID da sua conta para confirmar", "disableNo": "Não, manter", diff --git a/packages/frontend/src/translations/ru.global.json b/packages/frontend/src/translations/ru.global.json index a0fe13f3b4..4e4d5072a4 100644 --- a/packages/frontend/src/translations/ru.global.json +++ b/packages/frontend/src/translations/ru.global.json @@ -676,6 +676,20 @@ "snackbarCopySuccess": "Ссылка для восстанавления скопирована", "title": "Восстановление Учетной Записи" }, + "importAccountWithLink": { + "accountFound": "1 Account Found", + "accountsFound": "${count} Accounts Found", + "accountImported": "Account Imported", + "alreadyImported": "The account secured by this link has been imported.", + "copyUrl": "copy the URL", + "continue": "and continue this process in your browser of choice.", + "foundAccount": "We found 1 account secured by this link.", + "foundAccounts": "We found ${count} accounts secured by this link.", + "import": "Import", + "importAccount": "Import Your Account", + "preferedBrowser": "If this isn't your preferred browser,", + "goToAccount": "Go to Account" + }, "recoveryMgmt": { "disableInputPlaceholder": "Введите свое имя пользователя для подтверждения", "disableNo": "Нет, продолжить", diff --git a/packages/frontend/src/translations/tr.global.json b/packages/frontend/src/translations/tr.global.json index 966bb48380..231b393016 100644 --- a/packages/frontend/src/translations/tr.global.json +++ b/packages/frontend/src/translations/tr.global.json @@ -783,6 +783,20 @@ "snackbarCopySuccess": "Kopyalanan URL'yi kurtar", "title": "Hesabı Geri Yükle" }, + "importAccountWithLink": { + "accountFound": "1 Account Found", + "accountsFound": "${count} Accounts Found", + "accountImported": "Account Imported", + "alreadyImported": "The account secured by this link has been imported.", + "copyUrl": "copy the URL", + "continue": "and continue this process in your browser of choice.", + "foundAccount": "We found 1 account secured by this link.", + "foundAccounts": "We found ${count} accounts secured by this link.", + "import": "Import", + "importAccount": "Import Your Account", + "preferedBrowser": "If this isn't your preferred browser,", + "goToAccount": "Go to Account" + }, "recoveryMgmt": { "disableInputPlaceholder": "Onaylamak için hesap kimliğinizi girin", "disableNo": "Hayır, Devam", diff --git a/packages/frontend/src/translations/vi.global.json b/packages/frontend/src/translations/vi.global.json index 0208816c43..9b4139c319 100644 --- a/packages/frontend/src/translations/vi.global.json +++ b/packages/frontend/src/translations/vi.global.json @@ -739,6 +739,20 @@ "snackbarCopySuccess": "Đã sao chép URL khôi phục", "title": "Khôi phục tài khoản" }, + "importAccountWithLink": { + "accountFound": "1 Account Found", + "accountsFound": "${count} Accounts Found", + "accountImported": "Account Imported", + "alreadyImported": "The account secured by this link has been imported.", + "copyUrl": "copy the URL", + "continue": "and continue this process in your browser of choice.", + "foundAccount": "We found 1 account secured by this link.", + "foundAccounts": "We found ${count} accounts secured by this link.", + "import": "Import", + "importAccount": "Import Your Account", + "preferedBrowser": "If this isn't your preferred browser,", + "goToAccount": "Go to Account" + }, "recoveryMgmt": { "disableInputPlaceholder": "Nhập ID tài khoản của bạn để xác nhận", "disableNo": "Không, giữ lại", diff --git a/packages/frontend/src/translations/zh-hans.global.json b/packages/frontend/src/translations/zh-hans.global.json index 56a3f1a4c2..b6321ae648 100644 --- a/packages/frontend/src/translations/zh-hans.global.json +++ b/packages/frontend/src/translations/zh-hans.global.json @@ -814,6 +814,20 @@ "snackbarCopySuccess": "恢复链接已复制", "title": "恢复账户" }, + "importAccountWithLink": { + "accountFound": "1 Account Found", + "accountsFound": "${count} Accounts Found", + "accountImported": "Account Imported", + "alreadyImported": "The account secured by this link has been imported.", + "copyUrl": "copy the URL", + "continue": "and continue this process in your browser of choice.", + "foundAccount": "We found 1 account secured by this link.", + "foundAccounts": "We found ${count} accounts secured by this link.", + "import": "Import", + "importAccount": "Import Your Account", + "preferedBrowser": "If this isn't your preferred browser,", + "goToAccount": "Go to Account" + }, "recoveryMgmt": { "disableInputPlaceholder": "请输入用户名确认", "disableNo": "不,保留。", diff --git a/packages/frontend/src/translations/zh-hant.global.json b/packages/frontend/src/translations/zh-hant.global.json index 0cc1fb7437..3ae5f1f50e 100644 --- a/packages/frontend/src/translations/zh-hant.global.json +++ b/packages/frontend/src/translations/zh-hant.global.json @@ -814,6 +814,20 @@ "snackbarCopySuccess": "恢復鏈接已複製", "title": "恢復賬戶" }, + "importAccountWithLink": { + "accountFound": "1 Account Found", + "accountsFound": "${count} Accounts Found", + "accountImported": "Account Imported", + "alreadyImported": "The account secured by this link has been imported.", + "copyUrl": "copy the URL", + "continue": "and continue this process in your browser of choice.", + "foundAccount": "We found 1 account secured by this link.", + "foundAccounts": "We found ${count} accounts secured by this link.", + "import": "Import", + "importAccount": "Import Your Account", + "preferedBrowser": "If this isn't your preferred browser,", + "goToAccount": "Go to Account" + }, "recoveryMgmt": { "disableInputPlaceholder": "請輸入用戶名確認", "disableNo": "不,保留。", diff --git a/packages/frontend/src/utils/helper-api.js b/packages/frontend/src/utils/helper-api.js index c1b40e51ac..2b97b0084e 100644 --- a/packages/frontend/src/utils/helper-api.js +++ b/packages/frontend/src/utils/helper-api.js @@ -1,4 +1,7 @@ +import * as nearApiJs from 'near-api-js'; +import { parseSeedPhrase } from 'near-seed-phrase'; + import { ACCOUNT_HELPER_URL } from '../config'; export let controller; @@ -8,6 +11,14 @@ export async function getAccountIds(publicKey) { return await fetch(`${ACCOUNT_HELPER_URL}/publicKey/${publicKey}/accounts`, { signal: controller.signal }).then((res) => res.json()); } +export async function getAccountIdsBySeedPhrase(seedPhrase) { + const { secretKey } = parseSeedPhrase(seedPhrase); + const keyPair = nearApiJs.KeyPair.fromString(secretKey); + const publicKey = keyPair.publicKey.toString(); + const accountIdsByPublickKey = await getAccountIds(publicKey); + return accountIdsByPublickKey; +} + export function isUrlNotJavascriptProtocol(url) { if (!url) { return true; diff --git a/packages/frontend/src/utils/wallet.js b/packages/frontend/src/utils/wallet.js index 59df9f3cef..35727f20bf 100644 --- a/packages/frontend/src/utils/wallet.js +++ b/packages/frontend/src/utils/wallet.js @@ -848,7 +848,10 @@ class Wallet { accountIds = [accountId]; } - accountIds.push(implicitAccountId); + if (!accountId) { + // Import implicit account only if no accountId is specified + accountIds.push(implicitAccountId); + } // remove duplicate and non-existing accounts const accountsSet = new Set(accountIds);