From b731e1bb510a66982e5a64d06e68584cb5770990 Mon Sep 17 00:00:00 2001 From: Pasquale Jordan <29759576+TrimVis@users.noreply.github.com> Date: Sun, 6 Oct 2024 18:59:23 +0200 Subject: [PATCH 1/4] Reworked tx-chaining code of nami etc --- README.md | 7 ++ package.json | 2 +- src/api/extension/index.js | 88 ++++++++++++++++++------- src/api/webpage/index.js | 5 ++ src/config/config.js | 1 + src/manifest.json | 4 +- src/pages/Background/index.js | 21 ++++++ src/pages/Content/injected.js | 3 + src/ui/app/components/historyViewer.jsx | 18 +++-- src/ui/app/components/transaction.jsx | 32 +++++---- 10 files changed, 137 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index d46b5c34..1d073d0d 100755 --- a/README.md +++ b/README.md @@ -86,6 +86,13 @@ cardano.getBalance() : Value `Value` is a hex encoded cbor string. +##### cardano.getMempoolTxs(paginate) + +``` +cardano.getMempoolTxs() : [TxHash] +``` + + ##### cardano.getUtxos(amount, paginate) ``` diff --git a/package.json b/package.json index 006813ea..c1e9fbab 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nami-wallet", "version": "3.8.5", - "description": "Maintained by IOG", + "description": "Transaction-Chaining Fork by Mueliswap", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/src/api/extension/index.js b/src/api/extension/index.js index 3a530e67..ba620ea7 100644 --- a/src/api/extension/index.js +++ b/src/api/extension/index.js @@ -226,21 +226,27 @@ export const getFullBalance = async () => { ).toString(); }; -export const getTransactions = async (paginate = 1, count = 10) => { +export const getTransactions = async (paginate = 1, count = 10, isMempool = false) => { const currentAccount = await getCurrentAccount(); const result = await blockfrostRequest( - `/addresses/${currentAccount.paymentKeyHashBech32}/transactions?page=${paginate}&order=desc&count=${count}` + !isMempool ? + `/addresses/${currentAccount.paymentKeyHashBech32}/transactions?page=${paginate}&order=desc&count=${count}` : + `/mempool/addresses/${currentAccount.paymentKeyHashBech32}?page=${paginate}&order=desc&count=${count}` ); if (!result || result.error) return []; - return result.map((tx) => ({ - txHash: tx.tx_hash, - txIndex: tx.tx_index, - blockHeight: tx.block_height, - })); + return !isMempool ? + result.map((tx) => ({ + txHash: tx.tx_hash, + txIndex: tx.tx_index, + blockHeight: tx.block_height, + })) : result.map((tx) => ({ txHash: tx.tx_hash, })); }; -export const getTxInfo = async (txHash) => { - const result = await blockfrostRequest(`/txs/${txHash}`); + +export const getTxInfo = async (txHash, isMempool = false) => { + let result = await blockfrostRequest( + !isMempool ? `/txs/${txHash}` : `/mempool/${txHash}` + ); if (!result || result.error) return null; return result; }; @@ -263,22 +269,54 @@ export const getTxMetadata = async (txHash) => { return result; }; -export const updateTxInfo = async (txHash) => { +export const updateTxInfo = async (txHash, pending) => { const currentAccount = await getCurrentAccount(); const network = await getNetwork(); - let detail = await currentAccount[network.id].history.details[txHash]; if (typeof detail !== 'object' || !detail.info || !detail.block || !detail.utxos || !detail.metadata) { detail = {}; - const info = getTxInfo(txHash); - const uTxOs = getTxUTxOs(txHash); - const metadata = getTxMetadata(txHash); - - detail.info = await info; - if (info) detail.block = await getBlock(detail.info.block_height); - detail.utxos = await uTxOs; - detail.metadata = await metadata; + let info = await getTxInfo(txHash, pending); + + if (!info && pending) { + // This transaction is no longer part of the mempool + info = await getTxInfo(txHash, false); + if (info) { + const accounts = await getStorage(STORAGE.accounts); + const currentAccountIndex = await getCurrentAccountIndex() + accounts[currentAccountIndex][network.id].history.pending = accounts[currentAccountIndex][network.id].history.pending.filter((t) => t !== txHash) + pending = false + await setStorage({ [STORAGE.accounts]: { ...accounts }, }); + } + } + + if (!pending) { + const uTxOs = getTxUTxOs(txHash); + const metadata = getTxMetadata(txHash); + + detail.info = info; + if (info) detail.block = await getBlock(detail.info.block_height); + detail.utxos = await uTxOs; + detail.metadata = await metadata; + } else { + // In this case it might simply no longer be in the mempool + const uTxOs = { + hash: txHash, + inputs: info ? info.inputs : [], + outputs: info ? info.outputs : [], + }; + // Need to manually resolve outputs here + // TODO automatically check if respective UTxO has to be fetched from mempool + for (const input of uTxOs.inputs) { + const uTxOs = await getTxUTxOs(input.tx_hash); + input.amount = uTxOs.outputs[input.output_index].amount; + } + detail.info = info && info.tx; + detail.utxos = uTxOs; + detail.block = "mempool"; + // This is not provided by blockfrost yet + detail.metadata = []; + } } return detail; @@ -1339,7 +1377,7 @@ export const createAccount = async (name, password, accountIndex = null) => { lovelace: null, minAda: 0, assets: [], - history: { confirmed: [], details: {} }, + history: { confirmed: [], pending: [], details: {} }, }; const newAccount = { @@ -1438,7 +1476,7 @@ export const createHWAccounts = async (accounts) => { lovelace: null, minAda: 0, assets: [], - history: { confirmed: [], details: {} }, + history: { confirmed: [], pending: [], details: {} }, }; existingAccounts[index] = { @@ -1908,11 +1946,15 @@ const updateTransactions = async (currentAccount, network) => { return true; }; -export const setTransactions = async (txs) => { +export const setTransactions = async (txs, isMempool=false) => { const currentIndex = await getCurrentAccountIndex(); const network = await getNetwork(); const accounts = await getStorage(STORAGE.accounts); - accounts[currentIndex][network.id].history.confirmed = txs; + if (!isMempool) { + accounts[currentIndex][network.id].history.confirmed = txs; + } else { + accounts[currentIndex][network.id].history.pending = txs; + } return await setStorage({ [STORAGE.accounts]: { ...accounts, diff --git a/src/api/webpage/index.js b/src/api/webpage/index.js index ff12038d..73806870 100644 --- a/src/api/webpage/index.js +++ b/src/api/webpage/index.js @@ -6,6 +6,11 @@ export const getBalance = async () => { return result.data; }; +export const getMempoolTxs = async () => { + const result = await Messaging.sendToContent({ method: METHOD.getMempoolTxs }); + return result.data; +}; + export const enable = async () => { const result = await Messaging.sendToContent({ method: METHOD.enable }); return result.data; diff --git a/src/config/config.js b/src/config/config.js index 58b92666..6feaf533 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -9,6 +9,7 @@ export const METHOD = { getBalance: 'getBalance', getDelegation: 'getDelegation', getUtxos: 'getUtxos', + getMempoolTxs: 'getMempoolTxs', getCollateral: 'getCollateral', getRewardAddress: 'getRewardAddress', getAddress: 'getAddress', diff --git a/src/manifest.json b/src/manifest.json index 3374d1b8..013983b0 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, - "name": "Nami", + "name": "Nami TxChaining", "version": "3.8.5", - "description": "Maintained by IOG", + "description": "Transaction-Chaining Fork by Mueliswap", "background": { "service_worker": "background.bundle.js" }, "action": { "default_popup": "mainPopup.html", diff --git a/src/pages/Background/index.js b/src/pages/Background/index.js index 4a613485..71035b10 100644 --- a/src/pages/Background/index.js +++ b/src/pages/Background/index.js @@ -6,6 +6,7 @@ import { getCollateral, getNetwork, getRewardAddress, + getTransactions, getUtxos, isWhitelisted, submitTx, @@ -154,6 +155,26 @@ app.add(METHOD.getRewardAddress, async (request, sendResponse) => { } }); +app.add(METHOD.getMempoolTxs, (request, sendResponse) => { + getTransactions(0, 100, true) + .then((txs) => { + sendResponse({ + id: request.id, + data: txs.map((t) => t.txHash), + target: TARGET, + sender: SENDER.extension, + }); + }) + .catch((e) => { + sendResponse({ + id: request.id, + error: e, + target: TARGET, + sender: SENDER.extension, + }); + }); +}); + app.add(METHOD.getUtxos, (request, sendResponse) => { getUtxos(request.data.amount, request.data.paginate) .then((utxos) => { diff --git a/src/pages/Content/injected.js b/src/pages/Content/injected.js index 67b8a74a..091d9582 100644 --- a/src/pages/Content/injected.js +++ b/src/pages/Content/injected.js @@ -3,6 +3,7 @@ import { getAddress, getBalance, getCollateral, + getMempoolTxs, getNetworkId, getRewardAddress, getUtxos, @@ -34,6 +35,7 @@ window.cardano = { signData: (address, payload) => logDeprecated() && signData(address, payload), signTx: (tx, partialSign) => logDeprecated() && signTx(tx, partialSign), submitTx: (tx) => logDeprecated() && submitTx(tx), + getMempoolTxs: () => logDeprecated() && getMempoolTxs(), getUtxos: (amount, paginate) => logDeprecated() && getUtxos(amount, paginate), getCollateral: () => logDeprecated() && getCollateral(), getUsedAddresses: async () => logDeprecated() && [await getAddress()], @@ -61,6 +63,7 @@ window.cardano = { signData: (address, payload) => signDataCIP30(address, payload), signTx: (tx, partialSign) => signTx(tx, partialSign), submitTx: (tx) => submitTx(tx), + getMempoolTxs: () => getMempoolTxs(), getUtxos: (amount, paginate) => getUtxos(amount, paginate), getUsedAddresses: async () => [await getAddress()], getUnusedAddresses: async () => [], diff --git a/src/ui/app/components/historyViewer.jsx b/src/ui/app/components/historyViewer.jsx index 17a95d91..3b2c2338 100644 --- a/src/ui/app/components/historyViewer.jsx +++ b/src/ui/app/components/historyViewer.jsx @@ -21,6 +21,7 @@ const HistoryViewer = ({ history, network, currentAddr, addresses }) => { const capture = useCaptureEvent(); const [historySlice, setHistorySlice] = React.useState(null); const [page, setPage] = React.useState(1); + const [memLoaded, setMemLoaded] = React.useState(false); const [final, setFinal] = React.useState(false); const [loadNext, setLoadNext] = React.useState(false); const getTxs = async () => { @@ -32,18 +33,23 @@ const HistoryViewer = ({ history, network, currentAddr, addresses }) => { return; } await new Promise((res, rej) => setTimeout(() => res(), 10)); + slice = slice.concat( - history.confirmed.slice((page - 1) * BATCH, page * BATCH) + (history.pending ?? []).concat(history.confirmed ?? []) + .slice((page - 1) * BATCH, page * BATCH) ); - if (slice.length < page * BATCH) { - const txs = await getTransactions(page, BATCH); + const txs = await getTransactions(page, BATCH, !memLoaded); if (txs.length <= 0) { - setFinal(true); + if (memLoaded) { + setFinal(true); + } else { + setMemLoaded(true) + } } else { slice = Array.from(new Set(slice.concat(txs.map((tx) => tx.txHash)))); - await setTransactions(slice); + await setTransactions(slice, memLoaded); } } if (slice.length < page * BATCH) setFinal(true); @@ -107,6 +113,7 @@ const HistoryViewer = ({ history, network, currentAddr, addresses }) => { return ( { + history.details[txHash] = txDetail; txObject[txHash] = txDetail; }} key={index} @@ -115,6 +122,7 @@ const HistoryViewer = ({ history, network, currentAddr, addresses }) => { currentAddr={currentAddr} addresses={addresses} network={network} + pending={history.pending.includes(txHash)} /> ); })} diff --git a/src/ui/app/components/transaction.jsx b/src/ui/app/components/transaction.jsx index 3fc4c127..0abaa434 100644 --- a/src/ui/app/components/transaction.jsx +++ b/src/ui/app/components/transaction.jsx @@ -92,6 +92,7 @@ const Transaction = ({ addresses, network, onLoad, + pending=false, }) => { const settings = useStoreState((state) => state.settings.settings); const isMounted = useIsMounted(); @@ -101,14 +102,15 @@ const Transaction = ({ const colorMode = { iconBg: useColorModeValue('white', 'gray.800'), - txBg: useColorModeValue('teal.50', 'gray.700'), - txBgHover: useColorModeValue('teal.100', 'gray.600'), + txBg: useColorModeValue(pending ? 'gray.100' : 'teal.50', pending ? 'gray.500' : 'gray.700'), + txBgHover: useColorModeValue(pending ? 'gray.200' : 'teal.100', pending ? 'gray.400' : 'gray.600'), assetsBtnHover: useColorModeValue('teal.200', 'gray.700'), }; const getTxDetail = async () => { - if (!displayInfo) { - let txDetail = await updateTxInfo(txHash); + if (true || !displayInfo || detail.block === "mempool") { + let txDetail = await updateTxInfo(txHash, pending); + detail.block = txDetail.block; onLoad(txHash, txDetail); if (!isMounted.current) return; setDisplayInfo(genDisplayInfo(txHash, txDetail, currentAddr, addresses)); @@ -124,12 +126,16 @@ const Transaction = ({ {displayInfo ? ( - + {pending ? ( + Pending... + ): ( + + )} ) : ( @@ -350,7 +356,7 @@ const TxDetail = ({ displayInfo, network }) => { > {displayInfo.txHash} - {displayInfo.detail.metadata.length > 0 ? ( + {displayInfo.detail.metadata?.length > 0 ? ( - - )} - - )} - - ); + {loadNext ? '...' : } + + + )} + + ); + } + + return {content}; }; const HistorySpinner = () => ( @@ -208,4 +197,4 @@ const HistorySpinner = () => ( ); -export default HistoryViewer; +export default HistoryViewer; \ No newline at end of file diff --git a/src/ui/app/components/transaction.jsx b/src/ui/app/components/transaction.jsx index 0abaa434..069a9db5 100644 --- a/src/ui/app/components/transaction.jsx +++ b/src/ui/app/components/transaction.jsx @@ -108,7 +108,7 @@ const Transaction = ({ }; const getTxDetail = async () => { - if (true || !displayInfo || detail.block === "mempool") { + if (!displayInfo || detail.block === "mempool") { let txDetail = await updateTxInfo(txHash, pending); detail.block = txDetail.block; onLoad(txHash, txDetail); @@ -501,9 +501,25 @@ const getTimestamp = (date) => { }; const getAddressCredentials = (address) => { - const cmlAddress = Loader.Cardano.Address.from_bech32(address); - return [cmlAddress.payment_cred()?.to_cbor_hex(), cmlAddress.staking_cred()?.to_cbor_hex()]; -} + try { + const cmlAddress = Loader.Cardano.Address.from_bech32(address); + return [ + cmlAddress.payment_cred()?.to_cbor_hex() || null, + cmlAddress.staking_cred()?.to_cbor_hex() || null, + ]; + } catch (error) { + try { + // try casting as byron address + const cmlAddress = Loader.Cardano.ByronAddress.from_base58(address); + return [ + cmlAddress.to_address()?.payment_cred()?.to_cbor_hex() || null, + cmlAddress.to_address()?.staking_cred()?.to_cbor_hex() || null, + ]; + } catch {} + console.error(error); + return [null, null]; + } +}; const matchesAnyCredential = (address, [ownPaymentCred, ownStakingCred]) => { const [otherPaymentCred, otherStakingCred] = getAddressCredentials(address); @@ -522,7 +538,8 @@ const calculateAmount = (currentAddr, uTxOList, validContract = true) => { let outputs = compileOutputs( uTxOList.outputs.filter( (output) => - matchesAnyCredential(output.address, ownCredentials) && !(output.collateral && validContract) + matchesAnyCredential(output.address, ownCredentials) && + !(output.collateral && validContract) ) ); let amounts = []; diff --git a/src/ui/app/pages/settings.jsx b/src/ui/app/pages/settings.jsx index 6887e309..bfc4f58a 100644 --- a/src/ui/app/pages/settings.jsx +++ b/src/ui/app/pages/settings.jsx @@ -14,6 +14,9 @@ import { InputRightElement, Icon, Select, + useToast, + Badge, + Flex, } from '@chakra-ui/react'; import { ChevronLeftIcon, @@ -23,7 +26,7 @@ import { RepeatIcon, CheckIcon, } from '@chakra-ui/icons'; -import React from 'react'; +import React, { useCallback } from 'react'; import { getCurrentAccount, getCurrentAccountIndex, @@ -47,6 +50,14 @@ import { ChangePasswordModal } from '../components/changePasswordModal'; import { useCaptureEvent } from '../../../features/analytics/hooks'; import { Events } from '../../../features/analytics/events'; import { LegalSettings } from '../../../features/settings/legal/LegalSettings'; +import { usePostHog } from 'posthog-js/react'; +import { useFeatureFlagsContext } from '../../../features/feature-flags/provider'; +import { + MigrationState, + MIGRATION_KEY, +} from '../../../api/migration-tool/migrator/migration-state.data'; +import { disableMigration } from '../../../api/migration-tool/cross-extension-messaging/nami-migration-client.extension'; +import { storage } from 'webextension-polyfill'; const Settings = () => { const navigate = useNavigate(); @@ -79,6 +90,7 @@ const Settings = () => { } /> } /> } /> + } /> @@ -88,7 +100,7 @@ const Settings = () => { const Overview = () => { const capture = useCaptureEvent(); const navigate = useNavigate(); - const { colorMode, toggleColorMode } = useColorMode(); + const { earlyAccessFeatures, featureFlags } = useFeatureFlagsContext(); return ( <> @@ -96,6 +108,50 @@ const Overview = () => { Settings + {earlyAccessFeatures?.find((f) => f.name === 'beta-partner') && + !featureFlags?.['is-migration-active'] && ( + + )} + {featureFlags?.['is-migration-active']?.dismissable && ( + + )} + + + + ); +}; + export default Settings; diff --git a/src/ui/indexMain.jsx b/src/ui/indexMain.jsx index 1f4e14c4..256d1c80 100644 --- a/src/ui/indexMain.jsx +++ b/src/ui/indexMain.jsx @@ -1,116 +1,28 @@ /** * indexMain is the entry point for the extension panel you open at the top right in the browser */ - import React from 'react'; import { createRoot } from 'react-dom/client'; -import { - BrowserRouter as Router, - Routes, - Route, - useLocation, -} from 'react-router-dom'; -import { useNavigate } from 'react-router-dom'; import { POPUP } from '../config/config'; -import { AnalyticsConsentModal } from '../features/analytics/ui/AnalyticsConsentModal'; -import Main from './index'; -import { Box, Spinner } from '@chakra-ui/react'; -import Welcome from './app/pages/welcome'; -import Wallet from './app/pages/wallet'; -import { getAccounts } from '../api/extension'; -import Settings from './app/pages/settings'; -import Send from './app/pages/send'; -import { useStoreActions, useStoreState } from 'easy-peasy'; -import { - AnalyticsProvider, - useAnalyticsContext, -} from '../features/analytics/provider'; import { EventTracker } from '../features/analytics/event-tracker'; import { ExtensionViews } from '../features/analytics/types'; -import { TermsAndPrivacyProvider } from '../features/terms-and-privacy'; - -const App = () => { - const route = useStoreState((state) => state.globalModel.routeStore.route); - const setRoute = useStoreActions( - (actions) => actions.globalModel.routeStore.setRoute - ); - const navigate = useNavigate(); - const location = useLocation(); - const [isLoading, setIsLoading] = React.useState(true); - const [analytics, setAnalyticsConsent] = useAnalyticsContext(); - const init = async () => { - const hasWallet = await getAccounts(); - if (hasWallet) { - navigate('/wallet'); - // Set route from localStorage if available - if (route && route !== '/wallet') { - route - .slice(1) - .split('/') - .reduce((acc, r) => { - const fullRoute = acc + `/${r}`; - navigate(fullRoute); - return fullRoute; - }, ''); - } - } else { - navigate('/welcome'); - } - setIsLoading(false); - }; - - React.useEffect(() => { - init(); - }, []); - - React.useEffect(() => { - if (!isLoading) { - setRoute(location.pathname); - } - }, [location, isLoading, setRoute]); - - return isLoading ? ( - - - - ) : ( -
- - - - - } - /> - } /> - } /> - } /> - - -
- ); -}; +import { AppWithMigration } from './lace-migration/components/migration.component'; +import Theme from './theme'; +import { BrowserRouter as Router } from 'react-router-dom'; +import Main from './index'; +import { AnalyticsProvider } from '../features/analytics/provider'; const root = createRoot(window.document.querySelector(`#${POPUP.main}`)); root.render( - -
- - - -
+ + +
+ + + +
+
); diff --git a/src/ui/lace-migration/assets/arrow.svg b/src/ui/lace-migration/assets/arrow.svg new file mode 100644 index 00000000..12330195 --- /dev/null +++ b/src/ui/lace-migration/assets/arrow.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/src/ui/lace-migration/assets/backpack.svg b/src/ui/lace-migration/assets/backpack.svg new file mode 100644 index 00000000..90bad5e6 --- /dev/null +++ b/src/ui/lace-migration/assets/backpack.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/ui/lace-migration/assets/checkmark.svg b/src/ui/lace-migration/assets/checkmark.svg new file mode 100644 index 00000000..cfe279d7 --- /dev/null +++ b/src/ui/lace-migration/assets/checkmark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/ui/lace-migration/assets/chevron-left.svg b/src/ui/lace-migration/assets/chevron-left.svg new file mode 100644 index 00000000..972e98b5 --- /dev/null +++ b/src/ui/lace-migration/assets/chevron-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/ui/lace-migration/assets/chevron-right.svg b/src/ui/lace-migration/assets/chevron-right.svg new file mode 100644 index 00000000..671294a1 --- /dev/null +++ b/src/ui/lace-migration/assets/chevron-right.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/src/ui/lace-migration/assets/clock.svg b/src/ui/lace-migration/assets/clock.svg new file mode 100644 index 00000000..f4cfb20d --- /dev/null +++ b/src/ui/lace-migration/assets/clock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/ui/lace-migration/assets/done-dark.svg b/src/ui/lace-migration/assets/done-dark.svg new file mode 100644 index 00000000..0a90a7ba --- /dev/null +++ b/src/ui/lace-migration/assets/done-dark.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/lace-migration/assets/done-white.svg b/src/ui/lace-migration/assets/done-white.svg new file mode 100644 index 00000000..ffb94686 --- /dev/null +++ b/src/ui/lace-migration/assets/done-white.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/lace-migration/assets/download.svg b/src/ui/lace-migration/assets/download.svg new file mode 100644 index 00000000..1df36734 --- /dev/null +++ b/src/ui/lace-migration/assets/download.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/ui/lace-migration/assets/features.svg b/src/ui/lace-migration/assets/features.svg new file mode 100644 index 00000000..b3c71ab3 --- /dev/null +++ b/src/ui/lace-migration/assets/features.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/lace-migration/assets/grouped-dark-mode.svg b/src/ui/lace-migration/assets/grouped-dark-mode.svg new file mode 100644 index 00000000..9c3258a3 --- /dev/null +++ b/src/ui/lace-migration/assets/grouped-dark-mode.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/lace-migration/assets/grouped-white-mode.svg b/src/ui/lace-migration/assets/grouped-white-mode.svg new file mode 100644 index 00000000..25fb8a7c --- /dev/null +++ b/src/ui/lace-migration/assets/grouped-white-mode.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/lace-migration/assets/lace-icon.svg b/src/ui/lace-migration/assets/lace-icon.svg new file mode 100644 index 00000000..7f87ae2d --- /dev/null +++ b/src/ui/lace-migration/assets/lace-icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/ui/lace-migration/assets/pending-dark-mode.svg b/src/ui/lace-migration/assets/pending-dark-mode.svg new file mode 100644 index 00000000..9265a45e --- /dev/null +++ b/src/ui/lace-migration/assets/pending-dark-mode.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/lace-migration/assets/pending-white-mode.svg b/src/ui/lace-migration/assets/pending-white-mode.svg new file mode 100644 index 00000000..e858af8b --- /dev/null +++ b/src/ui/lace-migration/assets/pending-white-mode.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/lace-migration/components/all-done/all-done.component.jsx b/src/ui/lace-migration/components/all-done/all-done.component.jsx new file mode 100644 index 00000000..a84d66d3 --- /dev/null +++ b/src/ui/lace-migration/components/all-done/all-done.component.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { useColorMode, Box } from '@chakra-ui/react'; +import { Slide } from '../slide.component'; +import { ReactComponent as Arrow } from '../../assets/arrow.svg'; +import { ReactComponent as DoneDark } from '../../assets/done-dark.svg'; +import { ReactComponent as DoneWhite } from '../../assets/done-white.svg'; + +export const AllDone = ({ isLaceInstalled, onAction }) => { + const { colorMode } = useColorMode(); + return ( + + {colorMode === 'light' ? ( + + ) : ( + + )} +
+ } + description="Your Nami wallet is now part of the Lace family." + buttonText={isLaceInstalled ? 'Open Lace' : 'Download Lace'} + buttonIcon={Arrow} + onButtonClick={onAction} + /> + ); +}; diff --git a/src/ui/lace-migration/components/all-done/all-done.stories.js b/src/ui/lace-migration/components/all-done/all-done.stories.js new file mode 100644 index 00000000..820f6d92 --- /dev/null +++ b/src/ui/lace-migration/components/all-done/all-done.stories.js @@ -0,0 +1,16 @@ +import { AllDone } from './all-done.component'; + +const meta = { + title: 'Nami Migration/Screens/AllDone', + component: AllDone, + parameters: { + layout: 'centered', + }, + args: { + onAction: () => {}, + }, +}; + +export default meta; + +export const Primary = {}; diff --git a/src/ui/lace-migration/components/almost-there/almost-there.component.jsx b/src/ui/lace-migration/components/almost-there/almost-there.component.jsx new file mode 100644 index 00000000..56a5554f --- /dev/null +++ b/src/ui/lace-migration/components/almost-there/almost-there.component.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { useColorMode, Box } from '@chakra-ui/react'; +import { Slide } from '../slide.component'; +import { ReactComponent as Download } from '../../assets/download.svg'; +import { ReactComponent as Arrow } from '../../assets/arrow.svg'; +import { ReactComponent as PendingDark } from '../../assets/pending-dark-mode.svg'; +import { ReactComponent as PendingWhite } from '../../assets/pending-white-mode.svg'; + +export const AlmostThere = ({ + isLaceInstalled, + onAction, + isDismissable, + dismissibleSeconds, +}) => { + const { colorMode } = useColorMode(); + return ( + + {colorMode === 'light' ? ( + + ) : ( + + )} + + } + description={ + isLaceInstalled + ? 'Your Nami wallet is now part of the Lace family.' + : 'Download the Lace extension to begin.' + } + buttonText={isLaceInstalled ? 'Open Lace' : 'Download Lace'} + buttonIcon={isLaceInstalled ? Arrow : Download} + onButtonClick={onAction} + isDismissable={isDismissable} + buttonOrientation="column" + dismissibleSeconds={dismissibleSeconds} + /> + ); +}; diff --git a/src/ui/lace-migration/components/almost-there/almost-there.stories.js b/src/ui/lace-migration/components/almost-there/almost-there.stories.js new file mode 100644 index 00000000..4a81f893 --- /dev/null +++ b/src/ui/lace-migration/components/almost-there/almost-there.stories.js @@ -0,0 +1,17 @@ +import { AlmostThere } from './almost-there.component'; + +const meta = { + title: 'Nami Migration/Screens/AlmostThere', + component: AlmostThere, + parameters: { + layout: 'centered', + }, + args: { + isLaceInstalled: true, + onAction: () => {}, + }, +}; + +export default meta; + +export const Primary = {}; diff --git a/src/ui/lace-migration/components/carousel/carousel.component.jsx b/src/ui/lace-migration/components/carousel/carousel.component.jsx new file mode 100644 index 00000000..05031501 --- /dev/null +++ b/src/ui/lace-migration/components/carousel/carousel.component.jsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; +import { Box, Button, useColorModeValue, Flex } from '@chakra-ui/react'; +import { ReactComponent as Left } from '../../assets/chevron-left.svg'; +import { ReactComponent as Right } from '../../assets/chevron-right.svg'; +const CarouselButton = ({ children, ...rest }) => ( + +); + +export const Carousel = ({ children, onSlideSwitched }) => { + const [currentIndex, setCurrentIndex] = useState(0); + + const prevSlide = () => { + setCurrentIndex((prevIndex) => { + const nextIndex = prevIndex === 0 ? children.length - 1 : prevIndex - 1; + onSlideSwitched?.(nextIndex); + return nextIndex; + }); + }; + + const nextSlide = () => { + setCurrentIndex((prevIndex) => { + const nextIndex = prevIndex === children.length - 1 ? 0 : prevIndex + 1; + onSlideSwitched?.(nextIndex); + return nextIndex; + }); + }; + + return ( + + + + + + + + {children[currentIndex]} + + + + + + + ); +}; diff --git a/src/ui/lace-migration/components/carousel/carousel.stories.jsx b/src/ui/lace-migration/components/carousel/carousel.stories.jsx new file mode 100644 index 00000000..e8bca605 --- /dev/null +++ b/src/ui/lace-migration/components/carousel/carousel.stories.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Carousel } from './carousel.component'; +import { Slide1 } from './slides/Slide1.component'; +import { Slide2 } from './slides/Slide2.component'; +import { Slide3 } from './slides/Slide3.component'; +import { AlmostThere } from '../almost-there/almost-there.component'; +import { AllDone } from '../all-done/all-done.component'; + +const meta = { + title: 'Nami Migration/Screens/Carousel', + component: Carousel, + parameters: { + layout: 'centered', + }, + args: { + children: [ + {}} />, + {}} />, + {}} />, + {}} />, + {}} />, + ], + }, +}; + +export default meta; + +export const Primary = {}; diff --git a/src/ui/lace-migration/components/carousel/slides/Slide1.component.jsx b/src/ui/lace-migration/components/carousel/slides/Slide1.component.jsx new file mode 100644 index 00000000..2d2c0fc9 --- /dev/null +++ b/src/ui/lace-migration/components/carousel/slides/Slide1.component.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Box } from '@chakra-ui/react'; +import { Slide } from '../../slide.component'; +import { ReactComponent as Arrow } from '../../../assets/arrow.svg'; +import { ReactComponent as BackpackImg } from '../../../assets/backpack.svg'; + +export const Slide1 = ({ onAction, isDismissable, dismissibleSeconds }) => { + return ( + + + + } + description="The Nami Wallet is now integrated into Lace. Click 'Upgrade your wallet' to begin the process." + buttonText="Upgrade your wallet" + buttonIcon={Arrow} + onButtonClick={onAction} + isDismissable={isDismissable} + dismissibleSeconds={dismissibleSeconds} + buttonOrientation={isDismissable ? 'column' : 'row'} + /> + ); +}; diff --git a/src/ui/lace-migration/components/carousel/slides/Slide1.stories.js b/src/ui/lace-migration/components/carousel/slides/Slide1.stories.js new file mode 100644 index 00000000..d8e1e7c3 --- /dev/null +++ b/src/ui/lace-migration/components/carousel/slides/Slide1.stories.js @@ -0,0 +1,17 @@ +import { Slide1 } from './Slide1.component'; + +const meta = { + title: 'Nami Migration/Screens/Carousel/Slide 1', + component: Slide1, + parameters: { + layout: 'centered', + }, + args: { + onAction: () => {}, + isDismissable: true, + }, +}; + +export default meta; + +export const Primary = {}; diff --git a/src/ui/lace-migration/components/carousel/slides/Slide2.component.jsx b/src/ui/lace-migration/components/carousel/slides/Slide2.component.jsx new file mode 100644 index 00000000..b3e56c2a --- /dev/null +++ b/src/ui/lace-migration/components/carousel/slides/Slide2.component.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Slide } from '../../slide.component'; +import { ReactComponent as Arrow } from '../../../assets/arrow.svg'; +import { ReactComponent as SeamlessDark } from '../../../assets/grouped-dark-mode.svg'; +import { ReactComponent as SeamlessWhite } from '../../../assets/grouped-white-mode.svg'; +import { useColorMode, Box } from '@chakra-ui/react'; + +export const Slide2 = ({ onAction, isDismissable, dismissibleSeconds }) => { + const { colorMode } = useColorMode(); + return ( + + {colorMode === 'light' ? ( + + ) : ( + + )} + + } + description="On the surface, Nami is the same. But now, with Lace's advanced technology supporting it." + buttonText="Upgrade your wallet" + buttonIcon={Arrow} + onButtonClick={onAction} + isDismissable={isDismissable} + dismissibleSeconds={dismissibleSeconds} + buttonOrientation={isDismissable ? 'column' : 'row'} + /> + ); +}; diff --git a/src/ui/lace-migration/components/carousel/slides/Slide2.stories.js b/src/ui/lace-migration/components/carousel/slides/Slide2.stories.js new file mode 100644 index 00000000..5769c7d6 --- /dev/null +++ b/src/ui/lace-migration/components/carousel/slides/Slide2.stories.js @@ -0,0 +1,16 @@ +import { Slide2 } from './Slide2.component'; + +const meta = { + title: 'Nami Migration/Screens/Carousel/Slide 2', + component: Slide2, + parameters: { + layout: 'centered', + }, + args: { + onAction: () => {}, + }, +}; + +export default meta; + +export const Primary = {}; diff --git a/src/ui/lace-migration/components/carousel/slides/Slide3.component.jsx b/src/ui/lace-migration/components/carousel/slides/Slide3.component.jsx new file mode 100644 index 00000000..ce248b07 --- /dev/null +++ b/src/ui/lace-migration/components/carousel/slides/Slide3.component.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Slide } from '../../slide.component'; +import { ReactComponent as Arrow } from '../../../assets/arrow.svg'; +import { Box } from '@chakra-ui/react'; +import { ReactComponent as FeaturesImg } from '../../../assets/features.svg'; + +export const Slide3 = ({ onAction, isDismissable, dismissibleSeconds }) => { + return ( + + + + } + buttonText="Upgrade your wallet" + buttonIcon={Arrow} + onButtonClick={onAction} + isDismissable={isDismissable} + dismissibleSeconds={dismissibleSeconds} + buttonOrientation={isDismissable ? 'column' : 'row'} + /> + ); +}; diff --git a/src/ui/lace-migration/components/carousel/slides/Slide3.stories.js b/src/ui/lace-migration/components/carousel/slides/Slide3.stories.js new file mode 100644 index 00000000..06ffe721 --- /dev/null +++ b/src/ui/lace-migration/components/carousel/slides/Slide3.stories.js @@ -0,0 +1,16 @@ +import { Slide3 } from './Slide3.component'; + +const meta = { + title: 'Nami Migration/Screens/Carousel/Slide 3', + component: Slide3, + parameters: { + layout: 'centered', + }, + args: { + onAction: () => {}, + }, +}; + +export default meta; + +export const Primary = {}; diff --git a/src/ui/lace-migration/components/dismiss-btn.jsx b/src/ui/lace-migration/components/dismiss-btn.jsx new file mode 100644 index 00000000..f5ec7379 --- /dev/null +++ b/src/ui/lace-migration/components/dismiss-btn.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Button, Flex, useColorModeValue } from '@chakra-ui/react'; +import { Text } from './text.component'; +import { dismissMigration } from '../../../api/migration-tool/cross-extension-messaging/nami-migration-client.extension'; +import { ReactComponent as PendingDark } from '../assets/clock.svg'; + +export const DismissBtn = ({ dismissableIntervalSeconds, hasIcon }) => { + const futureDate = new Date(); + const futureTime = futureDate.setTime( + futureDate.getTime() + dismissableIntervalSeconds * 1000 + ); + const textColor = useColorModeValue('#6F7786', '#FFFFFF'); + const Icon = !!hasIcon && ; + + return ( + + ); +}; diff --git a/src/ui/lace-migration/components/get-color.js b/src/ui/lace-migration/components/get-color.js new file mode 100644 index 00000000..011c1999 --- /dev/null +++ b/src/ui/lace-migration/components/get-color.js @@ -0,0 +1,18 @@ +const primary = { + default: '#3D3B39', + _dark: '#FFFFFF', +}; + +const secondary = { default: '#FFFFFF', _dark: '#3D3B39' }; + +export const getColor = (color = 'primary', colorMode) => { + const isLight = colorMode === 'light'; + switch (color) { + case 'secondary': + return isLight ? secondary.default : secondary._dark; + case 'primary': + return isLight ? primary.default : primary._dark; + default: + return color; + } +}; diff --git a/src/ui/lace-migration/components/index.js b/src/ui/lace-migration/components/index.js new file mode 100644 index 00000000..a5bc5752 --- /dev/null +++ b/src/ui/lace-migration/components/index.js @@ -0,0 +1,6 @@ +export { AllDone } from './all-done/all-done.component'; +export { AlmostThere } from './almost-there/almost-there.component'; +export { Carousel } from './carousel/carousel.component'; +export { Slide1 } from './carousel/slides/Slide1.component'; +export { Slide3 } from './carousel/slides/Slide3.component'; +export { SeamlessUpgrade } from './seamless-upgrade/seamless-upgrade.component'; diff --git a/src/ui/lace-migration/components/migration-view/migration-view.component.jsx b/src/ui/lace-migration/components/migration-view/migration-view.component.jsx new file mode 100644 index 00000000..df6a6dda --- /dev/null +++ b/src/ui/lace-migration/components/migration-view/migration-view.component.jsx @@ -0,0 +1,191 @@ +import React from 'react'; +import { MigrationState } from '../../../../api/migration-tool/migrator/migration-state.data'; +import { Carousel } from '../carousel/carousel.component'; +import { Slide1 } from '../carousel/slides/Slide1.component'; +import { Slide2 } from '../carousel/slides/Slide2.component'; +import { Slide3 } from '../carousel/slides/Slide3.component'; +import { AlmostThere } from '../almost-there/almost-there.component'; +import { AllDone } from '../all-done/all-done.component'; +import { NoWallet } from '../no-wallet/no-wallet.component'; +import { useColorModeValue, Flex } from '@chakra-ui/react'; +import { useFeatureFlagsContext } from '../../../../features/feature-flags/provider'; + +export const MigrationView = ({ + migrationState, + isLaceInstalled, + onSlideSwitched, + onWaitingForLaceScreenViewed, + onOpenLaceScreenViewed, + onAllDoneScreenViewed, + onUpgradeWalletClicked, + onDownloadLaceClicked, + onOpenLaceClicked, + onNoWalletActionClick, + hasWallet, +}) => { + const panelBg = useColorModeValue('#349EA3', 'gray.800'); + const bgColor = useColorModeValue('#FFF', '#1A202C'); + const { featureFlags } = useFeatureFlagsContext(); + const isDismissable = + featureFlags?.['is-migration-active']?.dismissable || false; + + const dismissibleSeconds = + featureFlags?.['is-migration-active']?.dismissInterval; + + if (!hasWallet) { + return ( + + + + + + ); + } + + switch (migrationState) { + case MigrationState.Dismissed: + case MigrationState.None: + return ( + + + + + + + + + + ); + + case MigrationState.InProgress: + if (!isLaceInstalled) { + onWaitingForLaceScreenViewed?.(); + return ( + + + + + + ); + } else { + onOpenLaceScreenViewed?.(); + return ( + + + + + + ); + } + + case MigrationState.Completed: + onAllDoneScreenViewed?.(); + return ( + + + + + + ); + } +}; diff --git a/src/ui/lace-migration/components/migration-view/migration-view.stories.js b/src/ui/lace-migration/components/migration-view/migration-view.stories.js new file mode 100644 index 00000000..9ab79b69 --- /dev/null +++ b/src/ui/lace-migration/components/migration-view/migration-view.stories.js @@ -0,0 +1,48 @@ +import { MigrationView } from './migration-view.component'; + +export default { + title: 'Nami Migration/State Flow', + component: MigrationView, + parameters: { + layout: 'centered', + actions: { argTypesRegex: '^on.*' }, + }, +}; + +export const None = { + args: { + migrationState: 'none', + hasWallet: true, + }, +}; + +export const WaitingForLace = { + args: { + migrationState: 'in-progress', + isLaceInstalled: false, + hasWallet: true, + }, +}; + +export const InProgress = { + args: { + migrationState: 'in-progress', + isLaceInstalled: true, + hasWallet: true, + }, +}; + +export const Completed = { + args: { + migrationState: 'completed', + isLaceInstalled: true, + hasWallet: true, + }, +}; + +export const NoWallet = { + args: { + migrationState: 'none', + hasWallet: false, + }, +}; diff --git a/src/ui/lace-migration/components/migration.component.jsx b/src/ui/lace-migration/components/migration.component.jsx new file mode 100644 index 00000000..3d778cf9 --- /dev/null +++ b/src/ui/lace-migration/components/migration.component.jsx @@ -0,0 +1,176 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { storage } from 'webextension-polyfill'; +import { + MIGRATION_KEY, + MigrationState, + DISMISS_MIGRATION_UNTIL, +} from '../../../api/migration-tool/migrator/migration-state.data'; +import { MigrationView } from './migration-view/migration-view.component'; +import { + checkLaceInstallation, + enableMigration, + openLace, +} from '../../../api/migration-tool/cross-extension-messaging/nami-migration-client.extension'; +import { useCaptureEvent } from '../../../features/analytics/hooks'; +import { Events } from '../../../features/analytics/events'; +import { STORAGE } from '../../../config/config'; +import { setStorage, getAccounts } from '../../../api/extension'; +import { useFeatureFlagsContext } from '../../../features/feature-flags/provider'; +import { App } from '../../app'; +import secrets from '../../../config/provider'; + +const isDismissedTimeInPast = (dismissedUntil) => + !!dismissedUntil && dismissedUntil > Date.now(); + +export const AppWithMigration = () => { + const captureEvent = useCaptureEvent(); + const [state, setState] = useState({ + migrationState: MigrationState.None, + isLaceInstalled: false, + ui: 'loading', + hasWallet: false, + dismissedUntil: undefined, + }); + const themeColor = localStorage['chakra-ui-color-mode']; + const { featureFlags, isFFLoaded, earlyAccessFeatures } = + useFeatureFlagsContext(); + + useEffect(() => { + storage.local.get().then((store) => { + // Wait for Lace installation check before declaring UI to be ready + checkLaceInstallation().then((laceInstalled) => { + // Check if the wallet exists + getAccounts().then((accounts) => { + setState((s) => ({ + ...s, + ui: 'ready', + isLaceInstalled: laceInstalled, + migrationState: store[MIGRATION_KEY] ?? MigrationState.None, + hasWallet: typeof accounts !== 'undefined', + dismissedUntil: store[DISMISS_MIGRATION_UNTIL] ?? undefined, + })); + // Capture events for initial migration state when Nami is opened + switch (store[MIGRATION_KEY]) { + case MigrationState.Dismissed: + return captureEvent(Events.NamiMigrationDismissed); + case undefined: + case MigrationState.None: + return captureEvent(Events.NamiOpenedMigrationNotStarted); + case MigrationState.InProgress: + return laceInstalled + ? captureEvent(Events.NamiOpenedMigrationInProgress) + : captureEvent(Events.NamiOpenedMigrationWaitingForLace); + case MigrationState.Completed: + return captureEvent(Events.NamiOpenedMigrationCompleted); + } + }); + }); + }); + }, []); + + useEffect(() => { + const observeMigrationState = async (changes) => { + setState((s) => ({ + ...s, + migrationState: changes[MIGRATION_KEY]?.newValue ?? s.migrationState, + dismissedUntil: + changes[DISMISS_MIGRATION_UNTIL]?.newValue ?? s.dismissedUntil, + })); + }; + + storage.local.onChanged.addListener(observeMigrationState); + return () => storage.onChanged.removeListener(observeMigrationState); + }, []); + + useEffect(() => { + setStorage({ [STORAGE.themeColor]: themeColor }); + }, [themeColor]); + + const shouldShowApp = useMemo(() => { + let showApp = true; + + const isBetaProgramIsActive = + !!earlyAccessFeatures && + earlyAccessFeatures?.some((eaf) => eaf.name === 'beta-partner'); + + const isBetaProgramActiveAndUserEnrolled = + isBetaProgramIsActive && + featureFlags?.['is-migration-active'] !== undefined; + + if (state.migrationState === MigrationState.Completed) { + showApp = false; + } else if (isBetaProgramActiveAndUserEnrolled) { + // Canary phase entry + // Check if the migration state is dormant aka not yet chosen settings to upgrade wallet + if (state.migrationState !== MigrationState.Dormant) { + showApp = + isDismissedTimeInPast(state.dismissedUntil) && + state.migrationState !== MigrationState.InProgress; + } + } else if (featureFlags?.['is-migration-active'] !== undefined) { + if (!!featureFlags['is-migration-active'].dismissable) { + // Phase 2-3 entry dismissible with gradual rollout + showApp = + isDismissedTimeInPast(state.dismissedUntil) && + state.migrationState !== MigrationState.InProgress; + } else { + // Phase 4 entry - non-dismissible + showApp = false; + } + } + + return showApp; + }, [state, featureFlags, earlyAccessFeatures]); + + if (shouldShowApp && isFFLoaded) { + return ; + } + + return state.ui === 'loading' || !isFFLoaded ? null : ( + { + await captureEvent(Events.MigrationSlideSwitched); + await captureEvent(Events.MigrationSlideViewed, { + slideIndex: nextSlideIndex, + }); + }} + onUpgradeWalletClicked={() => { + enableMigration(); + captureEvent(Events.MigrationUpgradeYourWalletClicked); + }} + onWaitingForLaceScreenViewed={() => { + captureEvent(Events.MigrationDownloadLaceScreenViewed); + }} + onOpenLaceScreenViewed={() => { + captureEvent(Events.MigrationOpenLaceScreenViewed); + }} + onDownloadLaceClicked={() => { + captureEvent(Events.MigrationDownloadLaceClicked); + window.open( + `https://chromewebstore.google.com/detail/lace/${secrets.LACE_EXTENSION_ID}` + ); + }} + onOpenLaceClicked={() => { + captureEvent(Events.MigrationOpenLaceClicked); + openLace(); + }} + onAllDoneScreenViewed={() => { + captureEvent(Events.MigrationAllDoneScreenViewed); + }} + onNoWalletActionClick={() => { + if (state.isLaceInstalled) { + captureEvent(Events.MigrationOpenLaceClicked, { noWallet: true }); + openLace(); + } else { + captureEvent(Events.MigrationDownloadLaceClicked, { noWallet: true }); + window.open( + `https://chromewebstore.google.com/detail/lace/${secrets.LACE_EXTENSION_ID}` + ); + } + }} + /> + ); +}; diff --git a/src/ui/lace-migration/components/no-wallet/no-wallet.component.jsx b/src/ui/lace-migration/components/no-wallet/no-wallet.component.jsx new file mode 100644 index 00000000..2b9bb7b6 --- /dev/null +++ b/src/ui/lace-migration/components/no-wallet/no-wallet.component.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Slide } from '../slide.component'; +import { ReactComponent as LaceIcon } from '../../assets/lace-icon.svg'; +import { ReactComponent as BackpackImg } from '../../assets/backpack.svg'; +import { Box } from '@chakra-ui/react'; + +export const NoWallet = ({ onAction, isDismissable, dismissibleSeconds }) => ( + + + + } + description="To create or import a wallet, proceed using the Lace extension." + buttonText="Get started with Lace" + buttonIcon={LaceIcon} + onButtonClick={onAction} + isDismissable={isDismissable} + buttonOrientation={isDismissable ? 'column' : 'row'} + dismissibleSeconds={dismissibleSeconds} + showTerms={false} + showFindOutMore + /> +); diff --git a/src/ui/lace-migration/components/no-wallet/no-wallet.stories.js b/src/ui/lace-migration/components/no-wallet/no-wallet.stories.js new file mode 100644 index 00000000..f2009053 --- /dev/null +++ b/src/ui/lace-migration/components/no-wallet/no-wallet.stories.js @@ -0,0 +1,17 @@ +import { NoWallet } from './no-wallet.component'; + +const meta = { + title: 'Nami Migration/Screens/NoWallet', + component: NoWallet, + parameters: { + layout: 'centered', + }, + args: { + isLaceInstalled: true, + onAction: () => {}, + }, +}; + +export default meta; + +export const Primary = {}; diff --git a/src/ui/lace-migration/components/slide.component.jsx b/src/ui/lace-migration/components/slide.component.jsx new file mode 100644 index 00000000..616a9067 --- /dev/null +++ b/src/ui/lace-migration/components/slide.component.jsx @@ -0,0 +1,208 @@ +import React from 'react'; +import { Flex, Box, Button, Link, useColorModeValue } from '@chakra-ui/react'; +import { Text } from './text.component'; +import { DismissBtn } from './dismiss-btn'; + +export const Slide = ({ + title, + image, + description, + showTerms, + buttonText, + buttonIcon: Icon, + onButtonClick, + noWallet, + isDismissable, + dismissibleSeconds, + buttonOrientation, + showFindOutMore, +}) => { + const borderColor = useColorModeValue('#C0C0C0', '#383838'); + const slideBoxBgColor = useColorModeValue('#FFFFFF', '#2D3848'); + const termsTextColor = useColorModeValue('#6F7786', '#FFFFFF'); + const buttonTextColor = useColorModeValue('#FFFFFF', '#000000'); + const buttonBgColor = useColorModeValue('#549CA1', '#4FD1C5'); + const noWalletButtonColor = useColorModeValue('#3D3B39', '#fff'); + const noWalletButtonBg = useColorModeValue( + 'linear-gradient(#fff, #fff, #fff, #fff, #fff, #fff) padding-box, linear-gradient(94.22deg, #ff92e1 -18.3%, #fdc300 118.89%) border-box', + 'linear-gradient(rgb(46, 46, 46), rgb(46, 46, 46), rgb(46, 46, 46), rgb(46, 46, 46), rgb(46, 46, 46), rgb(46, 46, 46)) padding-box, linear-gradient(94.22deg, #ff92e1 -18.3%, #fdc300 118.89%) border-box' + ); + const noWalletButtonBgHover = useColorModeValue( + 'linear-gradient(#fff, #fff, #fff, #fff, #fff, #fff) padding-box, linear-gradient(94.22deg, #ff92e1 -18.3%, #fdc300 118.89%) border-box', + 'linear-gradient(#000, #000, #000, #000, #000, #000) padding-box, linear-gradient(94.22deg, #ff92e1 -18.3%, #fdc300 118.89%) border-box' + ); + + const getButtons = ({ noWallet }) => { + if (noWallet) { + // No Wallet Button with Lace icon + return ( + + ); + } + // Default Button + return ( + + ); + }; + + const getTermsContent = () => { + return ( + <> + + By clicking 'Upgrade your wallet', you have read and agree + + + to our{' '} + + Terms and Conditions + {' '} + and{' '} + + Privacy Policy + + . + + + ); + }; + + const getFindOutMore = () => { + return ( + <> + + To keep using Nami, enable 'Nami mode' on Lace settings. + + + + Find out more + {' '} + + + ); + }; + + return ( + + + + + {title} + + {image} + + {description} + + FAQ + + + + + + {showTerms && ( + + {getTermsContent()} + + )} + {showFindOutMore && ( + + {getFindOutMore()} + + )} + + {buttonText && getButtons({ noWallet })} + {isDismissable && !!dismissibleSeconds && ( + + )} + + + + ); +}; diff --git a/src/ui/lace-migration/components/text.component.jsx b/src/ui/lace-migration/components/text.component.jsx new file mode 100644 index 00000000..f6b1c762 --- /dev/null +++ b/src/ui/lace-migration/components/text.component.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Text as ChakraText, useColorMode } from '@chakra-ui/react'; +import { getColor } from './get-color'; + +export const Text = ({ children, color, ...rest }) => { + const { colorMode } = useColorMode(); + return ( + + {children} + + ); +}; diff --git a/webpack.config.js b/webpack.config.js index a4ce8be1..df972630 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -8,7 +8,9 @@ var webpack = require('webpack'), TerserPlugin = require('terser-webpack-plugin'), NodePolyfillPlugin = require('node-polyfill-webpack-plugin'), ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); -const { sentryWebpackPlugin } = require("@sentry/webpack-plugin"); +const { sentryWebpackPlugin } = require('@sentry/webpack-plugin'); + +require('dotenv').config(); const ASSET_PATH = process.env.ASSET_PATH || '/'; @@ -19,7 +21,7 @@ var secretsPath = path.join(__dirname, 'secrets.' + env.NODE_ENV + '.js'); require('dotenv-defaults').config({ path: './.env', - encoding: 'utf8' + encoding: 'utf8', }); var fileExtensions = [ @@ -42,12 +44,15 @@ if (fileSystem.existsSync(secretsPath)) { const isDevelopment = process.env.NODE_ENV === 'development'; const hasSentryConfig = - !!process.env.SENTRY_AUTH_TOKEN && - !!process.env.SENTRY_ORG && - !!process.env.SENTRY_PROJECT && - !!process.env.SENTRY_DSN + !!process.env.SENTRY_AUTH_TOKEN && + !!process.env.SENTRY_ORG && + !!process.env.SENTRY_PROJECT && + !!process.env.SENTRY_DSN; -const withMaybeSentry = (p) => hasSentryConfig ? [ path.join(__dirname, 'src', 'features', 'sentry.js'), p ] : p; +const withMaybeSentry = (p) => + hasSentryConfig + ? [path.join(__dirname, 'src', 'features', 'sentry.js'), p] + : p; const envsToExpose = ['NODE_ENV']; if (hasSentryConfig) envsToExpose.push('SENTRY_DSN'); @@ -59,28 +64,25 @@ var options = { }, mode: process.env.NODE_ENV || 'development', entry: { - mainPopup: withMaybeSentry(path.join(__dirname, 'src', 'ui', 'indexMain.jsx')), - internalPopup: withMaybeSentry(path.join(__dirname, 'src', 'ui', 'indexInternal.jsx')), - hwTab: withMaybeSentry(path.join(__dirname, 'src', 'ui', 'app', 'tabs', 'hw.jsx')), - createWalletTab: withMaybeSentry(path.join( - __dirname, - 'src', - 'ui', - 'app', - 'tabs', - 'createWallet.jsx' - )), - trezorTx: withMaybeSentry(path.join(__dirname, 'src', 'ui', 'app', 'tabs', 'trezorTx.jsx')), - background: withMaybeSentry(path.join(__dirname, 'src', 'pages', 'Background', 'index.js')), - contentScript: withMaybeSentry(path.join(__dirname, 'src', 'pages', 'Content', 'index.js')), - injected: withMaybeSentry(path.join(__dirname, 'src', 'pages', 'Content', 'injected.js')), - trezorContentScript: withMaybeSentry(path.join( - __dirname, - 'src', - 'pages', - 'Content', - 'trezorContentScript.js' - )) + mainPopup: withMaybeSentry( + path.join(__dirname, 'src', 'ui', 'indexMain.jsx') + ), + internalPopup: withMaybeSentry( + path.join(__dirname, 'src', 'ui', 'indexInternal.jsx') + ), + hwTab: withMaybeSentry( + path.join(__dirname, 'src', 'ui', 'app', 'tabs', 'hw.jsx') + ), + createWalletTab: withMaybeSentry( + path.join(__dirname, 'src', 'ui', 'app', 'tabs', 'createWallet.jsx') + ), + trezorTx: withMaybeSentry( + path.join(__dirname, 'src', 'ui', 'app', 'tabs', 'trezorTx.jsx') + ), + background: path.join(__dirname, 'src', 'pages', 'Background', 'index.js'), + contentScript: path.join(__dirname, 'src', 'pages', 'Content', 'index.js'), + injected: path.join(__dirname, 'src', 'pages', 'Content', 'injected.js'), + trezorContentScript: path.join(__dirname, 'src', 'pages', 'Content', 'trezorContentScript.js'), }, chromeExtensionBoilerplate: { notHotReload: ['contentScript', 'devtools', 'injected'], @@ -149,13 +151,38 @@ var options = { options: { name: '[name].[ext]', }, - exclude: /node_modules/, + exclude: [ + /node_modules/, + path.resolve(__dirname, 'src', 'ui', 'lace-migration'), + ], }, { test: /\.html$/, loader: 'html-loader', exclude: /node_modules/, }, + { + test: /\.svg$/i, + issuer: /\.jsx?$/, + include: path.resolve(__dirname, 'src', 'ui', 'lace-migration'), + use: [ + { + loader: '@svgr/webpack', + options: { + icon: true, + exportType: 'named', + }, + }, + ], + }, + { + test: /\.png$/i, + loader: 'file-loader', + options: { + name: '[name].[ext]', + }, + include: path.resolve(__dirname, 'src', 'ui', 'lace-migration'), + }, ], }, resolve: { @@ -166,13 +193,19 @@ var options = { }, plugins: [ ...(isDevelopment ? [new ReactRefreshWebpackPlugin()] : []), - ...(hasSentryConfig ? [sentryWebpackPlugin({ - authToken: process.env.SENTRY_AUTH_TOKEN, - org: process.env.SENTRY_ORG, - project: process.env.SENTRY_PROJECT, - telemetry: false, - url: 'https://sentry.io/' - })] : []), + ...(hasSentryConfig + ? [ + sentryWebpackPlugin({ + authToken: process.env.SENTRY_AUTH_TOKEN, + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT, + telemetry: false, + include: './build', + url: 'https://sentry.io/', + ignore: ['node_modules', 'webpack.config.js'], + }), + ] + : []), new webpack.BannerPlugin({ banner: () => { return 'globalThis.document={getElementsByTagName:()=>[],createElement:()=>({ setAttribute:()=>{}}),head:{appendChild:()=>{}}};';