diff --git a/background/main.ts b/background/main.ts index 9e732a8935..7d223e326b 100644 --- a/background/main.ts +++ b/background/main.ts @@ -1099,10 +1099,6 @@ export default class Main extends BaseService { await this.keyringService.unlock(password, true) }) - keyringSliceEmitter.on("unlockKeyrings", async (password) => { - await this.keyringService.unlock(password) - }) - keyringSliceEmitter.on("lockKeyrings", async () => { await this.keyringService.lock() }) @@ -1575,6 +1571,10 @@ export default class Main extends BaseService { }) } + async unlockKeyrings(password: string): Promise { + return this.keyringService.unlock(password) + } + async getActivityDetails(txHash: string): Promise { const addressNetwork = this.store.getState().ui.selectedAccount const transaction = await this.chainService.getTransaction( diff --git a/background/redux-slices/keyrings.ts b/background/redux-slices/keyrings.ts index 798af7febf..9b74c373fb 100644 --- a/background/redux-slices/keyrings.ts +++ b/background/redux-slices/keyrings.ts @@ -30,7 +30,6 @@ export const initialState: KeyringsState = { export type Events = { createPassword: string - unlockKeyrings: string lockKeyrings: never generateNewKeyring: string | undefined deriveAddress: string @@ -148,8 +147,8 @@ export const deriveAddress = createBackgroundAsyncThunk( export const unlockKeyrings = createBackgroundAsyncThunk( "keyrings/unlockKeyrings", - async (password: string) => { - await emitter.emit("unlockKeyrings", password) + async (password: string, { extra: { main } }) => { + return { success: await main.unlockKeyrings(password) } } ) diff --git a/ui/_locales/en/messages.json b/ui/_locales/en/messages.json index d17214f1dc..80548e9068 100644 --- a/ui/_locales/en/messages.json +++ b/ui/_locales/en/messages.json @@ -113,7 +113,8 @@ "stepsExplainer": "Please follow the steps below and click on Try Again!", "step1": "Plug in a single Ledger", "step2": "Enter pin to unlock", - "step3": "Open Ethereum App" + "step3": "Open Ethereum App", + "tip": "After clicking continue, select device and click connect" }, "selectDevice": "Select the device", "clickConnect": "Click connect", @@ -300,23 +301,20 @@ }, "onboarding": { "tabbed": { + "walletShortcut": "Did you know that you can open Taho using a keyboard shortcut?", "routeBasedContent": { "newSeed": { - "tip": "If you want an easy way to preview Taho, you can start by adding a view only account", + "tip": "If you want an easy way to preview Taho, you can start by adding a read only account.", "action": "Add preview account" }, - "ledger": { - "tip": "Trezor integration coming soon, check out the open ", - "action": "Gitcoin bounty" - }, "addWallet": { - "tip": "Some of the code for this was written by Community contributors" + "tip": "Several of Taho's most popular features were developed by our community." }, "viewOnly": { - "tip": "A good way to take a peek at what Taho offers" + "tip": "A good way to take a peek at what Taho offers." }, "importSeed": { - "tip": "Taho offers the possibility of adding multiple recovery phrases" + "tip": "Taho offers the possibility of adding multiple recovery phrases." }, "default": { "fact1": "Fully owned by the community", @@ -331,11 +329,15 @@ }, "addWallet": { "title": "Use existing wallet", - "tip": "You can always add more wallets later", + "titleExisting": "Add account", + "tip": "You can always add more wallets later.", + "existingListTitle": "Existing account", + "newWalletTitle": "New wallet", "options": { "importSeed": "Import recovery phrase", "ledger": "Connect to Ledger", - "readOnly": "Read-only address" + "readOnly": "Read-only address", + "createNew": "Create new wallet" }, "importSeed": { "title": "Import secret recovery phrase", @@ -352,14 +354,19 @@ "title": "Read-only address", "subtitle": "Add an Ethereum address or ENS name to view an existing wallet in Taho", "submit": "Preview Taho", - "tip": "You can upgrade a view-only wallet later" + "tip": "You can upgrade a read-only wallet later." } }, "newWalletIntro": { "title": "Before we get started", "warning": "It's important to write down your secret recovery phrase and store it somewhere safe.

This is the only way to recover your accounts and funds.

You will not be able to export your recovery phrase later.", "submit": "Create recovery phrase", - "tip": "You can upgrade a view-only wallet later" + "tip": "You can upgrade a read-only wallet later." + }, + "unlockWallet": { + "title": "Password required", + "passwordInput": "Signing password", + "submit": "Confirm" }, "newWalletReview": { "title": "Save and store your recovery phrase", @@ -375,10 +382,11 @@ "invalidStateMsg": "Verify the order and remove the ones that aren't in the right position.", "verifyValidState": "Verify recovery phrase", "submit": "Finalize", - "tip": "If you didn’t write it down, you can start with a new phrase" + "tip": "If you didn’t write it down, you can start with a new phrase." }, "complete": { "title": "Welcome to Taho", + "titleExisting": "Account added to Taho!", "subtitle": "For faster access we recommend pinning Taho to your browser", "animationAlt": "Pin the wallet" } @@ -624,7 +632,7 @@ } }, "addNewChain": { - "subtitle": "Wants to add a main network to Tally Ho!", + "subtitle": "Wants to add a main network to Taho", "name": "Name", "chainId": "Chain ID", "currency": "Currency", diff --git a/ui/components/AccountsNotificationPanel/AccountsNotificationPanelAccounts.tsx b/ui/components/AccountsNotificationPanel/AccountsNotificationPanelAccounts.tsx index 29cd1ecad6..3bdfdd137a 100644 --- a/ui/components/AccountsNotificationPanel/AccountsNotificationPanelAccounts.tsx +++ b/ui/components/AccountsNotificationPanel/AccountsNotificationPanelAccounts.tsx @@ -435,7 +435,14 @@ export default function AccountsNotificationPanelAccounts({ size="medium" iconSmall="add" iconPosition="left" - linkTo="/onboarding/add-wallet" + onClick={() => { + if (isEnabled(FeatureFlags.SUPPORT_TABBED_ONBOARDING)) { + window.open("/tab.html#onboarding") + window.close() + } else { + history.push("/onboarding/add-wallet") + } + }} > {t("accounts.notificationPanel.addWallet")} diff --git a/ui/components/Ledger/LedgerPanelContainer.tsx b/ui/components/Ledger/LedgerPanelContainer.tsx index 54dbb1b38f..eff360a881 100644 --- a/ui/components/Ledger/LedgerPanelContainer.tsx +++ b/ui/components/Ledger/LedgerPanelContainer.tsx @@ -27,7 +27,7 @@ export default function LedgerPanelContainer({ .panel { display: flex; flex-flow: column; - max-width: 24rem; + max-width: 450px; margin: 0 auto; padding: 1rem; } diff --git a/ui/components/Onboarding/OnboardingDerivationPathSelect.tsx b/ui/components/Onboarding/OnboardingDerivationPathSelect.tsx index 4d5241b055..510572c38c 100644 --- a/ui/components/Onboarding/OnboardingDerivationPathSelect.tsx +++ b/ui/components/Onboarding/OnboardingDerivationPathSelect.tsx @@ -2,44 +2,60 @@ import React, { KeyboardEventHandler, ReactElement, useEffect, + useMemo, useRef, useState, } from "react" import { useTranslation } from "react-i18next" -import { i18n } from "../../_locales/i18n" +import { I18nKey } from "../../_locales/i18n" import SharedButton from "../Shared/SharedButton" import SharedInput from "../Shared/SharedInput" import SharedModal from "../Shared/SharedModal" import SharedSelect, { Option } from "../Shared/SharedSelect" +type DerivationPath = { + value: string + label: I18nKey + hideActiveValue?: boolean +} + +export enum DefaultPathIndex { + ledgerLive, + bip44, + ethTestnet, + ledgerLegacy, + rootstock, + rootstockTestnet, +} + // TODO make this network specific -const initialDerivationPaths: Option[] = [ - { +const defaultDerivationPaths: Record = { + [DefaultPathIndex.ledgerLive]: { value: "m/44'/60'/x'/0/0", - label: i18n.t("ledger.derivationPaths.ledgerLive"), + label: "ledger.derivationPaths.ledgerLive", }, - { + [DefaultPathIndex.bip44]: { value: "m/44'/60'/0'/0", - label: i18n.t("ledger.derivationPaths.bip44"), + label: "ledger.derivationPaths.bip44", }, - { + [DefaultPathIndex.ethTestnet]: { value: "m/44'/1'/0'/0", - label: i18n.t("ledger.derivationPaths.ethTestnet"), + label: "ledger.derivationPaths.ethTestnet", }, - { + [DefaultPathIndex.ledgerLegacy]: { value: "m/44'/60'/0'", - label: i18n.t("ledger.derivationPaths.ledgerLegacy"), + label: "ledger.derivationPaths.ledgerLegacy", hideActiveValue: true, }, - { + [DefaultPathIndex.rootstock]: { value: "m/44'/137'/0'/0", - label: i18n.t("ledger.derivationPaths.rsk"), + label: "ledger.derivationPaths.rsk", }, - { + [DefaultPathIndex.rootstockTestnet]: { value: "m/44'/37310'/0'/0", - label: i18n.t("ledger.derivationPaths.rskTestnet"), + label: "ledger.derivationPaths.rskTestnet", }, -] +} const initialCustomPath = { coinType: "0", @@ -50,16 +66,30 @@ const initialCustomPath = { export default function OnboardingDerivationPathSelect({ onChange, + defaultPath, }: { onChange: (path: string) => void + defaultPath?: DefaultPathIndex }): ReactElement { - const { t } = useTranslation("translation", { keyPrefix: "onboarding" }) - const [derivationPaths, setDerivationPaths] = useState(initialDerivationPaths) + const { t, i18n } = useTranslation("translation", { keyPrefix: "onboarding" }) + + const defaultPaths: Option[] = useMemo( + () => + Object.values(defaultDerivationPaths).map((path) => ({ + ...path, + label: i18n.t(path.label), + })), + [i18n] + ) + + const [derivationPaths, setDerivationPaths] = useState([]) const [modalStep, setModalStep] = useState(0) const [customPath, setCustomPath] = useState(initialCustomPath) const [customPathLabel, setCustomPathLabel] = useState("") - const [defaultIndex, setDefaultIndex] = useState() + const [defaultIndex, setDefaultIndex] = useState( + defaultPath + ) // Reset value to display placeholder after adding a custom path const customPathValue = customPath.isReset @@ -113,6 +143,8 @@ export default function OnboardingDerivationPathSelect({ if (e.key === "Enter") setModalStep(1) } + const derivationPathOptions = [...defaultPaths, ...derivationPaths] + return ( <> + + +
{t("addWallet.tip")}
+
+ +
{t("viewOnly.tip")}
+
+ +
{t("importSeed.tip")}
+
+ +
+ +
+
+ +
+

{t("default.fact1")}

+

{t("default.fact2")}

+

{t("default.fact3")}

+ +
+
+ + ) +} diff --git a/ui/components/Onboarding/SupportedChains.tsx b/ui/components/Onboarding/SupportedChains.tsx new file mode 100644 index 0000000000..14b6c222f6 --- /dev/null +++ b/ui/components/Onboarding/SupportedChains.tsx @@ -0,0 +1,64 @@ +import React from "react" +import { useTranslation } from "react-i18next" +import { + ARBITRUM_ONE, + AVALANCHE, + BINANCE_SMART_CHAIN, + ETHEREUM, + OPTIMISM, + POLYGON, +} from "@tallyho/tally-background/constants" +import { getNetworkIcon } from "../../utils/networks" + +// @TODO Rethink what networks we show once custom networks are supported +const productionNetworks = [ + ETHEREUM, + POLYGON, + OPTIMISM, + ARBITRUM_ONE, + AVALANCHE, + BINANCE_SMART_CHAIN, +] + +/** + * Renders a list of production network icons + */ +export default function SupportedChains(): JSX.Element { + const { t } = useTranslation("translation", { keyPrefix: "onboarding" }) + return ( +
+ {t("supportedChains")} +
+ {productionNetworks.map((network) => ( + {network.name} + ))} +
+ +
+ ) +} diff --git a/ui/components/Onboarding/WalletShortcut.tsx b/ui/components/Onboarding/WalletShortcut.tsx new file mode 100644 index 0000000000..bfed06ae53 --- /dev/null +++ b/ui/components/Onboarding/WalletShortcut.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +import browser from "webextension-polyfill" + +export default function WalletShortcut(): JSX.Element { + const [os, setOS] = useState("windows") + const { t } = useTranslation("translation") + + // Fetch the OS using the extension API to decide what shortcut to show + useEffect(() => { + const controller = new AbortController() + + browser.runtime.getPlatformInfo().then((platformInfo) => { + if (!controller.signal.aborted) { + setOS(platformInfo.os) + } + }) + + return () => controller.abort() + }, []) + + // state for alt, t, and option key status + const [tPressed, setTPressed] = useState(false) + const [altPressed, setAltPressed] = useState(false) + + // add keydown/up listeners for our shortcut code + useEffect(() => { + function downListener(e: KeyboardEvent) { + if (e.altKey || e.key === "Alt") { + setAltPressed(true) + } + if (e.key === "t") { + setTPressed(true) + } + } + + function upListener(e: KeyboardEvent) { + if (e.altKey || e.key === "Alt") { + setAltPressed(false) + } + if (e.key === "t") { + setTPressed(false) + } + } + + window.addEventListener("keydown", downListener) + window.addEventListener("keyup", upListener) + + return () => { + window.removeEventListener("keydown", downListener) + window.removeEventListener("keyup", upListener) + } + }) + return ( +
+ {t("onboarding.tabbed.walletShortcut")} + {os + +
+ ) +} diff --git a/ui/components/Shared/SharedSelect.tsx b/ui/components/Shared/SharedSelect.tsx index 4c67f4c051..d56d2dea6f 100644 --- a/ui/components/Shared/SharedSelect.tsx +++ b/ui/components/Shared/SharedSelect.tsx @@ -21,7 +21,7 @@ type Props = { onTrigger?: () => void showValue?: boolean showOptionValue?: boolean - width?: number + width?: string | number } export default function SharedSelect(props: Props): ReactElement { @@ -35,9 +35,11 @@ export default function SharedSelect(props: Props): ReactElement { onTrigger, showValue, showOptionValue, - width = 320, + width = "320px", } = props + const cssWidth = typeof width === "number" ? `${width}px` : width + const [isDropdownOpen, setIsDropdownOpen] = useState(false) const [activeIndex, setActiveIndex] = useState(defaultIndex) const previousdefaultIndex = useRef(defaultIndex) @@ -152,7 +154,7 @@ export default function SharedSelect(props: Props): ReactElement { box-sizing: border-box; display: inline-block; position: relative; - width: ${width}px; + width: ${cssWidth}; background-color: transparent; } @@ -218,7 +220,7 @@ export default function SharedSelect(props: Props): ReactElement { position: absolute; left: 2px; box-sizing: border-box; - width: ${width - 4}px; + width: calc(${cssWidth} - 4px); text-align: right; background-color: var(--green-95); border-radius: 5px; diff --git a/ui/hooks/index.ts b/ui/hooks/index.ts index d58a7f9245..89adacc594 100644 --- a/ui/hooks/index.ts +++ b/ui/hooks/index.ts @@ -1,7 +1,9 @@ import { isAllowedQueryParamPage } from "@tallyho/provider-bridge-shared" import { useState, useEffect, ReactElement, ReactNode } from "react" +import { getAllAddresses } from "@tallyho/tally-background/redux-slices/selectors/accountsSelectors" import SharedPanelSwitcher from "../components/Shared/SharedPanelSwitcher" +import { useBackgroundSelector } from "./redux-hooks" export * from "./redux-hooks" export * from "./signing-hooks" @@ -78,3 +80,7 @@ export function useSwitchablePanels(panels: PanelDescriptor[]): ReactNode { panels[panelNumber].panelElement(), ] } + +export function useIsOnboarding(): boolean { + return useBackgroundSelector(getAllAddresses).length < 1 +} diff --git a/ui/pages/Onboarding/Tabbed/AddWallet.tsx b/ui/pages/Onboarding/Tabbed/AddWallet.tsx index b7a02fed77..b460704c45 100644 --- a/ui/pages/Onboarding/Tabbed/AddWallet.tsx +++ b/ui/pages/Onboarding/Tabbed/AddWallet.tsx @@ -1,122 +1,15 @@ -import React, { ReactElement, useMemo } from "react" +import React, { ReactElement } from "react" import { useTranslation } from "react-i18next" -import { isLedgerSupported } from "@tallyho/tally-background/services/ledger" -import SharedButton from "../../../components/Shared/SharedButton" -import SharedIcon from "../../../components/Shared/SharedIcon" import OnboardingTip from "./OnboardingTip" -import OnboardingRoutes from "./Routes" +import { useIsOnboarding } from "../../../hooks" +import OnboardingAdditionalWallet from "./OnboardingAdditionalWallet" +import AddWalletOptions from "./AddWalletOptions" -const intersperseWith = (items: T[], getItem: (index: number) => K) => { - const result: (T | K)[] = [] - - for (let i = 0; i < items.length; i += 1) { - const element = items[i] - - result.push(element) - - if (i < items.length - 1) { - result.push(getItem(i)) - } - } - return result -} - -function AddWalletRow({ - icon, - url, - label, - onClick, -}: { - icon: string - label: string - url?: string - onClick?: () => void -}) { - return ( - -
-
- - {label} -
-
- -
- - - ) -} - -export default function AddWallet(): ReactElement { +function OnboardingAddWallet(): ReactElement { const { t } = useTranslation("translation", { keyPrefix: "onboarding.tabbed.addWallet", }) - const optionsWithSpacer = useMemo(() => { - const options = [ - { - label: t("options.importSeed"), - icon: "add_wallet/import.svg", - url: OnboardingRoutes.IMPORT_SEED, - isAvailable: true, - }, - { - label: t("options.ledger"), - icon: "add_wallet/ledger.svg", - url: OnboardingRoutes.LEDGER, - isAvailable: isLedgerSupported, - }, - { - label: t("options.readOnly"), - icon: "add_wallet/preview.svg", - url: OnboardingRoutes.VIEW_ONLY_WALLET, - isAvailable: true, - }, - ].filter((item) => item.isAvailable) - - return intersperseWith(options, () => "spacer" as const) - }, [t]) - return (
@@ -131,25 +24,8 @@ export default function AddWallet(): ReactElement {
-
    - {optionsWithSpacer.map((option, i) => { - if (option === "spacer") { - const key = `option-${i}` - - return ( -
  • -
    -
  • - ) - } - - const { label, icon, url } = option - return ( -
  • - -
  • - ) - })} +
      +
{t("tip")} @@ -182,7 +58,13 @@ export default function AddWallet(): ReactElement { margin: 0; } - ul { + .actions_container { + margin-bottom: 24px; + width: 100%; + max-width: 348px; + } + + .list_container { display: flex; flex-direction: column; background-color: var(--green-95); @@ -190,43 +72,16 @@ export default function AddWallet(): ReactElement { padding: 24px; gap: 24px; } - - li { - display: flex; - } - - .spacer { - width: 100%; - border: 0.5px solid var(--green-120); - } - - .actions_container { - margin-bottom: 24px; - } - - .top { - display: flex; - width: 100%; - justify-content: space-between; - align-items: center; - margin-bottom: 5px; - padding-top: 68.5px; - } - - .icon_close { - mask-image: url("./images/close.svg"); - mask-size: cover; - width: 11px; - height: 11px; - margin-right: 10px; - margin-top: 2px; - } - - .icon_close:hover { - background-color: var(--green-20); - } `} ) } + +export default function AddWallet(): JSX.Element { + const isOnboarding = useIsOnboarding() + + if (isOnboarding) return + + return +} diff --git a/ui/pages/Onboarding/Tabbed/AddWalletOptions.tsx b/ui/pages/Onboarding/Tabbed/AddWalletOptions.tsx new file mode 100644 index 0000000000..99d953719e --- /dev/null +++ b/ui/pages/Onboarding/Tabbed/AddWalletOptions.tsx @@ -0,0 +1,119 @@ +import React, { useMemo } from "react" +import { useTranslation } from "react-i18next" +import { isLedgerSupported } from "@tallyho/tally-background/services/ledger" +import OnboardingRoutes from "./Routes" +import { intersperseWith } from "../../../utils/lists" +import SharedButton from "../../../components/Shared/SharedButton" +import SharedIcon from "../../../components/Shared/SharedIcon" + +type AddWalletRowProps = { + icon: string + label: string + url?: string + onClick?: () => void +} + +export function AddWalletRow({ + icon, + url, + label, + onClick, +}: AddWalletRowProps): JSX.Element { + return ( + +
+ + {label} + +
+ +
+ ) +} + +export default function AddWalletOptions(): JSX.Element { + const { t } = useTranslation("translation", { + keyPrefix: "onboarding.tabbed.addWallet", + }) + + const optionsWithSpacer = useMemo(() => { + const options = [ + { + label: t("options.importSeed"), + icon: "add_wallet/import.svg", + url: OnboardingRoutes.IMPORT_SEED, + isAvailable: true, + }, + { + label: t("options.ledger"), + icon: "add_wallet/ledger.svg", + url: OnboardingRoutes.LEDGER, + isAvailable: isLedgerSupported, + }, + { + label: t("options.readOnly"), + icon: "add_wallet/preview.svg", + url: OnboardingRoutes.VIEW_ONLY_WALLET, + isAvailable: true, + }, + ].filter((item) => item.isAvailable) + + return intersperseWith(options, (i) => `spacer-${i}` as const) + }, [t]) + + return ( + <> + {optionsWithSpacer.map((option) => { + if (typeof option === "string") { + return
  • + } + + const { label, icon, url } = option + return ( +
  • + +
  • + ) + })} + + + ) +} diff --git a/ui/pages/Onboarding/Tabbed/Done.tsx b/ui/pages/Onboarding/Tabbed/Done.tsx index c97beadf0f..e0cc469a1a 100644 --- a/ui/pages/Onboarding/Tabbed/Done.tsx +++ b/ui/pages/Onboarding/Tabbed/Done.tsx @@ -1,11 +1,111 @@ +import { getAllAddresses } from "@tallyho/tally-background/redux-slices/selectors" import React, { ReactElement } from "react" import { useTranslation } from "react-i18next" +// eslint-disable-next-line import/no-extraneous-dependencies +import css from "styled-jsx/css" +import WalletShortcut from "../../../components/Onboarding/WalletShortcut" +import { useBackgroundSelector } from "../../../hooks" + +const styles = css` + section { + text-align: center; + } + header { + display: flex; + flex-direction: column; + gap: 24px; + align-items: center; + margin-bottom: 32px; + } + + header div { + display: flex; + flex-direction: column; + gap: 12px; + } + + header h1 { + display: inline-block; + font-family: "Quincy CF"; + font-weight: 500; + font-size: 36px; + line-height: 42px; + margin: 0; + color: var(--white); + } + + header span { + font-family: "Segment"; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: var(--green-40); + } + + header img { + border-radius: 22px; + } + + .wrapper { + position: relative; + z-index: 1; + } + + .confetti { + position: absolute; + display: none; + opacity: 0.7; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + .shortcut_container { + background: var(--green-95); + padding: 35px 32px 29px; + max-width: 402px; + margin: 0 auto; + border-radius: 4px; + } +` export default function Done(): ReactElement { const { t } = useTranslation("translation", { keyPrefix: "onboarding.tabbed.complete", }) + const firstAddress = useBackgroundSelector(getAllAddresses).length === 1 + + if (!firstAddress) { + return ( +
    +
    + Confetti +
    +
    +
    + Taho Gold +
    +

    {t("titleExisting")}

    +
    +
    +
    +
    + +
    + +
    + ) + } + return (
    @@ -31,64 +131,7 @@ export default function Done(): ReactElement { alt={t("animationAlt")} />
    - - +
    ) } diff --git a/ui/pages/Onboarding/Tabbed/ImportSeed.tsx b/ui/pages/Onboarding/Tabbed/ImportSeed.tsx index b4fb5429d7..b7d5e7d2d1 100644 --- a/ui/pages/Onboarding/Tabbed/ImportSeed.tsx +++ b/ui/pages/Onboarding/Tabbed/ImportSeed.tsx @@ -6,7 +6,9 @@ import { FeatureFlags, isEnabled } from "@tallyho/tally-background/features" import { useTranslation } from "react-i18next" import { selectCurrentNetwork } from "@tallyho/tally-background/redux-slices/selectors" import SharedButton from "../../../components/Shared/SharedButton" -import OnboardingDerivationPathSelect from "../../../components/Onboarding/OnboardingDerivationPathSelect" +import OnboardingDerivationPathSelect, { + DefaultPathIndex, +} from "../../../components/Onboarding/OnboardingDerivationPathSelect" import { useBackgroundDispatch, useBackgroundSelector, @@ -84,88 +86,95 @@ export default function ImportSeed(props: Props): ReactElement { ) return ( - <> -
    -
    { - event.preventDefault() - importWallet() - }} - > -
    -
    -

    {t("title")}

    -
    {t("subtitle")}
    -
    -
    { - e.preventDefault() - const text = e.clipboardData.getData("text/plain").trim() - e.currentTarget.innerText = text - setRecoveryPhrase(text) - }} - onDrop={(e) => { - e.preventDefault() - const text = e.dataTransfer.getData("text/plain").trim() - e.currentTarget.innerText = text - setRecoveryPhrase(text) - }} - onInput={(e) => { - setRecoveryPhrase(e.currentTarget.innerText.trim()) - }} - /> -
    - {t("inputLabel")} -
    - {errorMessage &&

    {errorMessage}

    } -
    - {!isEnabled(FeatureFlags.HIDE_IMPORT_DERIVATION_PATH) && ( -
    - -
    - )} +
    +
    +
    +

    {t("title")}

    +
    {t("subtitle")}
    +
    + { + event.preventDefault() + importWallet() + }} + > + {!isEnabled(FeatureFlags.HIDE_IMPORT_DERIVATION_PATH) && ( +
    +
    -
    - - {t("submit")} - - {!isEnabled(FeatureFlags.HIDE_IMPORT_DERIVATION_PATH) && ( - - )} + )} +
    +
    { + e.preventDefault() + const text = e.clipboardData.getData("text/plain").trim() + e.currentTarget.innerText = text + setRecoveryPhrase(text) + }} + onDrop={(e) => { + e.preventDefault() + const text = e.dataTransfer.getData("text/plain").trim() + e.currentTarget.innerText = text + setRecoveryPhrase(text) + }} + onInput={(e) => { + setRecoveryPhrase(e.currentTarget.innerText.trim()) + }} + /> +
    + {t("inputLabel")}
    - -
    + {errorMessage &&

    {errorMessage}

    } +
    +
    + + {t("submit")} + + {!isEnabled(FeatureFlags.HIDE_IMPORT_DERIVATION_PATH) && ( + + )} +
    + - +
    ) } diff --git a/ui/pages/Onboarding/Tabbed/Ledger/Ledger.tsx b/ui/pages/Onboarding/Tabbed/Ledger/Ledger.tsx index acea82b59e..d931dd231b 100644 --- a/ui/pages/Onboarding/Tabbed/Ledger/Ledger.tsx +++ b/ui/pages/Onboarding/Tabbed/Ledger/Ledger.tsx @@ -3,12 +3,12 @@ import React, { ReactElement, useState } from "react" import { ledgerUSBVendorId } from "@ledgerhq/devices" import { LedgerProductDatabase } from "@tallyho/tally-background/services/ledger" import { useTranslation } from "react-i18next" +import { useHistory } from "react-router-dom" import LedgerPanelContainer from "../../../../components/Ledger/LedgerPanelContainer" import { useBackgroundDispatch, useBackgroundSelector } from "../../../../hooks" -import LedgerConnectPopup from "./LedgerConnectPopup" -import LedgerImportDone from "./LedgerImportDone" import LedgerImportAccounts from "./LedgerImportAccounts" import LedgerPrepare from "./LedgerPrepare" +import OnboardingRoutes from "../Routes" const filters = Object.values(LedgerProductDatabase).map( ({ productId }): USBDeviceFilter => ({ @@ -18,9 +18,7 @@ const filters = Object.values(LedgerProductDatabase).map( ) export default function Ledger(): ReactElement { - const [phase, setPhase] = useState< - "0-prepare" | "1-request" | "2-connect" | "3-done" - >("0-prepare") + const [phase, setPhase] = useState<"1-prepare" | "2-connect">("1-prepare") const { t } = useTranslation("translation", { keyPrefix: "ledger.onboarding", @@ -39,6 +37,8 @@ export default function Ledger(): ReactElement { ) const dispatch = useBackgroundDispatch() + const history = useHistory() + const connectionError = phase === "2-connect" && !device && !connecting return (
    @@ -47,12 +47,11 @@ export default function Ledger(): ReactElement { position: relative; } `} - {(phase === "0-prepare" || connectionError) && ( + {(phase === "1-prepare" || connectionError) && ( { - setPhase("1-request") try { // Open popup for testing // TODO: use result (for multiple devices)? @@ -81,8 +80,6 @@ export default function Ledger(): ReactElement { }} /> )} - - {phase === "1-request" && } {phase === "2-connect" && !device && connecting && ( { - setPhase("3-done") - }} - /> - )} - {phase === "3-done" && ( - { - window.close() + history.push(OnboardingRoutes.ONBOARDING_COMPLETE) }} /> )} diff --git a/ui/pages/Onboarding/Tabbed/Ledger/LedgerConnectPopup.tsx b/ui/pages/Onboarding/Tabbed/Ledger/LedgerConnectPopup.tsx deleted file mode 100644 index 7e05014d73..0000000000 --- a/ui/pages/Onboarding/Tabbed/Ledger/LedgerConnectPopup.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { ReactElement } from "react" -import { useTranslation } from "react-i18next" -import LedgerPanelContainer from "../../../../components/Ledger/LedgerPanelContainer" - -export default function LedgerConnectPopup(): ReactElement { - const { t } = useTranslation("translation", { - keyPrefix: "ledger.onboarding", - }) - return ( - <> - -
    -
    -
    {t("selectDevice")}
    -
    -
    -
    -
    {t("clickConnect")}
    -
    - - - ) -} diff --git a/ui/pages/Onboarding/Tabbed/Ledger/LedgerImportDone.tsx b/ui/pages/Onboarding/Tabbed/Ledger/LedgerImportDone.tsx deleted file mode 100644 index 4f29e822aa..0000000000 --- a/ui/pages/Onboarding/Tabbed/Ledger/LedgerImportDone.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { ReactElement } from "react" -import { useTranslation } from "react-i18next" -import LedgerPanelContainer from "../../../../components/Ledger/LedgerPanelContainer" -import SharedButton from "../../../../components/Shared/SharedButton" - -export default function LedgerImportDone({ - onClose, -}: { - onClose: () => void -}): ReactElement { - const { t } = useTranslation("translation", { - keyPrefix: "ledger.onboarding", - }) - return ( - - {t("doneMessageOne")} -
    - {t("doneMessageTwo")} - - } - subHeading={t("onboardingSuccessful")} - > -
    - - {t("closeTab")} - -
    - -
    - ) -} diff --git a/ui/pages/Onboarding/Tabbed/Ledger/LedgerPrepare.tsx b/ui/pages/Onboarding/Tabbed/Ledger/LedgerPrepare.tsx index 04c2949411..dcfda0310e 100644 --- a/ui/pages/Onboarding/Tabbed/Ledger/LedgerPrepare.tsx +++ b/ui/pages/Onboarding/Tabbed/Ledger/LedgerPrepare.tsx @@ -2,6 +2,7 @@ import React, { ReactElement } from "react" import { useTranslation } from "react-i18next" import LedgerContinueButton from "../../../../components/Ledger/LedgerContinueButton" import LedgerPanelContainer from "../../../../components/Ledger/LedgerPanelContainer" +import OnboardingTip from "../OnboardingTip" export default function LedgerPrepare({ onContinue, @@ -54,10 +55,12 @@ export default function LedgerPrepare({ {buttonLabel}
    + {t("tip")} + + ) +} diff --git a/ui/pages/Onboarding/Tabbed/OnboardingTip.tsx b/ui/pages/Onboarding/Tabbed/OnboardingTip.tsx index 60fb6277a0..d5d6ead1a8 100644 --- a/ui/pages/Onboarding/Tabbed/OnboardingTip.tsx +++ b/ui/pages/Onboarding/Tabbed/OnboardingTip.tsx @@ -25,11 +25,13 @@ export default function OnboardingTip({ gap: 18px; max-width: 350px; margin: 0 auto; + justify-content: center; } .quote_icon::before, .quote_icon::after { content: ""; + max-width: 100px; display: inline-block; flex-grow: 1; border: 0.5px solid var(--green-80); diff --git a/ui/pages/Onboarding/Tabbed/Root.tsx b/ui/pages/Onboarding/Tabbed/Root.tsx index 0d796fc63b..bb821bd0b1 100644 --- a/ui/pages/Onboarding/Tabbed/Root.tsx +++ b/ui/pages/Onboarding/Tabbed/Root.tsx @@ -1,19 +1,15 @@ -import React, { ReactElement, useEffect, useState } from "react" +import React, { ReactElement, useState } from "react" -import { Route, Switch, matchPath, useLocation } from "react-router-dom" - -import { useTranslation } from "react-i18next" -import browser from "webextension-polyfill" import { - ARBITRUM_ONE, - AVALANCHE, - BINANCE_SMART_CHAIN, - ETHEREUM, - OPTIMISM, - POLYGON, -} from "@tallyho/tally-background/constants" + Route, + Switch, + matchPath, + useLocation, + Redirect, +} from "react-router-dom" + +import classNames from "classnames" -import { WEBSITE_ORIGIN } from "@tallyho/tally-background/constants/website" import SharedBackButton from "../../../components/Shared/SharedBackButton" import AddWallet from "./AddWallet" import Done from "./Done" @@ -23,258 +19,27 @@ import NewSeed, { NewSeedRoutes } from "./NewSeed" import InfoIntro from "./Intro" import ViewOnlyWallet from "./ViewOnlyWallet" import Ledger from "./Ledger/Ledger" - -import SharedButton from "../../../components/Shared/SharedButton" import OnboardingRoutes from "./Routes" +import RouteBasedContent from "../../../components/Onboarding/RouteBasedContent" +import SupportedChains from "../../../components/Onboarding/SupportedChains" +import { useIsOnboarding } from "../../../hooks" + +function Navigation({ + children, + isOnboarding, +}: { + children: React.ReactNode + isOnboarding: boolean +}): ReactElement { + const location = useLocation() -// @TODO Rethink what networks we show once custom networks are supported -const productionNetworks = [ - ETHEREUM, - POLYGON, - OPTIMISM, - ARBITRUM_ONE, - AVALANCHE, - BINANCE_SMART_CHAIN, -] - -const getNetworkIcon = (networkName: string) => { - const icon = networkName.replaceAll(" ", "").toLowerCase() - - return `/images/networks/${icon}@2x.png` -} - -/** - * Renders a list of production network icons - */ -function SupportedChains(): ReactElement { - const { t } = useTranslation("translation", { keyPrefix: "onboarding" }) - return ( -
    - {t("supportedChains")} -
    - {productionNetworks.map((network) => ( - {network.name} - ))} -
    - -
    - ) -} - -const WalletShortcut = () => { - const [os, setOS] = useState("windows") - - // fetch the OS using the extension API to decide what shortcut to show - useEffect(() => { - let active = true - - async function loadOS() { - if (active) { - setOS((await browser.runtime.getPlatformInfo()).os) - } - } - - loadOS() - - return () => { - active = false - } - }, []) - - // state for alt, t, and option key status - const [tPressed, setTPressed] = useState(false) - const [altPressed, setAltPressed] = useState(false) - - // add keydown/up listeners for our shortcut code - useEffect(() => { - const downListener = (e: KeyboardEvent) => { - if (e.altKey || e.key === "Alt") { - setAltPressed(true) - } - if (e.key === "t") { - setTPressed(true) - } - } - const upListener = (e: KeyboardEvent) => { - if (e.altKey || e.key === "Alt") { - setAltPressed(false) - } - if (e.key === "t") { - setTPressed(false) - } - } - - window.addEventListener("keydown", downListener.bind(window)) - window.addEventListener("keyup", upListener.bind(window)) - - return () => { - window.removeEventListener("keydown", downListener) - window.removeEventListener("keyup", upListener) - } - }) - return ( -
    - - Did you know that you can open Taho using a keyboard shortcut? - - {os - -
    - ) -} - -function RouteBasedContent() { - const { t } = useTranslation("translation", { - keyPrefix: "onboarding.tabbed.routeBasedContent", - }) - return ( - - -
    - {t("newSeed.tip")} - - {t("newSeed.action")} - -
    - -
    - -
    - {t("ledger.tip")} - - {t("ledger.action")} - -
    - -
    - -
    {t("addWallet.tip")}
    -
    - -
    {t("viewOnly.tip")}
    -
    - -
    {t("importSeed.tip")}
    -
    - -
    - -
    -
    - -
    -

    {t("default.fact1")}

    -

    {t("default.fact2")}

    -

    {t("default.fact3")}

    - -
    -
    -
    - ) -} + const ROUTES_WITHOUT_BACK_BUTTON = [ + OnboardingRoutes.ONBOARDING_START, + OnboardingRoutes.ONBOARDING_COMPLETE, + NewSeedRoutes.VERIFY_SEED, + !isOnboarding && OnboardingRoutes.ADD_WALLET, + ].filter((path): path is Exclude => !!path) -function Navigation({ children }: { children: React.ReactNode }): ReactElement { - const location = useLocation() return (
    -
    +
    Onboarding logo
    @@ -359,11 +129,7 @@ function Navigation({ children }: { children: React.ReactNode }): ReactElement {
    {!matchPath(location.pathname, { - path: [ - OnboardingRoutes.ONBOARDING_START, - OnboardingRoutes.ONBOARDING_COMPLETE, - NewSeedRoutes.VERIFY_SEED, - ], + path: ROUTES_WITHOUT_BACK_BUTTON, exact: true, }) && (
    @@ -384,9 +150,20 @@ function Navigation({ children }: { children: React.ReactNode }): ReactElement { } export default function Root(): ReactElement { + // This prevents navigation "Onboarding" state from changing + // until this component is unmounted + const [isOnboarding] = useState(useIsOnboarding()) + return ( - + + {!isOnboarding && ( + + )} diff --git a/ui/pages/Onboarding/Tabbed/SetPassword.tsx b/ui/pages/Onboarding/Tabbed/SetPassword.tsx index ab44e9427d..2ab3022814 100644 --- a/ui/pages/Onboarding/Tabbed/SetPassword.tsx +++ b/ui/pages/Onboarding/Tabbed/SetPassword.tsx @@ -1,8 +1,17 @@ import React, { useEffect, useState } from "react" -import { createPassword } from "@tallyho/tally-background/redux-slices/keyrings" +import { + createPassword, + unlockKeyrings, +} from "@tallyho/tally-background/redux-slices/keyrings" import { Redirect, useHistory, useLocation } from "react-router-dom" import { Trans, useTranslation } from "react-i18next" -import { useBackgroundDispatch, useAreKeyringsUnlocked } from "../../../hooks" +import { selectKeyringStatus } from "@tallyho/tally-background/redux-slices/selectors" +import { + useBackgroundDispatch, + useAreKeyringsUnlocked, + useBackgroundSelector, + useIsOnboarding, +} from "../../../hooks" import SharedButton from "../../../components/Shared/SharedButton" import PasswordStrengthBar from "../../../components/Password/PasswordStrengthBar" import PasswordInput from "../../../components/Shared/PasswordInput" @@ -56,10 +65,89 @@ export default function SetPassword(): JSX.Element { } } + const keyringStatus = useBackgroundSelector(selectKeyringStatus) + const isOnboarding = useIsOnboarding() + if (!nextPage) { return } + // Unlock Wallet + if (!isOnboarding && keyringStatus === "locked") { + const handleAttemptUnlock: React.FormEventHandler = async ( + event + ) => { + const { currentTarget: form } = event + event.preventDefault() + + const input = form.elements.namedItem("password") as HTMLInputElement + + const { success } = await dispatch(unlockKeyrings(input.value)) + + if (success) { + history.replace(nextPage) + } else { + setPasswordErrorMessage(t("keyring.unlock.error.incorrect")) + } + } + + return ( +
    +
    + {t("onboarding.tabbed.unlockWallet.title")} + +

    {t("onboarding.tabbed.unlockWallet.title")}

    +
    +
    + + + {t("onboarding.tabbed.unlockWallet.submit")} + + + +
    + ) + } + + // Set new password return (
    diff --git a/ui/public/images/onboarding_pin_extension.gif b/ui/public/images/onboarding_pin_extension.gif index 4f124325a6..b40a4d70ff 100644 Binary files a/ui/public/images/onboarding_pin_extension.gif and b/ui/public/images/onboarding_pin_extension.gif differ diff --git a/ui/utils/lists.ts b/ui/utils/lists.ts new file mode 100644 index 0000000000..eeaf2c6176 --- /dev/null +++ b/ui/utils/lists.ts @@ -0,0 +1,19 @@ +export const intersperseWith = ( + items: T[], + getItem: (index: number) => K +): (T | K)[] => { + const result: (T | K)[] = [] + + for (let i = 0; i < items.length; i += 1) { + const element = items[i] + + result.push(element) + + if (i < items.length - 1) { + result.push(getItem(i)) + } + } + return result +} + +export default {}