diff --git a/packages/fetch-extension/src/components-v2/form/password-input.module.scss b/packages/fetch-extension/src/components-v2/form/password-input.module.scss index 1998544d47..f3d54cdd67 100644 --- a/packages/fetch-extension/src/components-v2/form/password-input.module.scss +++ b/packages/fetch-extension/src/components-v2/form/password-input.module.scss @@ -1,31 +1,69 @@ .capslockTooltipArrow { left: 10px !important; } -.password-input { - margin-top: 32px; +.text { + margin-top: 24px; + color: #fff; + font-family: Lexend; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ + opacity: 0.6; +} +.input { + background: transparent; + margin-bottom: none; + color: #fff; + font-family: Lexend; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ + width: 285px; + &:hover { + background: transparent; + color: white; + } + &:focus { + background: transparent; + color: white; + } + &::placeholder { + color: white; + } +} +.password-input-container { + margin-top: 8px; + margin-bottom: 20px; + width: 333px; display: flex; height: 56px; - flex-direction: column; - align-items: flex-start; - align-self: stretch; + justify-content: space-between; + align-items: center; background: transparent; - border: 1px solid rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.1); border-radius: 10px; color: white; + border: 1px solid rgba(255, 255, 255, 0.1); &:hover { background: transparent; - width: 333px; color: white; - border: 1px solid rgba(255, 255, 255, 0.5); + background: rgba(255, 255, 255, 0.1); + border: 1px solid var(--Indigo-Indigo-400, #7655fc); } &:focus { background: transparent; - width: 333px; color: white; - border: 1px solid rgba(255, 255, 255, 0.5); + background: rgba(255, 255, 255, 0.1); + border: 1px solid var(--Indigo-Indigo-400, #7655fc); } &::placeholder { color: white; } } +.eye { + margin-right: 12px; + cursor: pointer; +} diff --git a/packages/fetch-extension/src/components-v2/form/password-input.tsx b/packages/fetch-extension/src/components-v2/form/password-input.tsx index 93bc093998..7c93a30e03 100644 --- a/packages/fetch-extension/src/components-v2/form/password-input.tsx +++ b/packages/fetch-extension/src/components-v2/form/password-input.tsx @@ -4,50 +4,68 @@ import stylePasswordInput from "./password-input.module.scss"; import { Tooltip } from "reactstrap"; import { FormattedMessage } from "react-intl"; +interface PasswordInputProps extends Omit, "type" | "onKeyUp" | "onKeyDown"> { + passwordLabel?: string; +} + // eslint-disable-next-line react/display-name -export const PasswordInput = forwardRef< - HTMLInputElement, - Omit< - InputProps & React.InputHTMLAttributes, - "type" | "onKeyUp" | "onKeyDown" - > ->((props, ref) => { +export const PasswordInput = forwardRef((props, ref) => { + const { passwordLabel, ...rest } = props; const otherRef = useRef(null); const [isOnCapsLock, setIsOnCapsLock] = useState(false); + const [isPasswordVisible, setIsPasswordVisible] = useState(false); return ( - { - otherRef.current = argRef; - if (ref) { - if ("current" in ref) { - ref.current = argRef; +
{passwordLabel || "Password"}
+
+ { + otherRef.current = argRef; + if (ref) { + if ("current" in ref) { + ref.current = argRef; + } else { + ref(argRef); + } + } + }} + onKeyUp={(e) => { + if (e.getModifierState("CapsLock")) { + setIsOnCapsLock(true); } else { - ref(argRef); + setIsOnCapsLock(false); } + }} + onKeyDown={(e) => { + if (e.getModifierState("CapsLock")) { + setIsOnCapsLock(true); + } else { + setIsOnCapsLock(false); + } + }} + /> + { - if (e.getModifierState("CapsLock")) { - setIsOnCapsLock(true); - } else { - setIsOnCapsLock(false); - } - }} - onKeyDown={(e) => { - if (e.getModifierState("CapsLock")) { - setIsOnCapsLock(true); - } else { - setIsOnCapsLock(false); - } - }} - /> + alt="" + onClick={() => setIsPasswordVisible(!isPasswordVisible)} + /> +
{otherRef.current && ( React.ReactNode; onSearchTermChange: (term: string) => void; + itemsStyleProp?:any; } export const SearchBar: React.FC = ({ @@ -14,6 +15,7 @@ export const SearchBar: React.FC = ({ valuesArray, renderResult, onSearchTermChange, + itemsStyleProp }) => { const [suggestedValues, setSuggestedValues] = useState([]); @@ -49,7 +51,7 @@ export const SearchBar: React.FC = ({ /> {suggestedValues.length > 0 && ( -
+
{suggestedValues.map((value, index) => (
{renderResult(value, index)}
))} diff --git a/packages/fetch-extension/src/components-v2/tabs/tabsPanel-2/style.module.scss b/packages/fetch-extension/src/components-v2/tabs/tabsPanel-2/style.module.scss index a25cbe40d1..d9e9ff1b0f 100644 --- a/packages/fetch-extension/src/components-v2/tabs/tabsPanel-2/style.module.scss +++ b/packages/fetch-extension/src/components-v2/tabs/tabsPanel-2/style.module.scss @@ -44,6 +44,7 @@ font-size: 13px; align-items: center; color: var(--grey-white, #fff); + position: fixed; } // .tab-bar { diff --git a/packages/fetch-extension/src/index.tsx b/packages/fetch-extension/src/index.tsx index 5bbb687c54..2bacd2631a 100644 --- a/packages/fetch-extension/src/index.tsx +++ b/packages/fetch-extension/src/index.tsx @@ -20,7 +20,7 @@ import { LockPage } from "./pages-new/lock"; // import { MainPage } from "./pages/main"; import { MainPage } from "./pages-new/main"; import { MorePage } from "./pages/more"; -import { RegisterPage } from "./pages/register"; +import { RegisterPage } from "./pages-new/register"; import { SendPage } from "./pages-new/send"; import { SetKeyRingPage } from "./pages/setting/keyring"; diff --git a/packages/fetch-extension/src/layouts-v2/header/chain-list.tsx b/packages/fetch-extension/src/layouts-v2/header/chain-list.tsx index 5268ed20f4..9fca172070 100644 --- a/packages/fetch-extension/src/layouts-v2/header/chain-list.tsx +++ b/packages/fetch-extension/src/layouts-v2/header/chain-list.tsx @@ -55,6 +55,7 @@ export const ChainList: FunctionComponent = observer( onSearchTermChange={setCosmosSearchTerm} searchTerm={cosmosSearchTerm} valuesArray={mainChainList} + itemsStyleProp={{ overflow: "auto", height: "360px" }} renderResult={(chainInfo, index) => ( = observer( />
{ e.preventDefault(); navigate("/setting/addEvmChain"); diff --git a/packages/fetch-extension/src/pages-new/lock/index.tsx b/packages/fetch-extension/src/pages-new/lock/index.tsx index 592cdeb640..2029eef507 100644 --- a/packages/fetch-extension/src/pages-new/lock/index.tsx +++ b/packages/fetch-extension/src/pages-new/lock/index.tsx @@ -94,22 +94,21 @@ export const LockPage: FunctionComponent = observer(() => {
Welcome back
- +
Enter your password to sign in
+
+ +
diff --git a/packages/fetch-extension/src/pages-new/lock/style.module.scss b/packages/fetch-extension/src/pages-new/lock/style.module.scss index 884d9561ff..f347ba97ae 100644 --- a/packages/fetch-extension/src/pages-new/lock/style.module.scss +++ b/packages/fetch-extension/src/pages-new/lock/style.module.scss @@ -14,7 +14,7 @@ background-image: url("../../public/assets/svg/wireframe/bg-returning-user.svg"); } .banner { - margin-top: 60px; + margin-top: 15px; width: 108.996px; height: 18px; flex-shrink: 0; @@ -25,15 +25,20 @@ right: 122.5px; } .password-field { - margin-top: 172px; + margin-top: 110px; display: flex; - align-items: center; - justify-content: center; flex-direction: column; } - +.text { + color: #fff; + font-family: Lexend; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 25.6px */ + opacity: 0.6; +} .welcome-text { - text-align: center; font-family: Lexend; font-size: 24px; font-style: normal; @@ -42,7 +47,7 @@ letter-spacing: -0.48px; font-size: 24px; - background: linear-gradient(64deg, #cf447b 37.86%, #f9774b 78.96%); + background: white; background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; diff --git a/packages/fetch-extension/src/pages-new/main/wallet-details/index.tsx b/packages/fetch-extension/src/pages-new/main/wallet-details/index.tsx index 2e58075c9f..037703c2eb 100644 --- a/packages/fetch-extension/src/pages-new/main/wallet-details/index.tsx +++ b/packages/fetch-extension/src/pages-new/main/wallet-details/index.tsx @@ -94,7 +94,13 @@ export const WalletDetailsView = ({
-
+
{(() => { if (accountInfo.walletStatus === WalletStatus.Loaded) { @@ -116,37 +122,39 @@ export const WalletDetailsView = ({ })()}
- {accountInfo.walletStatus === WalletStatus.Rejected && ( - { - if ( - accountInfo.rejectionReason && - accountInfo.rejectionReason instanceof KeplrError && - accountInfo.rejectionReason.module === "keyring" && - accountInfo.rejectionReason.code === 152 - ) { - // Return unsupported device message - return "Ledger is not supported for this chain"; - } +
+ {accountInfo.walletStatus === WalletStatus.Rejected && ( + { + if ( + accountInfo.rejectionReason && + accountInfo.rejectionReason instanceof KeplrError && + accountInfo.rejectionReason.module === "keyring" && + accountInfo.rejectionReason.code === 152 + ) { + // Return unsupported device message + return "Ledger is not supported for this chain"; + } - let result = "Failed to load account by unknown reason"; - if (accountInfo.rejectionReason) { - result += `: ${accountInfo.rejectionReason.toString()}`; - } + let result = "Failed to load account by unknown reason"; + if (accountInfo.rejectionReason) { + result += `: ${accountInfo.rejectionReason.toString()}`; + } - return result; - })()} - theme="dark" - trigger="hover" - options={{ - placement: "top", - }} - > - - - )} + return result; + })()} + theme="dark" + trigger="hover" + options={{ + placement: "top", + }} + > + + + )} +
{accountInfo.walletStatus !== WalletStatus.Rejected && !isEvm && (
{ + const [bip44Option] = useState(() => new BIP44Option(coinType)); + + return bip44Option; +}; + +export const AdvancedBIP44Option: FunctionComponent<{ + bip44Option: BIP44Option; +}> = observer(({ bip44Option }) => { + const intl = useIntl(); + + const confirm = useConfirm(); + + const [isOpen, setIsOpen] = useState( + bip44Option.account !== 0 || + bip44Option.change !== 0 || + bip44Option.index !== 0 + ); + const toggleOpen = async () => { + if (isOpen) { + if ( + await confirm.confirm({ + paragraph: intl.formatMessage({ + id: "register.bip44.confirm.clear", + }), + }) + ) { + setIsOpen(false); + bip44Option.setAccount(0); + bip44Option.setChange(0); + bip44Option.setIndex(0); + } + } else { + setIsOpen(true); + } + }; + + return ( + + + {isOpen ? ( + + +
+
{`m/44'/${ + bip44Option.coinType != null ? bip44Option.coinType : "ยทยทยท" + }'/`}
+ { + e.preventDefault(); + + let value = e.target.value; + if (value) { + if (value !== "0") { + // Remove leading zeros + for (let i = 0; i < value.length; i++) { + if (value[i] === "0") { + value = value.replace("0", ""); + } else { + break; + } + } + } + const parsed = parseFloat(value); + // Should be integer and positive. + if (Number.isInteger(parsed) && parsed >= 0) { + bip44Option.setAccount(parsed); + } + } else { + bip44Option.setAccount(0); + } + }} + /> +
{`'/`}
+ { + e.preventDefault(); + + let value = e.target.value; + if (value) { + if (value !== "0") { + // Remove leading zeros + for (let i = 0; i < value.length; i++) { + if (value[i] === "0") { + value = value.replace("0", ""); + } else { + break; + } + } + } + const parsed = parseFloat(value); + // Should be integer and positive. + if ( + Number.isInteger(parsed) && + (parsed === 0 || parsed === 1) + ) { + bip44Option.setChange(parsed); + } + } else { + bip44Option.setChange(0); + } + }} + /> +
/
+ { + e.preventDefault(); + + let value = e.target.value; + if (value) { + if (value !== "0") { + // Remove leading zeros + for (let i = 0; i < value.length; i++) { + if (value[i] === "0") { + value = value.replace("0", ""); + } else { + break; + } + } + } + const parsed = parseFloat(value); + // Should be integer and positive. + if (Number.isInteger(parsed) && parsed >= 0) { + bip44Option.setIndex(parsed); + } + } else { + bip44Option.setIndex(0); + } + }} + /> +
+
+ ) : null} +
+ ); +}); diff --git a/packages/fetch-extension/src/pages-new/register/auth/cosmos-rpc.ts b/packages/fetch-extension/src/pages-new/register/auth/cosmos-rpc.ts new file mode 100644 index 0000000000..ab6813d5a4 --- /dev/null +++ b/packages/fetch-extension/src/pages-new/register/auth/cosmos-rpc.ts @@ -0,0 +1,103 @@ +import type { SafeEventEmitterProvider } from "@web3auth/base"; +import { SigningStargateClient, StargateClient } from "@cosmjs/stargate"; +import { + DirectSecp256k1Wallet, + OfflineDirectSigner, +} from "@cosmjs/proto-signing"; + +const rpc = "https://rpc.sentry-02.theta-testnet.polypore.xyz"; +// eslint-disable-next-line import/no-default-export +export default class CosmosRpc { + private provider: SafeEventEmitterProvider; + + constructor(provider: SafeEventEmitterProvider) { + this.provider = provider; + } + + async getChainId(): Promise { + try { + const client = await StargateClient.connect(rpc); + + // Get the connected Chain's ID + const chainId = await client.getChainId(); + + return chainId.toString(); + } catch (error) { + return error as string; + } + } + + async getAccounts(): Promise { + try { + const privateKey = Buffer.from(await this.getPrivateKey(), "hex"); + const walletPromise = await DirectSecp256k1Wallet.fromKey( + privateKey, + "cosmos" + ); + return (await walletPromise.getAccounts())[0].address; + } catch (error) { + return error; + } + } + + async getBalance(): Promise { + try { + const client = await StargateClient.connect(rpc); + + const privateKey = Buffer.from(await this.getPrivateKey(), "hex"); + const walletPromise = await DirectSecp256k1Wallet.fromKey( + privateKey, + "cosmos" + ); + const address = (await walletPromise.getAccounts())[0].address; + // Get user's balance in uAtom + return await client.getAllBalances(address); + } catch (error) { + return error as string; + } + } + + async sendTransaction( + fromAddress: string, + destination: string + ): Promise { + try { + await StargateClient.connect(rpc); + const privateKey = Buffer.from(await this.getPrivateKey(), "hex"); + const getSignerFromKey = async (): Promise => { + return DirectSecp256k1Wallet.fromKey(privateKey, "cosmos"); + }; + const signer: OfflineDirectSigner = await getSignerFromKey(); + + const signingClient = await SigningStargateClient.connectWithSigner( + rpc, + signer + ); + + const result = await signingClient.sendTokens( + fromAddress, + destination, + [{ denom: "uatom", amount: "250" }], + { + amount: [{ denom: "uatom", amount: "250" }], + gas: "100000", + } + ); + const transactionHash = result.transactionHash; + const height = result.height; + return { transactionHash, height }; + } catch (error) { + return error as string; + } + } + + async getPrivateKey(): Promise { + try { + return await this.provider.request({ + method: "private_key", + }); + } catch (error) { + return error as string; + } + } +} diff --git a/packages/fetch-extension/src/pages-new/register/auth/image.tsx b/packages/fetch-extension/src/pages-new/register/auth/image.tsx new file mode 100644 index 0000000000..59db688fdc --- /dev/null +++ b/packages/fetch-extension/src/pages-new/register/auth/image.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +export interface ImageProps { + hoverImageId?: string; + imageId: string; + isButton?: boolean; + height?: string; + width?: string; + image: string; +} + +export const Image = (props: ImageProps) => { + const { imageId, height = "auto", width = "auto", image } = props; + + return ( + + {imageId} + + ); +}; diff --git a/packages/fetch-extension/src/pages-new/register/auth/index.tsx b/packages/fetch-extension/src/pages-new/register/auth/index.tsx new file mode 100644 index 0000000000..7749d58663 --- /dev/null +++ b/packages/fetch-extension/src/pages-new/register/auth/index.tsx @@ -0,0 +1,253 @@ +import React, { FunctionComponent, useEffect, useState } from "react"; +import { Web3AuthNoModal as Web3Auth } from "@web3auth/no-modal"; +import { CommonPrivateKeyProvider } from "@web3auth/base-provider"; +import { CHAIN_NAMESPACES, WALLET_ADAPTERS } from "@web3auth/base"; +import { + OPENLOGIN_NETWORK, + OpenloginAdapter, +} from "@web3auth/openlogin-adapter"; +import style from "./style.module.scss"; +import { RegisterConfig } from "@keplr-wallet/hooks"; +import { observer } from "mobx-react-lite"; +import CosmosRpc from "./cosmos-rpc"; +import { Form, Label } from "reactstrap"; +import { FormattedMessage, useIntl } from "react-intl"; +import { BackButton } from ".."; +import { useForm } from "react-hook-form"; +import { Input, PasswordInput } from "@components-v2/form"; +// eslint-disable-next-line import/no-extraneous-dependencies +import { AuthApiKey } from "../../../config.ui"; +import { useStore } from "../../../stores"; +import { ButtonV2 } from "@components-v2/buttons/button"; +import { Card } from "@components-v2/card"; +// get from https://dashboard.web3auth.io + +export const AuthIntro: FunctionComponent<{ + registerConfig: RegisterConfig; +}> = observer(({ registerConfig }) => { + const { analyticsStore } = useStore(); + + const [web3auth, setWeb3auth] = useState(null); + const isEnvDevelopment = process.env.NODE_ENV !== "production"; + useEffect(() => { + if (!AuthApiKey) return; + const init = async () => { + try { + const chainConfig = { + chainNamespace: CHAIN_NAMESPACES.OTHER, + chainId: "fetchhub-4", + rpcTarget: "https://rpc-fetchhub.fetch-ai.com", + displayName: "fetch", + blockExplorer: "https://explore.fetch.ai/", + ticker: "FET", + tickerName: "Fetch Token", + }; + const web3auth = new Web3Auth({ + clientId: AuthApiKey, + chainConfig, + web3AuthNetwork: isEnvDevelopment + ? OPENLOGIN_NETWORK.TESTNET + : OPENLOGIN_NETWORK.CYAN, + }); + setWeb3auth(web3auth); + const privateKeyProvider = new CommonPrivateKeyProvider({ + config: { chainConfig }, + }); + const openloginAdapter = new OpenloginAdapter({ privateKeyProvider }); + web3auth.configureAdapter(openloginAdapter); + + await web3auth.init(); + } catch (error) { + console.error(error); + } + }; + + init(); + }, []); + + const login = async () => { + if (!web3auth) { + return; + } + return await web3auth.connectTo(WALLET_ADAPTERS.OPENLOGIN, { + loginProvider: "google", + }); + }; + + const logout = async () => { + if (!web3auth) { + return; + } + await web3auth.logout(); + }; + const getPrivateKey = async (provider: any) => { + if (!provider) { + return ""; + } + const rpc = new CosmosRpc(provider); + return await rpc.getPrivateKey(); + }; + + const getUserInfo = async () => { + if (!web3auth) { + return; + } + const user = await web3auth.getUserInfo(); + return user.email; + }; + + return ( + + {AuthApiKey && ( + { + e.preventDefault(); + const target = e.target as HTMLElement; + if (target.tagName === "A") { + const url = target.getAttribute("href"); + if (url) { + window.open(url, "_blank"); // Open the URL in a new window + } + return; + } + try { + const data = await login(); + const privateKey = await getPrivateKey(data); + if (!privateKey) return; + registerConfig.setType("auth"); + registerConfig.setPrivateKey(privateKey); + const email = await getUserInfo(); + registerConfig.setEmail(email || ""); + await logout(); + } catch (e) { + } finally { + analyticsStore.logEvent("Create/Import account started", { + registerType: "google", + }); + } + }} + leftImage={require("@assets/svg/wireframe/google-icon.svg")} + subheading={"Powered by Web3Auth"} + heading={"Continue with Google"} + /> + )} + + ); +}); + +interface FormData { + name: string; + words: string; + password: string; + confirmPassword: string; +} +export const AuthPage: FunctionComponent<{ + registerConfig: RegisterConfig; +}> = observer(({ registerConfig }) => { + const intl = useIntl(); + const { + register, + getValues, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + name: "", + password: "", + confirmPassword: "", + }, + }); + const privateKey = Buffer.from( + registerConfig.privateKey.trim().replace("0x", ""), + "hex" + ); + return ( + + { + registerConfig.clear(); + }} + /> +
{ + registerConfig.createPrivateKey( + data.name, + privateKey, + data.password, + { email: registerConfig.email } + ); + })} + > + + + {registerConfig.mode === "create" ? ( + + { + if (password.length < 8) { + return intl.formatMessage({ + id: "register.create.input.password.error.too-short", + }); + } + }, + })} + error={errors.password && errors.password.message} + /> + { + if (confirmPassword !== getValues()["password"]) { + return intl.formatMessage({ + id: "register.create.input.confirm-password.error.unmatched", + }); + } + }, + })} + error={errors.confirmPassword && errors.confirmPassword.message} + /> + + ) : null} + + + +
+
+ ); +}); + +// eslint-disable-next-line import/no-default-export diff --git a/packages/fetch-extension/src/pages-new/register/auth/style.module.scss b/packages/fetch-extension/src/pages-new/register/auth/style.module.scss new file mode 100644 index 0000000000..61db69740c --- /dev/null +++ b/packages/fetch-extension/src/pages-new/register/auth/style.module.scss @@ -0,0 +1,75 @@ +.card { + margin: 0.5rem; + padding: 0.7rem; + text-align: center; + color: #0070f3; + text-decoration: none; + border: 1px solid #0070f3; + border-radius: 10px; + transition: color 0.15s ease, border-color 0.15s ease; + width: 100%; +} + +.container { + padding: 0; + margin-bottom: 18px; +} + +.w3abutton { + border: 1px solid #f3f3f4; + box-shadow: none; + box-sizing: border-box; + border-radius: 24px; + height: 48px; + width: 100%; + padding: 8px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--text-body); + font-style: normal; + font-weight: 600; + cursor: pointer; + background-color: #0364ff; + color: #fff; + + &:hover { + transition: 200ms; + background-color: #0a49af; + } +} + +.gTitle { + position: relative; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + margin-left: 10px; + margin-right: 10px; +} + +.authPoweredBy { + font-size: 10px; + position: absolute; + bottom: 2px; + right: 12px; + color: white; + &:hover { + color: white; + background-color: transparent; + } +} + +.addressInput { + height: 53px; + border: 1px solid rgba(255, 255, 255, 0.1); + color: white; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + width: 333px !important; +} +.addressInput:hover { + border: 1px solid var(--Indigo-Indigo-400, #7655fc); +} diff --git a/packages/fetch-extension/src/pages-new/register/index.tsx b/packages/fetch-extension/src/pages-new/register/index.tsx new file mode 100644 index 0000000000..9f2562fa97 --- /dev/null +++ b/packages/fetch-extension/src/pages-new/register/index.tsx @@ -0,0 +1,99 @@ +// Shim ------------ +require("setimmediate"); +// Shim ------------ +import React, { FunctionComponent, useEffect } from "react"; + +import { EmptyLayout } from "@layouts/empty-layout"; + +import { observer } from "mobx-react-lite"; + +import style from "./style.module.scss"; + +import { Button } from "reactstrap"; + +import { useRegisterConfig } from "@keplr-wallet/hooks"; +import { useStore } from "../../stores"; +import { NewMnemonicIntro, NewMnemonicPage, TypeNewMnemonic } from "./mnemonic"; +import { + RecoverMnemonicIntro, + RecoverMnemonicPage, + TypeRecoverMnemonic, +} from "./mnemonic"; +import { WelcomePage } from "./welcome"; +import { AdditionalSignInPrepend } from "../../config.ui"; +import classnames from "classnames"; +import { configure } from "mobx"; +configure({ + enforceActions: "always", // Make mobx to strict mode. +}); +export enum NunWords { + WORDS12, + WORDS24, +} + +export const BackButton: FunctionComponent<{ onClick: () => void }> = ({ + onClick, +}) => { + return ( +
+ +
+ ); +}; + +export const RegisterPage: FunctionComponent = observer(() => { + const { keyRingStore, analyticsStore } = useStore(); + + useEffect(() => { + analyticsStore.logEvent("Register page"); + document.documentElement.setAttribute("data-register-page", "true"); + + return () => { + document.documentElement.removeAttribute("data-register-page"); + }; + }, []); + + const registerConfig = useRegisterConfig(keyRingStore, [ + ...(AdditionalSignInPrepend ?? []), + { + type: TypeNewMnemonic, + intro: NewMnemonicIntro, + page: NewMnemonicPage, + }, + { + type: TypeRecoverMnemonic, + intro: RecoverMnemonicIntro, + page: RecoverMnemonicPage, + }, + ]); + + return ( + +
+
+ logo +
+
+ {registerConfig.render()} + {registerConfig.isFinalized ? : null} +
+ + ); +}); diff --git a/packages/fetch-extension/src/pages-new/register/keystone/index.tsx b/packages/fetch-extension/src/pages-new/register/keystone/index.tsx new file mode 100644 index 0000000000..37766ebba6 --- /dev/null +++ b/packages/fetch-extension/src/pages-new/register/keystone/index.tsx @@ -0,0 +1,166 @@ +import React, { FunctionComponent } from "react"; +import { RegisterConfig } from "@keplr-wallet/hooks"; +import { FormattedMessage, useIntl } from "react-intl"; +import { Button, Form } from "reactstrap"; +import { useForm } from "react-hook-form"; +import style from "../style.module.scss"; +import { Input, PasswordInput } from "@components/form"; +import { useBIP44Option } from "../advanced-bip44"; +import { BackButton } from "../index"; +import { observer } from "mobx-react-lite"; +import { useStore } from "../../../stores"; +import { KeystoneIntroduction } from "./introduction"; + +export const TypeImportKeystone = "import-keystone"; + +interface FormData { + name: string; + password: string; + confirmPassword: string; +} + +export const ImportKeystoneIntro: FunctionComponent<{ + registerConfig: RegisterConfig; +}> = observer(({ registerConfig }) => { + const { analyticsStore } = useStore(); + return ( + + ); +}); + +export const ImportKeystonePage: FunctionComponent<{ + registerConfig: RegisterConfig; +}> = observer(({ registerConfig }) => { + const intl = useIntl(); + + const bip44Option = useBIP44Option(); + + const { + register, + handleSubmit, + getValues, + formState: { errors }, + } = useForm({ + defaultValues: { + name: "", + password: "", + confirmPassword: "", + }, + }); + + const { analyticsStore } = useStore(); + + return ( +
+
+ {intl.formatMessage({ + id: "register.name", + })} +
+
{ + try { + await registerConfig.createKeystone( + data.name, + data.password, + bip44Option.bip44HDPath + ); + analyticsStore.setUserProperties({ + registerType: "keystone", + accountType: "keystone", + }); + } catch (e: any) { + alert(e.message ? e.message : e.toString()); + registerConfig.clear(); + } + })} + > + + {registerConfig.mode === "create" ? ( + + { + if (password.length < 8) { + return intl.formatMessage({ + id: "register.create.input.password.error.too-short", + }); + } + }, + })} + error={errors.password && errors.password.message} + /> + { + if (confirmPassword !== getValues()["password"]) { + return intl.formatMessage({ + id: "register.create.input.confirm-password.error.unmatched", + }); + } + }, + })} + error={errors.confirmPassword && errors.confirmPassword.message} + /> + + ) : null} + {/* */} + + + { + registerConfig.clear(); + }} + /> +
+ ); +}); diff --git a/packages/fetch-extension/src/pages-new/register/keystone/introduction.tsx b/packages/fetch-extension/src/pages-new/register/keystone/introduction.tsx new file mode 100644 index 0000000000..e61ca703e8 --- /dev/null +++ b/packages/fetch-extension/src/pages-new/register/keystone/introduction.tsx @@ -0,0 +1,109 @@ +import React, { useState } from "react"; +import { createPortal } from "react-dom"; +import { Button } from "reactstrap"; +import styles from "../style.module.scss"; + +function Introduction({ + isOpen, + onClose, +}: { + isOpen: boolean; + onClose: () => void; +}) { + return createPortal( +
e.stopPropagation()} + > +
+
+ + + + + +
+
+ Keystone is a top-notch hardware wallet for optimal security, + user-friendly interface and extensive compatibility. +
+ + +
+
, + document.body + ); +} + +export function KeystoneIntroduction({ className }: { className: any }) { + const [isOpen, setIsOpen] = useState(false); + + const onClick = (e: any) => { + e.stopPropagation(); + setIsOpen(true); + }; + + return ( + + + + + + + + setIsOpen(false)} /> + + ); +} diff --git a/packages/fetch-extension/src/pages-new/register/ledger/index.tsx b/packages/fetch-extension/src/pages-new/register/ledger/index.tsx new file mode 100644 index 0000000000..0746a3ad8b --- /dev/null +++ b/packages/fetch-extension/src/pages-new/register/ledger/index.tsx @@ -0,0 +1,223 @@ +import React, { FunctionComponent } from "react"; +import { RegisterConfig } from "@keplr-wallet/hooks"; +import { FormattedMessage, useIntl } from "react-intl"; +import { Button, Form, Label } from "reactstrap"; +import { useForm } from "react-hook-form"; +import style from "../style.module.scss"; +import { Input, PasswordInput } from "@components-v2/form"; +import { AdvancedBIP44Option, useBIP44Option } from "../advanced-bip44"; +import { BackButton } from "../index"; +import { observer } from "mobx-react-lite"; +import { useStore } from "../../../stores"; +import { ledgerUSBVendorId } from "@ledgerhq/devices"; +import { ButtonV2 } from "@components-v2/buttons/button"; + +export const TypeImportLedger = "import-ledger"; + +interface FormData { + name: string; + password: string; + confirmPassword: string; +} + +export const ImportLedgerIntro: FunctionComponent<{ + registerConfig: RegisterConfig; +}> = observer(({ registerConfig }) => { + const { analyticsStore } = useStore(); + return ( + { + e.preventDefault(); + + registerConfig.setType(TypeImportLedger); + analyticsStore.logEvent("Import account started", { + registerType: "ledger", + }); + }} + text={""} + > + + + ); +}); + +export const ImportLedgerPage: FunctionComponent<{ + registerConfig: RegisterConfig; + setSelectedCard:any; +}> = observer(({ registerConfig, setSelectedCard }) => { + const intl = useIntl(); + + const bip44Option = useBIP44Option(118); + + const { + register, + handleSubmit, + getValues, + formState: { errors }, + } = useForm({ + defaultValues: { + name: "", + password: "", + confirmPassword: "", + }, + }); + + const { analyticsStore, ledgerInitStore } = useStore(); + + const ensureUSBPermission = async () => { + const anyNavigator = navigator as any; + if (ledgerInitStore.isWebHID) { + const device = await anyNavigator.hid.requestDevice({ + filters: [ + { + vendorId: ledgerUSBVendorId, + }, + ], + }); + if (!device || (Array.isArray(device) && device.length === 0)) { + throw new Error("No device selected"); + } + } else { + if ( + !(await anyNavigator.usb.requestDevice({ + filters: [ + { + vendorId: ledgerUSBVendorId, + }, + ], + })) + ) { + throw new Error("No device selected"); + } + } + }; + + return ( +
+ { + setSelectedCard("main"); + }} + /> +
Connect hardware wallet
+
+ To keep your account safe, avoid any personal information or words +
+
+
{ + try { + await ensureUSBPermission(); + + await registerConfig.createLedger( + data.name, + data.password, + bip44Option.bip44HDPath, + "Cosmos" + ); + analyticsStore.setUserProperties({ + registerType: "ledger", + accountType: "ledger", + }); + } catch (e) { + alert(e.message ? e.message : e.toString()); + registerConfig.clear(); + } + })} + > + + + {registerConfig.mode === "create" ? ( + + { + if (password.length < 8) { + return intl.formatMessage({ + id: "register.create.input.password.error.too-short", + }); + } + }, + })} + error={errors.password && errors.password.message} + /> + { + if (confirmPassword !== getValues()["password"]) { + return intl.formatMessage({ + id: "register.create.input.confirm-password.error.unmatched", + }); + } + }, + })} + error={errors.confirmPassword && errors.confirmPassword.message} + /> + + ) : null} +
+ +
+ + + + +
+
+
+ ); +}); diff --git a/packages/fetch-extension/src/pages-new/register/migration/index.tsx b/packages/fetch-extension/src/pages-new/register/migration/index.tsx new file mode 100644 index 0000000000..aca6aba4ab --- /dev/null +++ b/packages/fetch-extension/src/pages-new/register/migration/index.tsx @@ -0,0 +1,77 @@ +import React, { FunctionComponent, useState } from "react"; +import { RegisterConfig } from "@keplr-wallet/hooks"; +import { observer } from "mobx-react-lite"; +import { FormattedMessage } from "react-intl"; +import { BackButton } from "../index"; +import { MigrateMetamaskPrivateKeyPage } from "./metamask-privatekey"; +import { ButtonV2 } from "@components-v2/buttons/button"; +import style from "../style.module.scss"; + +export const TypeMigrateEth = "migrate-from-eth"; + +enum MigrationMode { + SELECT_MODE, + METAMASK_PRIVATE_KEY, +} + +export const MigrateEthereumAddressIntro: FunctionComponent<{ + registerConfig: RegisterConfig; +}> = observer(({ registerConfig }) => { + return ( + + {" "} + { + e.preventDefault(); + + registerConfig.setType(TypeMigrateEth); + }} + text={""} + > + + + + ); +}); + +const MigrationSelectionPage: FunctionComponent<{ + setMode: (mode: MigrationMode) => void; + onBack: () => void; +}> = (props) => { + return ( +
+ + props.setMode(MigrationMode.METAMASK_PRIVATE_KEY)} + > + + +
+ ); +}; + +export const MigrateEthereumAddressPage: FunctionComponent<{ + registerConfig: RegisterConfig; + setSelectedCard:any; +}> = observer(({ registerConfig, setSelectedCard }) => { + const [mode, setMode] = useState(MigrationMode.SELECT_MODE); + + switch (mode) { + case MigrationMode.SELECT_MODE: + return ( + setSelectedCard("main")} + /> + ); + case MigrationMode.METAMASK_PRIVATE_KEY: + return ( + setMode(MigrationMode.SELECT_MODE)} + /> + ); + } +}); diff --git a/packages/fetch-extension/src/pages-new/register/migration/metamask-privatekey.tsx b/packages/fetch-extension/src/pages-new/register/migration/metamask-privatekey.tsx new file mode 100644 index 0000000000..94380517ec --- /dev/null +++ b/packages/fetch-extension/src/pages-new/register/migration/metamask-privatekey.tsx @@ -0,0 +1,214 @@ +import React, { FunctionComponent } from "react"; +import { BackButton } from "../index"; +import { FormattedMessage, useIntl } from "react-intl"; +import { Input, TextArea } from "@components/form"; +import style from "../style.module.scss"; +import { Form } from "reactstrap"; +import { useForm } from "react-hook-form"; +import { Buffer } from "buffer"; +import { parseEthPrivateKey } from "@fetchai/eth-migration"; +import { RegisterConfig } from "@keplr-wallet/hooks"; +import { ButtonV2 } from "@components-v2/buttons/button"; + +interface FormData { + name: string; + ethAddress: string; + ethPrivateKey: string; + password: string; + confirmPassword: string; +} + +function isPrivateKey(str: string): boolean { + if (str.startsWith("0x")) { + return true; + } + + return str.length === 64; +} + +export const MigrateMetamaskPrivateKeyPage: FunctionComponent<{ + registerConfig: RegisterConfig; + onBack: () => void; +}> = ({ registerConfig, onBack }) => { + const intl = useIntl(); + + const { + register, + handleSubmit, + formState: { errors }, + getValues, + } = useForm({ + defaultValues: { + name: "", + ethAddress: "", + ethPrivateKey: "", + password: "", + confirmPassword: "", + }, + }); + + return ( +
+ +

+ +

+
{ + // extract the private key + const privateKey = Buffer.from( + data.ethPrivateKey.trim().replace("0x", ""), + "hex" + ); + + // attempt to parse the private key information + const parsedKey = parseEthPrivateKey(privateKey); + if (parsedKey === undefined) { + alert("Unable to parse private key"); + return; + } + + // check that the parsed private key matches + if (parsedKey.ethAddress !== data.ethAddress) { + alert("This private key does not match the address provided"); + return; + } + + // trigger the on complete handler + await registerConfig.createPrivateKey( + data.name, + privateKey, + data.password + ); + })} + > + + +