diff --git a/@shared/api/external.ts b/@shared/api/external.ts index 664aa82cfc..b7eeade9d3 100644 --- a/@shared/api/external.ts +++ b/@shared/api/external.ts @@ -22,7 +22,8 @@ export const requestPublicKey = async (): Promise => { export const submitTransaction = async ( transactionXdr: string, - network?: string, + network?: string | null, + accountToSign?: string, ): Promise => { let response = { signedTransaction: "", error: "" }; if (network && network !== NETWORKS.PUBLIC && network !== NETWORKS.TESTNET) { @@ -33,6 +34,7 @@ export const submitTransaction = async ( response = await sendMessageToContentScript({ transactionXdr, network, + accountToSign, type: EXTERNAL_SERVICE_TYPES.SUBMIT_TRANSACTION, }); } catch (e) { diff --git a/@shared/api/types.ts b/@shared/api/types.ts index 3af59cccdd..b0d200c092 100644 --- a/@shared/api/types.ts +++ b/@shared/api/types.ts @@ -43,6 +43,7 @@ export interface Response { export interface ExternalRequest { transactionXdr: string; network: string; + accountToSign: string; type: EXTERNAL_SERVICE_TYPES; } diff --git a/@stellar/freighter-api/package.json b/@stellar/freighter-api/package.json index 0c10254586..ef48aa5c6e 100644 --- a/@stellar/freighter-api/package.json +++ b/@stellar/freighter-api/package.json @@ -1,6 +1,6 @@ { "name": "@stellar/freighter-api", - "version": "1.1.2", + "version": "1.2.0", "license": "Apache-2.0", "author": "Stellar Development Foundation ", "description": "Utility functions to interact with Freighter extension", diff --git a/@stellar/freighter-api/src/signTransaction.ts b/@stellar/freighter-api/src/signTransaction.ts index 494b9b8e81..655bd3f783 100644 --- a/@stellar/freighter-api/src/signTransaction.ts +++ b/@stellar/freighter-api/src/signTransaction.ts @@ -2,5 +2,6 @@ import { submitTransaction } from "@shared/api/external"; export const signTransaction = ( transactionXdr: string, - network?: "PUBLIC" | "TESTNET" -) => submitTransaction(transactionXdr, network); + network?: "PUBLIC" | "TESTNET" | null, + accountToSign?: string +) => submitTransaction(transactionXdr, network, accountToSign); diff --git a/docs/docs/guide/usingFreighterBrowser.md b/docs/docs/guide/usingFreighterBrowser.md index 1b1e733a8c..dfcf97a556 100644 --- a/docs/docs/guide/usingFreighterBrowser.md +++ b/docs/docs/guide/usingFreighterBrowser.md @@ -109,7 +109,7 @@ The user will need to provide their password if the extension does not currently _NOTE:_ The user must provide a valid transaction XDR string for the extension to properly sign. -The second parameter is an optional string that you may pass to indicate what network you’re intending this transaction to be signed on. The parameter must be either `PUBLIC` or `TESTNET`. If you choose not to pass a param, freighter-api will default to `PUBLIC`. +The second parameter is an optional string that you may pass to indicate what network you’re intending this transaction to be signed on. The network must be either `PUBLIC` or `TESTNET`. If you choose not to pass a network, freighter-api will default to `PUBLIC`. You may also pass `null` here if you choose not to pass a network param, but you would like to pass the third param available. This is useful in the case that the user's Freighter is configured to the wrong network. Freighter will be able to throw a blocking error message communicating that you intended this transaction to be signed on a different network. @@ -137,12 +137,20 @@ const retrievePublicKey = async () => { const retrievedPublicKey = await retrievePublicKey(); -const userSignTransaction = async (xdr: string, network: string) => { +const userSignTransaction = async ( + xdr: string, + network: string, + signWith: string +) => { let signedTransaction = ""; let error = ""; try { - signedTransaction = await window.freighterApi.signTransaction(xdr, network); + signedTransaction = await window.freighterApi.signTransaction( + xdr, + network, + signWith + ); } catch (e) { error = e; } @@ -161,12 +169,20 @@ const userSignedTransaction = await userSignTransaction(xdr, "TESTNET"); freighter-api will return a signed transaction xdr. Below is an example of how you might submit this signed transaction to Horizon using `stellar-sdk` (https://github.com/stellar/js-stellar-sdk): ```javascript -const userSignTransaction = async (xdr: string, network: string) => { +const userSignTransaction = async ( + xdr: string, + network: string, + signWith: string +) => { let signedTransaction = ""; let error = ""; try { - signedTransaction = await window.freighterApi.signTransaction(xdr, network); + signedTransaction = await window.freighterApi.signTransaction( + xdr, + network, + signWith + ); } catch (e) { error = e; } diff --git a/docs/docs/guide/usingFreighterNode.md b/docs/docs/guide/usingFreighterNode.md index aa08174738..1076cb6047 100644 --- a/docs/docs/guide/usingFreighterNode.md +++ b/docs/docs/guide/usingFreighterNode.md @@ -117,7 +117,7 @@ const result = retrieveNetwork(); ### signTransaction -#### `signTransaction(xdr: string, network:? string) -> >` +#### `signTransaction(xdr: string, network:? string, publicKey:? string) -> >` This function accepts a transaction XDR string as the first parameter, which it will decode, sign as the user, and then return the signed transaction to your application. @@ -125,10 +125,12 @@ The user will need to provide their password if the extension does not currently _NOTE:_ The user must provide a valid transaction XDR string for the extension to properly sign. -The second parameter is an optional string that you may pass to indicate what network you’re intending this transaction to be signed on. The parameter must be either `PUBLIC` or `TESTNET`. If you choose not to pass a param, freighter-api will default to `PUBLIC`. +The second parameter is an optional string that you may pass to indicate what network you’re intending this transaction to be signed on. The network must be either `PUBLIC` or `TESTNET`. If you choose not to pass a network, freighter-api will default to `PUBLIC`. You may also pass `null` here if you choose not to pass a network param, but you would like to pass the third param available. This is useful in the case that the user's Freighter is configured to the wrong network. Freighter will be able to throw a blocking error message communicating that you intended this transaction to be signed on a different network. +The third parameter is another optional parameter that gives you the ability to specify which account's signature you’re requesting. If Freighter has the public key, it will switch to that account. If not, it will alert the user that they do not have the requested account. + ```javascript import { isConnected, @@ -159,12 +161,16 @@ const retrievePublicKey = async () => { const retrievedPublicKey = retrievePublicKey(); -const userSignTransaction = async (xdr: string, network: string) => { +const userSignTransaction = async ( + xdr: string, + network: string, + signWith: string +) => { let signedTransaction = ""; let error = ""; try { - signedTransaction = await signTransaction(xdr, network); + signedTransaction = await signTransaction(xdr, network, signWith); } catch (e) { error = e; } @@ -185,12 +191,16 @@ freighter-api will return a signed transaction xdr. Below is an example of how y ```javascript import StellarSdk from "stellar-sdk"; -const userSignTransaction = async (xdr: string, network: string) => { +const userSignTransaction = async ( + xdr: string, + network: string, + signWith: string +) => { let signedTransaction = ""; let error = ""; try { - signedTransaction = await signTransaction(xdr, network); + signedTransaction = await signTransaction(xdr, network, signWith); } catch (e) { error = e; } diff --git a/docs/docs/playground/components/SignTransactionDemo.tsx b/docs/docs/playground/components/SignTransactionDemo.tsx index 5badfe539d..039b3afe87 100644 --- a/docs/docs/playground/components/SignTransactionDemo.tsx +++ b/docs/docs/playground/components/SignTransactionDemo.tsx @@ -1,19 +1,33 @@ import React, { useState } from "react"; import { signTransaction } from "@stellar/freighter-api"; -import { PlaygroundTextarea } from "./basics/inputs"; +import { PlaygroundInput, PlaygroundTextarea } from "./basics/inputs"; export const SignTransactionDemo = () => { const [transactionXdr, setTransactionXdr] = useState(""); + const [network, setNetwork] = useState(""); + const [publicKey, setPublicKey] = useState(""); const [transactionResult, setTransactionResult] = useState(""); - const inputOnChangeHandler = (e: React.ChangeEvent) => { + + const xdrOnChangeHandler = (e: React.ChangeEvent) => { setTransactionXdr(e.currentTarget.value); }; + const networkOnChangeHandler = (e: React.ChangeEvent) => { + setNetwork(e.currentTarget.value); + }; + const publicKeyOnChangeHandler = (e: React.ChangeEvent) => { + setPublicKey(e.currentTarget.value); + }; + const btnHandler = async () => { let signedTransaction; let error = ""; try { - signedTransaction = await signTransaction(transactionXdr); + signedTransaction = await signTransaction( + transactionXdr, + network === "PUBLIC" || network === "TESTNET" ? network : null, + publicKey + ); } catch (e) { error = e; } @@ -23,7 +37,15 @@ export const SignTransactionDemo = () => {
Enter transaction XDR: - + +
+
+ Enter network - "TESTNET"|"PUBLIC" (optional): + +
+
+ Request signature from specific public key (optional): +
Result: diff --git a/docs/docs/playground/signTransaction.mdx b/docs/docs/playground/signTransaction.mdx index ebf7a73343..ffee0cf0af 100644 --- a/docs/docs/playground/signTransaction.mdx +++ b/docs/docs/playground/signTransaction.mdx @@ -3,7 +3,7 @@ id: signTransaction title: signTransaction --- -#### `signTransaction( transactionXdr: string )` +#### `signTransaction( transactionXdr: string, network?: "PUBLIC"|"TESTNET"|null, publicKey?: string )` import { SignTransactionDemo } from "./components/SignTransactionDemo"; diff --git a/docs/package.json b/docs/package.json index 9573e008d2..a213448bc1 100644 --- a/docs/package.json +++ b/docs/package.json @@ -26,7 +26,7 @@ "@docusaurus/core": "2.0.0-alpha.68", "@docusaurus/preset-classic": "2.0.0-alpha.68", "@mdx-js/react": "^1.6.19", - "@stellar/freighter-api": "1.1.2", + "@stellar/freighter-api": "1.2.0", "clsx": "^1.1.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/extension/package.json b/extension/package.json index 5b5f37f00a..b484f2b925 100755 --- a/extension/package.json +++ b/extension/package.json @@ -69,4 +69,4 @@ "webextension-polyfill-ts": "^0.19.0", "yup": "^0.29.1" } -} \ No newline at end of file +} diff --git a/extension/public/static/manifest.json b/extension/public/static/manifest.json index 1b49c37c3a..4b1af5ef92 100644 --- a/extension/public/static/manifest.json +++ b/extension/public/static/manifest.json @@ -10,19 +10,13 @@ } }, "background": { - "scripts": [ - "background.min.js" - ], + "scripts": ["background.min.js"], "persistent": true }, "content_scripts": [ { - "matches": [ - "" - ], - "js": [ - "contentScript.min.js" - ], + "matches": [""], + "js": ["contentScript.min.js"], "run_at": "document_start" } ], @@ -42,4 +36,4 @@ "128": "images/icon128.png" }, "manifest_version": 2 -} \ No newline at end of file +} diff --git a/extension/src/background/messageListener/freighterApiMessageListener.ts b/extension/src/background/messageListener/freighterApiMessageListener.ts index a6452d9750..cdc998d22b 100644 --- a/extension/src/background/messageListener/freighterApiMessageListener.ts +++ b/extension/src/background/messageListener/freighterApiMessageListener.ts @@ -79,10 +79,8 @@ export const freighterApiMessageListener = ( }; const submitTransaction = async () => { - const { - transactionXdr, - network = MAINNET_NETWORK_DETAILS.network, - } = request; + const { transactionXdr, network: _network, accountToSign } = request; + const network = _network ?? MAINNET_NETWORK_DETAILS.network; const isTestnet = getIsTestnet(); const { networkUrl } = getNetworkDetails(isTestnet); const transaction = StellarSdk.TransactionBuilder.fromXDR( @@ -115,7 +113,7 @@ export const freighterApiMessageListener = ( accountData.forEach( ({ address, tags }: { address: string; tags: Array }) => { if (address === operation.destination) { - const collectedTags = [...tags]; + let collectedTags = [...tags]; /* if the user has opted out of validation, remove applicable tags */ if (!isValidatingMemo) { @@ -124,10 +122,10 @@ export const freighterApiMessageListener = ( ); } if (!isValidatingSafety) { - collectedTags.filter( + collectedTags = collectedTags.filter( (tag) => tag !== TRANSACTION_WARNING.unsafe, ); - collectedTags.filter( + collectedTags = collectedTags.filter( (tag) => tag !== TRANSACTION_WARNING.malicious, ); } @@ -157,6 +155,7 @@ export const freighterApiMessageListener = ( isDomainListedAllowed, url: tabUrl, flaggedKeys, + accountToSign, } as TransactionInfo; transactionQueue.push(transaction); diff --git a/extension/src/helpers/stellar.ts b/extension/src/helpers/stellar.ts index a4b555a345..f722503a94 100644 --- a/extension/src/helpers/stellar.ts +++ b/extension/src/helpers/stellar.ts @@ -23,6 +23,7 @@ export const getTransactionInfo = (search: string) => { const transactionInfo = parsedSearchParam(search); const { + accountToSign, url, transaction, isDomainListedAllowed, @@ -36,6 +37,7 @@ export const getTransactionInfo = (search: string) => { ); return { + accountToSign, transaction, domain: hostname, domainTitle: title, @@ -90,3 +92,15 @@ export const getConversionRate = ( sourceAmount: string, destAmount: string, ): BigNumber => new BigNumber(destAmount).div(new BigNumber(sourceAmount)); + +export const formatDomain = (domain: string) => { + if (domain) { + domain.replace("https://", "").replace("www.", ""); + return domain; + } + return "Stellar Network"; +}; + +export const isMuxedAccount = (publicKey: string) => publicKey.startsWith("M"); + +export const isFederationAddress = (address: string) => address.includes("*"); diff --git a/extension/src/popup/Router.tsx b/extension/src/popup/Router.tsx index 5d4f2584b6..b58a74e6b0 100644 --- a/extension/src/popup/Router.tsx +++ b/extension/src/popup/Router.tsx @@ -54,6 +54,8 @@ import { About } from "popup/views/About"; import { SendPayment } from "popup/views/SendPayment"; import { ManageAssets } from "popup/views/ManageAssets"; import { VerifyAccount } from "popup/views/VerifyAccount"; +import { Swap } from "popup/views/Swap"; +import { PinExtension } from "popup/views/PinExtension"; import "popup/metrics/views"; import { DEV_SERVER } from "@shared/constants/services"; @@ -143,6 +145,23 @@ const UnlockAccountRoute = (props: RouteProps) => { return ; }; +export const VerifiedAccountRoute = (props: RouteProps) => { + const location = useLocation(); + const hasPrivateKey = useSelector(hasPrivateKeySelector); + + if (!hasPrivateKey) { + return ( + + ); + } + return ; +}; + const HomeRoute = () => { const allAccounts = useSelector(allAccountsSelector); const applicationState = useSelector(applicationStateSelector); @@ -233,9 +252,9 @@ export const Router = () => { - + - + @@ -263,6 +282,9 @@ export const Router = () => { + + + @@ -281,6 +303,9 @@ export const Router = () => { + + + {DEV_SERVER && ( diff --git a/extension/src/popup/assets/extensions-menu.png b/extension/src/popup/assets/extensions-menu.png new file mode 100644 index 0000000000..9589b0dff3 Binary files /dev/null and b/extension/src/popup/assets/extensions-menu.png differ diff --git a/extension/src/popup/assets/extensions-pin.png b/extension/src/popup/assets/extensions-pin.png new file mode 100644 index 0000000000..967b4207be Binary files /dev/null and b/extension/src/popup/assets/extensions-pin.png differ diff --git a/extension/src/popup/assets/icon-swap.svg b/extension/src/popup/assets/icon-swap.svg new file mode 100644 index 0000000000..9b013ced44 --- /dev/null +++ b/extension/src/popup/assets/icon-swap.svg @@ -0,0 +1,3 @@ + + + diff --git a/extension/src/popup/assets/image-missing.svg b/extension/src/popup/assets/image-missing.svg new file mode 100644 index 0000000000..0795931bb6 --- /dev/null +++ b/extension/src/popup/assets/image-missing.svg @@ -0,0 +1,3 @@ + + + diff --git a/extension/src/popup/basics/InfoBlock/index.tsx b/extension/src/popup/basics/InfoBlock/index.tsx index 93a11b6666..43882fa38a 100644 --- a/extension/src/popup/basics/InfoBlock/index.tsx +++ b/extension/src/popup/basics/InfoBlock/index.tsx @@ -12,7 +12,7 @@ enum InfoBlockVariant { interface InfoBlockProps extends React.InputHTMLAttributes { variant?: InfoBlockVariant; - children: string | React.ReactElement; + children: string | React.ReactNode; } export const InfoBlock = ({ children, variant }: InfoBlockProps) => ( diff --git a/extension/src/popup/basics/InfoBlock/styles.scss b/extension/src/popup/basics/InfoBlock/styles.scss index ec8773799f..a65e8179b7 100644 --- a/extension/src/popup/basics/InfoBlock/styles.scss +++ b/extension/src/popup/basics/InfoBlock/styles.scss @@ -1,6 +1,7 @@ .BasicInfoBlock { .InfoBlock { border: none; + border-radius: 0.5rem; display: flex; font-size: var(--font-size-secondary); gap: 0.5rem; diff --git a/extension/src/popup/basics/LoadingBackground/index.tsx b/extension/src/popup/basics/LoadingBackground/index.tsx new file mode 100644 index 0000000000..6594b6912e --- /dev/null +++ b/extension/src/popup/basics/LoadingBackground/index.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +import "./styles.scss"; + +interface LoadingBackgroundProps { + onClick?: () => void; + isActive: boolean; +} + +export const LoadingBackground = ({ + isActive, + onClick, +}: LoadingBackgroundProps) => ( +
+); diff --git a/extension/src/popup/basics/LoadingBackground/styles.scss b/extension/src/popup/basics/LoadingBackground/styles.scss new file mode 100644 index 0000000000..0c4f111ef3 --- /dev/null +++ b/extension/src/popup/basics/LoadingBackground/styles.scss @@ -0,0 +1,17 @@ +.LoadingBackground { + opacity: 0; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: -1; + transition: all var(--dropdown-animation); + transition-delay: 0.1s; + + &--active { + background-color: rgba(var(--pal-shadow-rbg), 0.6); + opacity: 1; + z-index: 1; + } +} diff --git a/extension/src/popup/basics/TransactionHeading/index.tsx b/extension/src/popup/basics/TransactionHeading/index.tsx new file mode 100644 index 0000000000..48706a6364 --- /dev/null +++ b/extension/src/popup/basics/TransactionHeading/index.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +import "./styles.scss"; + +interface TransactionHeadingProps { + children: React.ReactNode; +} + +export const TransactionHeading = ({ children }: TransactionHeadingProps) => ( +
{children}
+); diff --git a/extension/src/popup/basics/TransactionHeading/styles.scss b/extension/src/popup/basics/TransactionHeading/styles.scss new file mode 100644 index 0000000000..af46772524 --- /dev/null +++ b/extension/src/popup/basics/TransactionHeading/styles.scss @@ -0,0 +1,9 @@ +.TransactionHeading { + border-bottom: 1px solid var(--pal-border-secondary); + font-size: 0.875rem; + font-weight: var(--font-weight-medium); + line-height: 0.72rem; + margin-bottom: 1rem; + padding-bottom: 1rem; + text-transform: uppercase; +} diff --git a/extension/src/popup/components/BottomNav/index.tsx b/extension/src/popup/components/BottomNav/index.tsx index d2385f0b1b..9e2647d119 100644 --- a/extension/src/popup/components/BottomNav/index.tsx +++ b/extension/src/popup/components/BottomNav/index.tsx @@ -6,6 +6,7 @@ import { ROUTES } from "popup/constants/routes"; import HistoryIcon from "popup/assets/icon-history.svg"; import WalletIcon from "popup/assets/icon-wallet.svg"; import SettingsIcon from "popup/assets/icon-settings.svg"; +import SwapIcon from "popup/assets/icon-swap.svg"; import "./styles.scss"; @@ -32,7 +33,9 @@ export const BottomNav = () => ( history icon - {/* swap icon */} + + swap icon + settings icon diff --git a/extension/src/popup/components/Header/index.tsx b/extension/src/popup/components/Header/index.tsx index 9b62cde65f..f28fcbac12 100644 --- a/extension/src/popup/components/Header/index.tsx +++ b/extension/src/popup/components/Header/index.tsx @@ -1,7 +1,4 @@ import React from "react"; -import { useSelector } from "react-redux"; - -import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; import FreighterLogoLockup from "popup/assets/logo-lockup-freighter.svg"; @@ -11,25 +8,10 @@ interface HeaderProps { isPopupView?: boolean; } -export const Header = ({ isPopupView = false }: HeaderProps) => { - const { isTestnet, networkName } = useSelector( - settingsNetworkDetailsSelector, - ); - return ( -
-
- Freighter logo - {isPopupView ? null : ( -
-
-
{networkName}
-
- )} -
-
- ); -}; +export const Header = ({ isPopupView = false }: HeaderProps) => ( +
+
+ Freighter logo +
+
+); diff --git a/extension/src/popup/components/Header/styles.scss b/extension/src/popup/components/Header/styles.scss index cfedcc9d42..cc21b269c9 100644 --- a/extension/src/popup/components/Header/styles.scss +++ b/extension/src/popup/components/Header/styles.scss @@ -15,31 +15,4 @@ margin: 0 auto; max-width: var(--fullscreen--max-width); } - - &__network { - background: var(--pal-background-secondary); - border-radius: 8rem; - align-items: center; - display: flex; - padding: 0.5rem 0.875rem; - - &__icon { - background: var(--pal-success); - border-radius: 2rem; - height: 0.6875rem; - margin-right: 0.5rem; - position: relative; - width: 0.6875rem; - - &--testnet { - background: var(--pal-warning); - } - } - - &__name { - font-size: 0.875rem; - font-weight: var(--font-weight-medium); - text-transform: uppercase; - } - } } diff --git a/extension/src/popup/components/PunycodedDomain/index.tsx b/extension/src/popup/components/PunycodedDomain/index.tsx index 9d1e3ce36b..3e13c2906b 100644 --- a/extension/src/popup/components/PunycodedDomain/index.tsx +++ b/extension/src/popup/components/PunycodedDomain/index.tsx @@ -8,10 +8,12 @@ import "./styles.scss"; export const PunycodedDomain = ({ domain, domainTitle, + isRow, ...props }: { domain: string; domainTitle?: string; + isRow?: boolean; }) => { const punycodedDomain = getPunycodedDomain(domain); const isDomainValid = punycodedDomain === domain; @@ -19,16 +21,23 @@ export const PunycodedDomain = ({ const favicon = getSiteFavicon(domain); return ( -
+
- Site favicon + Site favicon
-
+
{isDomainValid ? punycodedDomain : `xn-${punycodedDomain}`}
-
{domainTitle}
+
{domainTitle}
); }; diff --git a/extension/src/popup/components/PunycodedDomain/styles.scss b/extension/src/popup/components/PunycodedDomain/styles.scss index 7cc167a66a..ed53626068 100644 --- a/extension/src/popup/components/PunycodedDomain/styles.scss +++ b/extension/src/popup/components/PunycodedDomain/styles.scss @@ -3,18 +3,29 @@ display: flex; flex-direction: column; justify-content: center; - margin-bottom: 1rem; + margin-bottom: 0.5rem; - &--domain { + &--row { + flex-direction: row; + gap: 0.5rem; + justify-content: start; + margin-bottom: 0; + } + + &__favicon { + width: 2rem; + } + + &__domain { color: var(--pal-text-primary); } - &--title { + &__title { color: var(--pal-text-tertiary); font-size: 0.875rem; } div { - margin-bottom: 0.5rem; + margin-bottom: 1rem; } } diff --git a/extension/src/popup/components/account/AccountAssets/index.tsx b/extension/src/popup/components/account/AccountAssets/index.tsx index ff0b5fb383..edf54de192 100644 --- a/extension/src/popup/components/account/AccountAssets/index.tsx +++ b/extension/src/popup/components/account/AccountAssets/index.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from "react"; import { useSelector } from "react-redux"; import { BigNumber } from "bignumber.js"; +import { isEmpty } from "lodash"; import { AssetIcons } from "@shared/api/types"; import { retryAssetIcon } from "@shared/api/internal"; @@ -9,6 +10,8 @@ import { getCanonicalFromAsset } from "helpers/stellar"; import StellarLogo from "popup/assets/stellar-logo.png"; import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; +import ImageMissingIcon from "popup/assets/image-missing.svg"; + import "./styles.scss"; export const AssetIcon = ({ @@ -21,25 +24,64 @@ export const AssetIcon = ({ code: string; issuerKey: string; retryAssetIconFetch?: (arg: { key: string; code: string }) => void; -}) => - assetIcons[getCanonicalFromAsset(code, issuerKey)] || code === "XLM" ? ( - {`${code} { - if (retryAssetIconFetch) { - retryAssetIconFetch({ key: issuerKey, code }); - } - }} - /> +}) => { + /* + We load asset icons in 2 ways: + Method 1. We get an asset's issuer and use that to look up toml info to get the icon path + Method 2. We get an icon path directly from an API (like in the trustline flow) and just pass it to this component to render + */ + + const isXlm = code === "XLM"; + + // in Method 1, while we wait for the icon path to load, `assetIcons` will be empty until the promise resolves + // This does not apply for XLM as there is no lookup as that logo lives in this codebase + const isFetchingAssetIcons = isEmpty(assetIcons) && !isXlm; + + const [hasError, setHasError] = useState(false); + + // For all non-XLM assets (assets where we need to fetch the icon from elsewhere), start by showing a loading state as there is work to do + const [isLoading, setIsLoading] = useState(!isXlm); + + const canonicalAsset = assetIcons[getCanonicalFromAsset(code, issuerKey)]; + const imgSrc = hasError ? ImageMissingIcon : canonicalAsset || ""; + + // If we're waiting on the icon lookup (Method 1), just return the loader until this re-renders with `assetIcons`. We can't do anything until we have it. + if (isFetchingAssetIcons) { + return ( +
+ ); + } + + // if we have an asset path, start loading the path in an `` + return canonicalAsset || isXlm ? ( +
+ {`${code} { + if (retryAssetIconFetch) { + retryAssetIconFetch({ key: issuerKey, code }); + } + // we tried to load an image path but it failed, so show the broken image icon here + setHasError(true); + }} + onLoad={() => { + // we've sucessfully loaded an icon, end the "loading" state + setIsLoading(false); + }} + /> +
) : ( -
+ // the image path wasn't found, show a default broken image icon +
+ Asset icon missing +
); +}; export const AccountAssets = ({ assetIcons: inputAssetIcons, diff --git a/extension/src/popup/components/account/AccountAssets/styles.scss b/extension/src/popup/components/account/AccountAssets/styles.scss index 6a42aac975..e7bea3333b 100644 --- a/extension/src/popup/components/account/AccountAssets/styles.scss +++ b/extension/src/popup/components/account/AccountAssets/styles.scss @@ -1,3 +1,6 @@ +$loader-dark-color: #191b22; +$loader-light-color: #444961; + .AccountAssets { &__asset { align-items: center; @@ -11,12 +14,50 @@ margin-right: 1rem; width: 2rem; height: 2rem; + + img { + width: 100%; + height: 100%; + } + } + + &--error { + align-items: center; + background: rgba(100, 50, 241, 0.32); + border-radius: 2rem; + display: flex; + justify-content: center; + + img { + height: 1rem; + width: 1rem; + } } - &--bullet { - @extend .AccountAssets__asset--logo; - background: var(--pal-brand-primary); - border-radius: 10rem; + &--loading { + background: linear-gradient( + to right, + $loader-dark-color 1%, + $loader-light-color 25%, + $loader-dark-color 50% + ); + background-size: 18.75rem; + animation: loadingAnimation 1.5s linear 0s infinite normal forwards; + border-radius: 2rem; + + img { + width: 0; + height: 0; + } + } + } + + @keyframes loadingAnimation { + 0% { + background-position: -9.375rem 0; + } + 100% { + background-position: 9.375rem 0; } } diff --git a/extension/src/popup/components/account/AccountHeader/index.tsx b/extension/src/popup/components/account/AccountHeader/index.tsx index cdb492ed2a..ad48d77e30 100644 --- a/extension/src/popup/components/account/AccountHeader/index.tsx +++ b/extension/src/popup/components/account/AccountHeader/index.tsx @@ -1,19 +1,18 @@ import React, { useEffect, useRef, useState } from "react"; import { useSelector } from "react-redux"; import { Link } from "react-router-dom"; +import { Icon } from "@stellar/design-system"; import { ROUTES } from "popup/constants/routes"; +import { LoadingBackground } from "popup/basics/LoadingBackground"; import { AccountListIdenticon } from "popup/components/identicons/AccountListIdenticon"; import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; import { Account } from "@shared/api/types"; -import { Icon } from "@stellar/design-system"; -import "./styles.scss"; +import { AccountList } from "popup/components/account/AccountList"; -const ImportedTagEl = () => ( - • Imported -); +import "./styles.scss"; interface AccountHeaderProps { accountDropDownRef: React.RefObject; @@ -64,31 +63,11 @@ export const AccountHeader = ({
    - {allAccounts.map( - ({ publicKey: accountPublicKey, name: accountName, imported }) => { - const isSelected = publicKey === accountPublicKey; - - return ( -
  • - - {imported ? : null} - - - {isSelected ? : null} - -
  • - ); - }, - )} +
-
setIsDropdownOpen(false)} - className={`AccountHeader__dropdown-background ${ - isDropdownOpen ? "activate" : null - }`} - >
+ isActive={isDropdownOpen} + />
); }; diff --git a/extension/src/popup/components/account/AccountHeader/styles.scss b/extension/src/popup/components/account/AccountHeader/styles.scss index 56d30b810d..dcbfe0ac7d 100644 --- a/extension/src/popup/components/account/AccountHeader/styles.scss +++ b/extension/src/popup/components/account/AccountHeader/styles.scss @@ -1,12 +1,13 @@ -$account-row-height: 4.25rem; -$dropdown-animation: 0.3s ease-out; +:root { + --acount-row-height: 4.25rem; +} .AccountHeader { display: flex; flex-direction: row; justify-content: space-between; align-items: center; - height: $account-row-height; + height: var(--acount-row-height); &__icon-btn { cursor: pointer; @@ -53,7 +54,7 @@ $dropdown-animation: 0.3s ease-out; top: 0; left: 0; z-index: calc(var(--z-index-tooltip) + 1); - transition: max-height $dropdown-animation; + transition: max-height var(--dropdown-animation); // default max-height on page load max-height: 0; @@ -63,24 +64,6 @@ $dropdown-animation: 0.3s ease-out; } } - &__dropdown-background { - opacity: 0; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: -1; - transition: all $dropdown-animation; - transition-delay: 0.1s; - - &.activate { - background-color: rgba(var(--pal-shadow-rbg), 0.6); - opacity: 1; - z-index: 1; - } - } - &__option-list-item { padding-bottom: 0.5rem; } @@ -106,38 +89,6 @@ $dropdown-animation: 0.3s ease-out; margin: 0.25rem; } - &__account-list-item { - display: flex; - justify-content: space-between; - width: 100%; - height: $account-row-height; - align-items: center; - - > div { - max-width: 15rem; - } - - .AccountListIdenticon__active-wrapper.active { - border-color: var(--pal-brand-primary-on); - } - } - - &--option-tag { - color: var(--secondary-text); - flex: 1; - text-align: left; - font-size: 0.875rem; - line-height: 1.375rem; - padding-top: 1.1875rem; - } - - &--option-check { - align-items: center; - display: flex; - margin-right: 1rem; - width: 1.25rem; - } - &__list-divider { border: none; height: 1px; diff --git a/extension/src/popup/components/account/AccountList/index.tsx b/extension/src/popup/components/account/AccountList/index.tsx new file mode 100644 index 0000000000..c5f0593ca3 --- /dev/null +++ b/extension/src/popup/components/account/AccountList/index.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { Icon } from "@stellar/design-system"; + +import { AccountListIdenticon } from "popup/components/identicons/AccountListIdenticon"; + +import { Account } from "@shared/api/types"; + +import "./styles.scss"; + +export const ImportedTag = () => ( + • Imported +); + +interface AccountListItemProps { + accountName: string; + isSelected: boolean; + accountPublicKey: string; + setIsDropdownOpen: (isDropdownOpen: boolean) => void; + imported: boolean; +} + +export const AccountListItem = ({ + accountName, + isSelected, + accountPublicKey, + setIsDropdownOpen, + imported, +}: AccountListItemProps) => ( +
  • + + {imported ? : null} + + + {isSelected ? : null} + +
  • +); + +interface AccounsListProps { + allAccounts: Array; + publicKey: string; + setIsDropdownOpen: (isDropdownOpen: boolean) => void; +} + +export const AccountList = ({ + allAccounts, + publicKey, + setIsDropdownOpen, +}: AccounsListProps) => ( + <> + {allAccounts.map( + ({ publicKey: accountPublicKey, name: accountName, imported }) => { + const isSelected = publicKey === accountPublicKey; + + return ( + + ); + }, + )} + +); diff --git a/extension/src/popup/components/account/AccountList/styles.scss b/extension/src/popup/components/account/AccountList/styles.scss new file mode 100644 index 0000000000..8db6918a5c --- /dev/null +++ b/extension/src/popup/components/account/AccountList/styles.scss @@ -0,0 +1,37 @@ +.AccountList { + &__item { + display: flex; + justify-content: space-between; + width: 100%; + height: var(--acount-row-height); + align-items: center; + + > div { + max-width: 15rem; + } + + .AccountListIdenticon__active-wrapper.active { + border-color: var(--pal-brand-primary-on); + } + + &::before { + content: none; + } + } + + &__option-check { + align-items: center; + display: flex; + margin-right: 1rem; + width: 1.25rem; + } + + &__option-tag { + color: var(--secondary-text); + flex: 1; + text-align: left; + font-size: 0.875rem; + line-height: 1.375rem; + padding-top: 1.1875rem; + } +} diff --git a/extension/src/popup/components/manageAssets/AddAsset/index.tsx b/extension/src/popup/components/manageAssets/AddAsset/index.tsx index 7220a0f0a0..53045554af 100644 --- a/extension/src/popup/components/manageAssets/AddAsset/index.tsx +++ b/extension/src/popup/components/manageAssets/AddAsset/index.tsx @@ -81,7 +81,7 @@ export const AddAsset = ({ setErrorAsset }: AddAssetProps) => { void; + selectingAssetType: string; } -export const ChooseAsset = ({ balances, setErrorAsset }: ChooseAssetProps) => { +export const ChooseAsset = ({ + balances, + setErrorAsset, + selectingAssetType, +}: ChooseAssetProps) => { const { assetIcons } = useSelector(transactionSubmissionSelector); const { networkUrl } = useSelector(settingsNetworkDetailsSelector); const [assetRows, setAssetRows] = useState([] as ManageAssetCurrency[]); const ManageAssetRowsWrapperRef = useRef(null); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { const fetchDomains = async () => { + setIsLoading(true); const collection = [] as ManageAssetCurrency[]; const sortedBalances = sortBalances(balances); @@ -60,28 +69,57 @@ export const ChooseAsset = ({ balances, setErrorAsset }: ChooseAssetProps) => { image: assetIcons[getCanonicalFromAsset(code, issuer?.key)], domain, }); + // include native asset for asset dropdown selection + } else if (selectingAssetType) { + collection.push({ + code, + issuer: "", + image: "", + domain: "", + }); } } setAssetRows(collection); + setIsLoading(false); }; fetchDomains(); - }, [assetIcons, balances, networkUrl]); + }, [assetIcons, balances, networkUrl, selectingAssetType]); return (
    - + {isLoading && ( +
    + +
    + )} + : undefined} + />
    - + {selectingAssetType ? ( + + ) : ( + + )}
    - + diff --git a/extension/src/popup/components/manageAssets/ChooseAsset/styles.scss b/extension/src/popup/components/manageAssets/ChooseAsset/styles.scss index d5a25ee86d..89ae1c8ce1 100644 --- a/extension/src/popup/components/manageAssets/ChooseAsset/styles.scss +++ b/extension/src/popup/components/manageAssets/ChooseAsset/styles.scss @@ -1,6 +1,19 @@ .ChooseAsset { padding: var(--popup-vertical-padding) var(--popup--side-padding); + &__loader { + height: var(--popup--height); + width: var(--popup--width); + z-index: calc(var(--back--button-z-index) + 1); + position: absolute; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + top: 0; + left: 0; + } + &__wrapper { display: flex; flex-direction: column; diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx index 627b4a9990..7394f05368 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx @@ -9,7 +9,11 @@ import { AppDispatch } from "popup/App"; import { emitMetric } from "helpers/metrics"; import { navigateTo } from "popup/helpers/navigate"; import { useNetworkFees } from "popup/helpers/useNetworkFees"; -import { getCanonicalFromAsset, xlmToStroop } from "helpers/stellar"; +import { + formatDomain, + getCanonicalFromAsset, + xlmToStroop, +} from "helpers/stellar"; import { PillButton } from "popup/basics/buttons/PillButton"; @@ -36,12 +40,16 @@ import "./styles.scss"; export type ManageAssetCurrency = CURRENCY & { domain: string }; interface ManageAssetRowsProps { + children?: React.ReactNode; + header?: React.ReactNode; assetRows: ManageAssetCurrency[]; setErrorAsset: (errorAsset: string) => void; maxHeight: number; } export const ManageAssetRows = ({ + children, + header, assetRows, setErrorAsset, maxHeight, @@ -135,6 +143,7 @@ export const ManageAssetRows = ({ maxHeight: `${maxHeight}px`, }} > + {header}
    {assetRows.map(({ code, domain, image, issuer }) => { if (!balances) return null; @@ -154,9 +163,7 @@ export const ManageAssetRows = ({
    {code}
    - {domain - ? domain.replace("https://", "").replace("www.", "") - : "Stellar Network"} + {formatDomain(domain)}
    @@ -177,6 +184,7 @@ export const ManageAssetRows = ({ ); })}
    + {children} ); }; diff --git a/extension/src/popup/components/manageAssets/SearchAsset/index.tsx b/extension/src/popup/components/manageAssets/SearchAsset/index.tsx new file mode 100644 index 0000000000..44b7c2489e --- /dev/null +++ b/extension/src/popup/components/manageAssets/SearchAsset/index.tsx @@ -0,0 +1,203 @@ +import React, { useEffect, useCallback, useRef, useState } from "react"; +import { useSelector } from "react-redux"; +import { Link } from "react-router-dom"; +import { Formik, Form, Field, FieldProps } from "formik"; +import { Input, Loader } from "@stellar/design-system"; +import debounce from "lodash/debounce"; + +import { Button } from "popup/basics/buttons/Button"; +import { InfoBlock } from "popup/basics/InfoBlock"; +import { FormRows } from "popup/basics/Forms"; + +import { ROUTES } from "popup/constants/routes"; + +import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; + +import { SubviewHeader } from "popup/components/SubviewHeader"; + +import { ManageAssetRows, ManageAssetCurrency } from "../ManageAssetRows"; + +import "./styles.scss"; + +interface FormValues { + asset: string; +} +const initialValues: FormValues = { + asset: "", +}; + +interface SearchAssetProps { + setErrorAsset: (errorAsset: string) => void; +} + +const AddManualAssetLink = () => ( +
    + Can’t find the asset you’re looking for? +
    + Add it manually +
    +
    +); + +const ResultsHeader = () => ( +
    + +
    + Multiple assets have a similar code, please check the domain before + adding. + +
    +
    +
    +); + +export const SearchAsset = ({ setErrorAsset }: SearchAssetProps) => { + const { isTestnet } = useSelector(settingsNetworkDetailsSelector); + const [assetRows, setAssetRows] = useState([] as ManageAssetCurrency[]); + const [maxHeight, setMaxHeight] = useState(0); + const [isSearching, setIsSearching] = useState(false); + const [hasNoResults, setHasNoResults] = useState(false); + const ResultsRef = useRef(null); + + interface AssetRecord { + asset: string; + domain?: string; + tomlInfo?: { image: string }; + } + + const handleSearch = useCallback( + debounce(async ({ target: { value: asset } }) => { + let res; + if (!asset) { + setAssetRows([]); + return; + } + setIsSearching(true); + + try { + res = await fetch( + `https://api.stellar.expert/explorer/${ + isTestnet ? "testnet" : "public" + }/asset?search=${asset}`, + ); + } catch (e) { + console.error(e); + setIsSearching(false); + throw new Error("Unable to search for assets"); + } + + const resJson = await res.json(); + + setIsSearching(false); + + setAssetRows( + resJson._embedded.records + // only show records that have a domain and domains that don't have just whitespace + .filter( + (record: AssetRecord) => record.domain && /\S/.test(record.domain), + ) + .map((record: AssetRecord) => { + const assetSplit = record.asset.split("-"); + return { + code: assetSplit[0], + issuer: assetSplit[1], + image: record?.tomlInfo?.image, + domain: record.domain, + }; + }), + ); + }, 500), + [], + ); + + useEffect(() => { + setMaxHeight(ResultsRef?.current?.clientHeight || 600); + setHasNoResults(!assetRows.length); + }, [assetRows]); + + return ( + {}}> + {({ dirty }) => ( +
    { + handleSearch(e); + setHasNoResults(false); + }} + > +
    + + +
    + + {({ field }: FieldProps) => ( + + )} + +
    + powered by{" "} + + stellar.expert + +
    +
    +
    + {isSearching ? ( +
    + +
    + ) : null} + + {assetRows.length ? ( + 1 ? : null} + assetRows={assetRows} + setErrorAsset={setErrorAsset} + maxHeight={maxHeight} + > + + + ) : null} + {hasNoResults && dirty && !isSearching ? ( + + ) : null} +
    + {!dirty && hasNoResults ? ( +
    + + + +
    + ) : null} +
    +
    +
    + )} +
    + ); +}; diff --git a/extension/src/popup/components/manageAssets/SearchAsset/styles.scss b/extension/src/popup/components/manageAssets/SearchAsset/styles.scss new file mode 100644 index 0000000000..fea0e654d7 --- /dev/null +++ b/extension/src/popup/components/manageAssets/SearchAsset/styles.scss @@ -0,0 +1,53 @@ +:root { + --SearchAsset--loader--dimension: 1rem; +} + +.SearchAsset { + padding: var(--popup-vertical-padding) var(--popup--side-padding); + + &__search-copy { + font-size: 0.8125rem; + line-height: 1.5rem; + text-align: right; + + a { + color: #00b2ff; + } + } + + &__InfoBlock { + padding-bottom: 1.625rem; + a { + color: var(--pal-brand-primary-on); + text-decoration: underline; + } + } + + &__loader { + position: absolute; + top: calc(50% - var(--SearchAsset--loader--dimension)); + left: calc(50% - var(--SearchAsset--loader--dimension)); + width: var(--SearchAsset--loader--dimension); + } + + &__results { + flex-grow: 1; + height: 21.4rem; + + &--active { + height: 26.1rem; + } + } + + &__copy { + font-size: 0.875rem; + line-height: 1.5rem; + margin-top: 2.875rem; + text-align: center; + + a { + color: var(--pal-brand-primary-on); + text-decoration: underline; + } + } +} diff --git a/extension/src/popup/components/manageAssets/SelectAssetRows/index.tsx b/extension/src/popup/components/manageAssets/SelectAssetRows/index.tsx new file mode 100644 index 0000000000..9bf6cd8c1b --- /dev/null +++ b/extension/src/popup/components/manageAssets/SelectAssetRows/index.tsx @@ -0,0 +1,100 @@ +import React from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useHistory } from "react-router-dom"; +import SimpleBar from "simplebar-react"; +import "simplebar-react/dist/simplebar.min.css"; +import { Types } from "@stellar/wallet-sdk"; + +import { AppDispatch } from "popup/App"; +import { + transactionSubmissionSelector, + saveAsset, + saveDestinationAsset, +} from "popup/ducks/transactionSubmission"; +import { AssetIcon } from "popup/components/account/AccountAssets"; +import { ASSET_SELECT } from "popup/components/sendPayment/SendAmount/AssetSelect"; +import { ManageAssetCurrency } from "popup/components/manageAssets/ManageAssetRows"; +import { getCanonicalFromAsset, formatDomain } from "helpers/stellar"; + +import { Balances } from "@shared/api/types"; + +import "./styles.scss"; + +interface SelectAssetRowsProps { + assetRows: ManageAssetCurrency[]; + maxHeight: number; + selectingAssetType: string; +} + +export const SelectAssetRows = ({ + assetRows, + maxHeight, + selectingAssetType, +}: SelectAssetRowsProps) => { + const { + accountBalances: { balances = {} }, + } = useSelector(transactionSubmissionSelector); + const dispatch: AppDispatch = useDispatch(); + const history = useHistory(); + + const getAccountBalance = (canonical: string) => { + if (!balances) { + return ""; + } + const bal: Types.Balance = balances[canonical as keyof Balances]; + if (bal) { + return bal.total.toString(); + } + return ""; + }; + + return ( + +
    + {assetRows.map(({ code, domain, image, issuer }) => ( +
    { + if (selectingAssetType === ASSET_SELECT.SOURCE) { + dispatch(saveAsset(getCanonicalFromAsset(code, issuer))); + history.goBack(); + } else if (selectingAssetType === ASSET_SELECT.DEST) { + dispatch( + saveDestinationAsset(getCanonicalFromAsset(code, issuer)), + ); + history.goBack(); + } + }} + > + +
    + {code} +
    + {formatDomain(domain)} +
    +
    + {selectingAssetType === ASSET_SELECT.SOURCE && ( +
    + {getAccountBalance(getCanonicalFromAsset(code, issuer))} {code} +
    + )} +
    + ))} +
    +
    + ); +}; diff --git a/extension/src/popup/components/manageAssets/SelectAssetRows/styles.scss b/extension/src/popup/components/manageAssets/SelectAssetRows/styles.scss new file mode 100644 index 0000000000..f359986b6f --- /dev/null +++ b/extension/src/popup/components/manageAssets/SelectAssetRows/styles.scss @@ -0,0 +1,46 @@ +.SelectAssetRows { + &__scrollbar { + padding-right: var(--popup--side-padding); + width: calc(var(--popup--width) - var(--popup--side-padding)); + } + + &__content { + display: flex; + flex-direction: column; + gap: 1.5rem; + justify-content: space-between; + } + + &--scrolling { + padding-right: 0.625rem; + } + + &__row { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + } + + &__icon { + width: 2rem; + } + + &__bullet { + background: var(--pal-brand-primary); + height: 1.5rem; + width: 1.5rem; + } + + &__code { + color: var(--pal-text-primary); + flex-grow: 1; + line-height: 1.5rem; + } + + &__domain { + color: var(--pal-text-tertiary); + font-size: var(--font-size-secondary); + line-height: 1.375rem; + } +} diff --git a/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx b/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx new file mode 100644 index 0000000000..1340eb8966 --- /dev/null +++ b/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx @@ -0,0 +1,116 @@ +import React from "react"; +import { useSelector } from "react-redux"; +import { Icon } from "@stellar/design-system"; + +import { ROUTES } from "popup/constants/routes"; +import { navigateTo } from "popup/helpers/navigate"; +import { AssetIcon } from "popup/components/account/AccountAssets"; +import { transactionSubmissionSelector } from "popup/ducks/transactionSubmission"; + +import "./styles.scss"; + +export enum ASSET_SELECT { + QUERY_PARAM = "assetSelect", + SOURCE = "source", + DEST = "dest", +} + +export function AssetSelect({ + assetCode, + issuerKey, +}: { + assetCode: string; + issuerKey: string; +}) { + const { assetIcons } = useSelector(transactionSubmissionSelector); + + const handleSelectAsset = () => { + navigateTo( + ROUTES.manageAssets, + `?${ASSET_SELECT.QUERY_PARAM}=${ASSET_SELECT.SOURCE}`, + ); + }; + + return ( +
    +
    +
    + + {assetCode} +
    +
    + +
    +
    +
    + ); +} + +export function PathPayAssetSelect({ + source, + assetCode, + issuerKey, + balance, +}: { + source: boolean; + assetCode: string; + issuerKey: string; + balance: string; +}) { + const { assetIcons } = useSelector(transactionSubmissionSelector); + + const handleSelectAsset = () => { + if (source) { + navigateTo( + ROUTES.manageAssets, + `?${ASSET_SELECT.QUERY_PARAM}=${ASSET_SELECT.SOURCE}`, + ); + return; + } + navigateTo( + ROUTES.manageAssets, + `?${ASSET_SELECT.QUERY_PARAM}=${ASSET_SELECT.DEST}`, + ); + }; + + const truncateLongAssetCode = (code: string) => { + if (code.length >= 5) { + return `${code.slice(0, 5)}...`; + } + return code; + }; + + return ( +
    +
    +
    + + {source ? "From" : "To"} + + + + {truncateLongAssetCode(assetCode)} + {" "} + +
    +
    + + {balance && balance !== "0" ? balance : ""}{" "} + {truncateLongAssetCode(assetCode)} + +
    +
    +
    + ); +} diff --git a/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/styles.scss b/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/styles.scss new file mode 100644 index 0000000000..2c2a7f4f2c --- /dev/null +++ b/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/styles.scss @@ -0,0 +1,85 @@ +.AssetSelect { + &__wrapper { + height: 3.5rem; + border: solid 1px var(--pal-background-secondary); + border-radius: 0.5rem; + width: 100%; + display: flex; + align-items: center; + position: relative; + cursor: pointer; + line-height: 1.75rem; + padding: 0.5rem 0.75rem; + + &--path-pay { + &:nth-of-type(1) { + border-bottom: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + &:nth-of-type(2) { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + } + + &__medium-copy { + font-size: 1rem; + line-height: 1.5rem; + font-weight: var(--font-weight-medium); + color: var(--pal-text-primary); + } + + &__light-copy { + font-size: 0.875rem; + line-height: 1rem; + font-weight: var(--font-weight-normal); + color: var(--pal-text-secondary); + + &__label { + min-width: 2rem; + } + } + + &__content { + display: flex; + flex-direction: row; + font-size: 0.875rem; + width: 100%; + + &__left { + display: flex; + align-items: center; + + svg { + width: 1rem; + height: 1rem; + margin: 0 0.5rem; + } + + .AccountAssets__asset--logo { + margin: 0 0.5rem 0 0.75rem; + width: 1.5rem; + height: 1.5rem; + } + + .AccountAssets__asset--error img { + width: 0.75rem; + height: 0.75rem; + } + } + + &__right { + flex-grow: 1; + display: flex; + align-items: center; + justify-content: end; + svg { + height: 1rem; + width: 1rem; + margin-left: 0.5rem; + } + } + } +} diff --git a/extension/src/popup/components/sendPayment/SendAmount/index.tsx b/extension/src/popup/components/sendPayment/SendAmount/index.tsx index 1a9db1a132..4aa9d1f729 100644 --- a/extension/src/popup/components/sendPayment/SendAmount/index.tsx +++ b/extension/src/popup/components/sendPayment/SendAmount/index.tsx @@ -3,20 +3,25 @@ import { useDispatch, useSelector } from "react-redux"; import debounce from "lodash/debounce"; import { BigNumber } from "bignumber.js"; import { useFormik } from "formik"; -import { Types } from "@stellar/wallet-sdk"; -import { Select, Icon, Loader } from "@stellar/design-system"; +import SimpleBar from "simplebar-react"; +import "simplebar-react/dist/simplebar.min.css"; +import { Icon, Loader } from "@stellar/design-system"; import StellarSdk from "stellar-sdk"; +import { + AssetSelect, + PathPayAssetSelect, +} from "popup/components/sendPayment/SendAmount/AssetSelect"; import { InfoBlock } from "popup/basics/InfoBlock"; import { Button } from "popup/basics/buttons/Button"; import { PillButton } from "popup/basics/buttons/PillButton"; -import { PopupWrapper } from "popup/basics/PopupWrapper"; import { ROUTES } from "popup/constants/routes"; import { METRIC_NAMES } from "popup/constants/metricsNames"; import { AppDispatch } from "popup/App"; import { getAssetFromCanonical } from "helpers/stellar"; import { navigateTo } from "popup/helpers/navigate"; import { useNetworkFees } from "popup/helpers/useNetworkFees"; +import { useIsSwap } from "popup/helpers/useIsSwap"; import { emitMetric } from "helpers/metrics"; import { SubviewHeader } from "popup/components/SubviewHeader"; import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; @@ -26,11 +31,13 @@ import { saveAsset, saveDestinationAsset, getBestPath, + resetDestinationAmount, } from "popup/ducks/transactionSubmission"; import { AccountDoesntExistWarning, shouldAccountDoesntExistWarning, } from "popup/components/sendPayment/SendTo"; +import { BottomNav } from "popup/components/BottomNav"; import "../styles.scss"; @@ -59,7 +66,11 @@ const ConversionRate = ({ <> {destAmount ? ( - {sourceAmount} {source} ≈ {destAmount} {dest} + 1 {source} ≈{" "} + {new BigNumber(destAmount) + .div(new BigNumber(sourceAmount)) + .toFixed(7)}{" "} + {dest} ) : ( no path found @@ -69,46 +80,16 @@ const ConversionRate = ({
    ); -const BalanceOption = ({ - balance: [key, balance], -}: { - balance: [string, Types.AssetBalance | Types.NativeBalance]; -}) => { - const [assetDomain, setAssetDomain] = useState("Stellar Lumens"); - const assetIssuer = "issuer" in balance.token ? balance.token.issuer.key : ""; - const networkDetails = useSelector(settingsNetworkDetailsSelector); - const server = new StellarSdk.Server(networkDetails.networkUrl); - - useEffect(() => { - const fetchAssetDomain = async () => { - let homeDomain = ""; - // https://github.com/stellar/freighter/issues/410 - try { - ({ home_domain: homeDomain } = await server.loadAccount(assetIssuer)); - } catch (e) { - console.error(e); - } - - setAssetDomain(homeDomain); - }; - - if (balance.token.type !== "native") { - fetchAssetDomain(); - } - }, [assetIssuer, server, balance.token.type]); - - return ( - - ); -}; - // default so can find a path even if user has not given input const defaultSourceAmount = "1"; -export const SendAmount = ({ previous }: { previous: ROUTES }) => { +export const SendAmount = ({ + previous, + next, +}: { + previous: ROUTES; + next: ROUTES; +}) => { const dispatch: AppDispatch = useDispatch(); const networkDetails = useSelector(settingsNetworkDetailsSelector); @@ -122,7 +103,9 @@ export const SendAmount = ({ previous }: { previous: ROUTES }) => { destinationAsset, } = transactionData; + const isSwap = useIsSwap(); const { recommendedFee } = useNetworkFees(); + const [loadingRate, setLoadingRate] = useState(false); const calculateAvailBalance = useCallback( (selectedAsset: string) => { @@ -156,8 +139,6 @@ export const SendAmount = ({ previous }: { previous: ROUTES }) => { calculateAvailBalance(asset), ); - const [loadingRate, setLoadingRate] = useState(false); - const handleContinue = (values: { amount: string; asset: string; @@ -168,7 +149,8 @@ export const SendAmount = ({ previous }: { previous: ROUTES }) => { if (values.destinationAsset) { dispatch(saveDestinationAsset(values.destinationAsset)); } - navigateTo(ROUTES.sendPaymentSettings); + + navigateTo(next); }; const validate = (values: { amount: string }) => { @@ -186,8 +168,15 @@ export const SendAmount = ({ previous }: { previous: ROUTES }) => { initialValues: { amount, asset, destinationAsset }, onSubmit: handleContinue, validate, + enableReinitialize: true, }); + const showSourceAndDestAsset = !!formik.values.destinationAsset; + const parsedSourceAsset = getAssetFromCanonical(formik.values.asset); + const parsedDestAsset = getAssetFromCanonical( + formik.values.destinationAsset || "native", + ); + const db = useCallback( debounce(async (formikAm, sourceAsset, destAsset) => { await dispatch( @@ -211,6 +200,8 @@ export const SendAmount = ({ previous }: { previous: ROUTES }) => { useEffect(() => { if (!formik.values.destinationAsset) return; setLoadingRate(true); + // clear dest amount before re-calculating for UI + dispatch(resetDestinationAmount()); db( formik.values.amount || defaultSourceAmount, formik.values.asset, @@ -222,8 +213,26 @@ export const SendAmount = ({ previous }: { previous: ROUTES }) => { formik.values.asset, formik.values.destinationAsset, formik.values.amount, + dispatch, ]); + // for swaps we're loading the destinationAsset here + useEffect(() => { + if (isSwap && !destinationAsset) { + // default to first non-native asset if exists + const nonXlmAssets = Object.keys(accountBalances.balances || {}).filter( + (b) => b !== StellarSdk.Asset.native().toString(), + ); + dispatch( + saveDestinationAsset( + nonXlmAssets[0] + ? nonXlmAssets[0] + : StellarSdk.Asset.native().toString(), + ), + ); + } + }, [isSwap, dispatch, destinationAsset, accountBalances]); + const getAmountFontSize = () => { const length = formik.values.amount.length; if (length <= 9) { @@ -257,6 +266,7 @@ export const SendAmount = ({ previous }: { previous: ROUTES }) => { const DecideWarning = () => { // unfunded destination if ( + !isSwap && shouldAccountDoesntExistWarning( destinationBalances.isFunded || false, asset, @@ -283,134 +293,136 @@ export const SendAmount = ({ previous }: { previous: ROUTES }) => { }; return ( - - navigateTo(previous)} - rightButton={ - - } - /> -
    -
    - {availBalance}{" "} - {getAssetFromCanonical(formik.values.asset).code}{" "} - available -
    -
    - { - emitMetric(METRIC_NAMES.sendPaymentSetMax); - formik.setFieldValue( - "amount", - calculateAvailBalance(formik.values.asset), - ); + <> +
    + navigateTo(previous)} + rightButton={ + isSwap ? null : ( + + ) + } + /> +
    +
    + {availBalance} {parsedSourceAsset.code}{" "} + available +
    +
    + { + emitMetric(METRIC_NAMES.sendPaymentSetMax); + formik.setFieldValue( + "amount", + calculateAvailBalance(formik.values.asset), + ); + }} + > + SET MAX + +
    + +
    { + e.preventDefault(); + formik.submitForm(); }} > - SET MAX - -
    - { - e.preventDefault(); - formik.submitForm(); - }} - > - <> - - formik.setFieldValue("amount", formatAmount(e.target.value)) - } - autoFocus - autoComplete="off" - /> -
    - {getAssetFromCanonical(formik.values.asset).code} -
    - {destinationAsset && formik.values.amount !== "0" && ( - - )} -
    - -
    - -
    - -
    - {destinationAsset && ( - <> -
    - - formik.setFieldValue("destinationAsset", e.target.value) + formik.setFieldValue("amount", formatAmount(e.target.value)) } + autoFocus + autoComplete="off" + /> +
    + {parsedSourceAsset.code} +
    + {showSourceAndDestAsset && formik.values.amount !== "0" && ( + + )} +
    - {destinationBalances.balances && - Object.entries( - destinationBalances.balances, - ).map(([k, v]) => )} - -
    -
    - Sending {getAssetFromCanonical(formik.values.asset).code}, they - will receive{" "} - {getAssetFromCanonical(formik.values.destinationAsset).code} + +
    +
    + {!showSourceAndDestAsset && ( + + )} + {showSourceAndDestAsset && ( + <> + + + + )} +
    - - )} - -
    - -
    - + +
    + +
    + +
    - + {isSwap && } + ); }; diff --git a/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx b/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx index 0be16d60d0..f9cade713e 100644 --- a/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx +++ b/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx @@ -1,19 +1,20 @@ import React, { useEffect } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector } from "react-redux"; import { Asset } from "stellar-sdk"; import get from "lodash/get"; import { Icon, TextLink } from "@stellar/design-system"; +import { AssetIcons } from "@shared/api/types"; + import { InfoBlock } from "popup/basics/InfoBlock"; import { Button } from "popup/basics/buttons/Button"; import { getAssetFromCanonical } from "helpers/stellar"; import { navigateTo } from "popup/helpers/navigate"; import { RESULT_CODES } from "popup/helpers/parseTransaction"; +import { useIsSwap } from "popup/helpers/useIsSwap"; import { ROUTES } from "popup/constants/routes"; import { - resetSubmission, - transactionDataSelector, transactionSubmissionSelector, isPathPaymentSelector, } from "popup/ducks/transactionSubmission"; @@ -22,30 +23,81 @@ import { shouldAccountDoesntExistWarning, } from "popup/components/sendPayment/SendTo"; import { FedOrGAddress } from "popup/basics/sendPayment/FedOrGAddress"; +import { AssetIcon } from "popup/components/account/AccountAssets"; import "./styles.scss"; import { emitMetric } from "helpers/metrics"; import { METRIC_NAMES } from "popup/constants/metricsNames"; -export const SubmitSuccess = ({ viewDetails }: { viewDetails: () => void }) => { - const dispatch = useDispatch(); - const { destination, federationAddress, amount, asset } = useSelector( - transactionDataSelector, +const SwapAssetsIcon = ({ + sourceCanon, + destCanon, + assetIcons, +}: { + sourceCanon: string; + destCanon: string; + assetIcons: AssetIcons; +}) => { + const source = getAssetFromCanonical(sourceCanon); + const dest = getAssetFromCanonical(destCanon); + return ( +
    + + {source.code} + + + {dest.code} +
    ); +}; + +export const SubmitSuccess = ({ viewDetails }: { viewDetails: () => void }) => { + const { + transactionData: { + destination, + federationAddress, + amount, + asset, + destinationAsset, + }, + assetIcons, + } = useSelector(transactionSubmissionSelector); + const isSwap = useIsSwap(); - const horizonAsset = getAssetFromCanonical(asset); + const sourceAsset = getAssetFromCanonical(asset); return (
    -
    Successfuly sent
    +
    + Successfuly {isSwap ? "swapped" : "sent"} +
    - {amount} {horizonAsset.code} + {amount} {sourceAsset.code}
    - + {isSwap ? ( + + ) : ( + + )}
    - +
    )}
    diff --git a/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/styles.scss b/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/styles.scss index 7187db44bf..8703b856b1 100644 --- a/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/styles.scss +++ b/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/styles.scss @@ -14,10 +14,15 @@ padding: 1rem; } - img { + .AccountAssets__asset--logo { width: 1.5rem; height: 1.5rem; } + + .AccountAssets__asset--error img { + width: 0.75rem; + height: 0.75rem; + } } &__row { @@ -107,3 +112,56 @@ } } } + +.TwoAssetCard { + display: flex; + flex-direction: column; + border-radius: 0.5rem; + border: 1px solid var(--pal-background-secondary); + position: relative; + margin-bottom: 1.5rem; + + &__row:first-child { + border-bottom: 1px solid var(--pal-background-secondary); + } + + &__row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1rem; + height: 4.25rem; + + &__left { + display: flex; + align-items: center; + font-weight: var(--font-weight-medium); + color: var(--pal-text-primary); + } + + &__right { + font-weight: var(--font-weight-medium); + color: var(--pal-text-primary); + } + } + + &__arrow-icon { + position: absolute; + top: calc((100% - 2.5rem) / 2); + left: calc((100% - 2.5rem) / 2); + width: 2.5rem; + height: 2.5rem; + border: 1px solid var(--pal-background-secondary); + border-radius: 2.5rem; + z-index: 2; + display: flex; + align-items: center; + justify-content: center; + background: var(--background-color); + + svg { + width: 1.5rem; + height: 1.5rem; + } + } +} diff --git a/extension/src/popup/components/sendPayment/SendSettings/Slippage/index.tsx b/extension/src/popup/components/sendPayment/SendSettings/Slippage/index.tsx index ec10522d13..6f3a356b0c 100644 --- a/extension/src/popup/components/sendPayment/SendSettings/Slippage/index.tsx +++ b/extension/src/popup/components/sendPayment/SendSettings/Slippage/index.tsx @@ -25,7 +25,7 @@ import "./styles.scss"; const defaultSlippage = "1"; -export const SendSettingsSlippage = () => { +export const SendSettingsSlippage = ({ previous }: { previous: ROUTES }) => { const dispatch = useDispatch(); const { allowedSlippage } = useSelector(transactionDataSelector); @@ -41,7 +41,7 @@ export const SendSettingsSlippage = () => { navigateTo(ROUTES.sendPaymentSettings)} + customBackAction={() => navigateTo(previous)} customBackIcon={} rightButton={ { values.customSlippage || values.presetSlippage, ), ); - navigateTo(ROUTES.sendPaymentSettings); + navigateTo(previous); }} validationSchema={YupObject().shape({ customSlippage: YupNumber().max(10, "must be below 10%"), diff --git a/extension/src/popup/components/sendPayment/SendSettings/TransactionFee/index.tsx b/extension/src/popup/components/sendPayment/SendSettings/TransactionFee/index.tsx index 8d859994c4..d21273d8fb 100644 --- a/extension/src/popup/components/sendPayment/SendSettings/TransactionFee/index.tsx +++ b/extension/src/popup/components/sendPayment/SendSettings/TransactionFee/index.tsx @@ -1,5 +1,6 @@ import React from "react"; import { useDispatch, useSelector } from "react-redux"; + import { Formik, Form, Field, FieldProps } from "formik"; import { Input, Icon, TextLink, DetailsTooltip } from "@stellar/design-system"; @@ -18,7 +19,7 @@ import { import "./styles.scss"; -export const SendSettingsFee = () => { +export const SendSettingsFee = ({ previous }: { previous: ROUTES }) => { const dispatch = useDispatch(); const { transactionFee } = useSelector(transactionDataSelector); const { networkCongestion, recommendedFee } = useNetworkFees(); @@ -27,7 +28,7 @@ export const SendSettingsFee = () => { navigateTo(ROUTES.sendPaymentSettings)} + customBackAction={() => navigateTo(previous)} customBackIcon={} rightButton={ { initialValues={{ transactionFee }} onSubmit={(values) => { dispatch(saveTransactionFee(String(values.transactionFee))); - navigateTo(ROUTES.sendPaymentSettings); + navigateTo(previous); }} > {({ setFieldValue }) => ( diff --git a/extension/src/popup/components/sendPayment/SendSettings/index.tsx b/extension/src/popup/components/sendPayment/SendSettings/index.tsx index a140df685b..75c460acfa 100644 --- a/extension/src/popup/components/sendPayment/SendSettings/index.tsx +++ b/extension/src/popup/components/sendPayment/SendSettings/index.tsx @@ -11,6 +11,8 @@ import { import { Button } from "popup/basics/buttons/Button"; import { navigateTo } from "popup/helpers/navigate"; import { useNetworkFees } from "popup/helpers/useNetworkFees"; +import { useIsSwap } from "popup/helpers/useIsSwap"; +import { isMuxedAccount } from "helpers/stellar"; import { ROUTES } from "popup/constants/routes"; import { PopupWrapper } from "popup/basics/PopupWrapper"; import { SubviewHeader } from "popup/components/SubviewHeader"; @@ -24,12 +26,19 @@ import { import "../styles.scss"; -export const SendSettings = ({ previous }: { previous: ROUTES }) => { +export const SendSettings = ({ + previous, + next, +}: { + previous: ROUTES; + next: ROUTES; +}) => { const dispatch = useDispatch(); const { destination, transactionFee, memo, allowedSlippage } = useSelector( transactionDataSelector, ); const isPathPayment = useSelector(isPathPaymentSelector); + const isSwap = useIsSwap(); const { recommendedFee } = useNetworkFees(); // use default transaction fee if unset @@ -39,11 +48,23 @@ export const SendSettings = ({ previous }: { previous: ROUTES }) => { } }, [dispatch, recommendedFee, transactionFee]); + const handleTxFeeNav = () => + navigateTo(isSwap ? ROUTES.swapSettingsFee : ROUTES.sendPaymentSettingsFee); + + const handleSlippageNav = () => + navigateTo( + isSwap ? ROUTES.swapSettingsSlippage : ROUTES.sendPaymentSettingsSlippage, + ); + + // dont show memo for regular sends to Muxed, or for swaps + const showMemo = !isSwap && !isMuxedAccount(destination); + const showSlippage = isPathPayment || isSwap; + return (
    navigateTo(previous)} /> {
    - + { + submitForm(); + handleTxFeeNav(); + }} + > Transaction fee {
    -
    +
    { + submitForm(); + handleTxFeeNav(); + }} + > {transactionFee} XLM
    -
    { - submitForm(); - navigateTo(ROUTES.sendPaymentSettingsFee); - }} - > - -
    +
    - {isPathPayment && ( + {showSlippage && (
    - + { + submitForm(); + handleSlippageNav(); + }} + > Allowed slippage {
    -
    +
    { + submitForm(); + handleSlippageNav(); + }} + > {allowedSlippage}%
    -
    { - submitForm(); - navigateTo(ROUTES.sendPaymentSettingsSlippage); - }} - > - -
    +
    )} - {!destination.startsWith("M") && ( + {showMemo && ( <>
    @@ -182,9 +211,9 @@ export const SendSettings = ({ previous }: { previous: ROUTES }) => { fullWidth type="submit" variant={Button.variant.tertiary} - onClick={() => navigateTo(ROUTES.sendPaymentConfirm)} + onClick={() => navigateTo(next)} > - Review Send + Review {isSwap ? "Swap" : "Send"}
    diff --git a/extension/src/popup/components/sendPayment/SendTo/index.tsx b/extension/src/popup/components/sendPayment/SendTo/index.tsx index 717aa97ade..fa0f89aa37 100644 --- a/extension/src/popup/components/sendPayment/SendTo/index.tsx +++ b/extension/src/popup/components/sendPayment/SendTo/index.tsx @@ -6,7 +6,11 @@ import { useFormik } from "formik"; import BigNumber from "bignumber.js"; import { Input, Loader, TextLink } from "@stellar/design-system"; -import { truncatedPublicKey } from "helpers/stellar"; +import { + isFederationAddress, + isMuxedAccount, + truncatedPublicKey, +} from "helpers/stellar"; import { AppDispatch } from "popup/App"; import { SubviewHeader } from "popup/components/SubviewHeader"; @@ -108,8 +112,6 @@ export const SendTo = ({ previous }: { previous: ROUTES }) => { }, }); - const isFederationAddress = (address: string) => address.includes("*"); - const isValidPublicKey = (publicKey: string) => { if (StrKey.isValidMed25519PublicKey(publicKey)) { return true; @@ -132,7 +134,7 @@ export const SendTo = ({ previous }: { previous: ROUTES }) => { return; } // muxed account - if (inputDest.startsWith("M")) { + if (isMuxedAccount(inputDest)) { setValidatedPubKey(inputDest); } // federation address @@ -181,7 +183,7 @@ export const SendTo = ({ previous }: { previous: ROUTES }) => { // TODO - remove once wallet-sdk can handle muxed let publicKey = validatedPubKey; - if (validatedPubKey.startsWith("M")) { + if (isMuxedAccount(validatedPubKey)) { const mAccount = MuxedAccount.fromAddress(validatedPubKey, "0"); publicKey = mAccount.baseAccount().accountId(); } diff --git a/extension/src/popup/components/sendPayment/styles.scss b/extension/src/popup/components/sendPayment/styles.scss index d6ccf21470..65f9f2d240 100644 --- a/extension/src/popup/components/sendPayment/styles.scss +++ b/extension/src/popup/components/sendPayment/styles.scss @@ -17,10 +17,37 @@ } .SendAmount { + padding: var(--popup-vertical-padding) var(--account-view-padding-left) 1rem + var(--account-view-padding-left); + height: var(--popup--height); display: flex; flex-direction: column; - align-items: center; - margin-top: -2rem; + + &__full-height { + height: calc(var(--popup--height) - var(--bottom-nav--height)); + } + + &__content { + display: flex; + flex-direction: column; + align-items: center; + flex-grow: 1; + margin-top: -1.5rem; + } + &__simplebar { + margin-top: 1rem; + height: 19rem; + + &__full-height { + height: 23rem; + } + + &__content { + display: flex; + flex-direction: column; + height: 100%; + } + } &__icon-slider { background: none; @@ -29,6 +56,9 @@ display: flex; padding: 0; width: 0.9rem; + height: 1rem; + display: flex; + align-items: center; z-index: var(--back--button-z-index); } @@ -40,7 +70,7 @@ } &__form { - margin-top: 3.5rem; + height: 100%; width: 100%; display: flex; flex-direction: column; @@ -48,11 +78,9 @@ &__amount-warning { margin: 1rem 0; - min-height: 9rem; &__path-payment { margin: 1rem 0; - min-height: 1.5rem; } } @@ -62,6 +90,7 @@ } &__input-amount { + margin-top: 5rem; font-size: 2.5rem; line-height: 4.5rem; font-weight: var(--font-weight-light); @@ -75,6 +104,10 @@ width: 100%; transition: opacity 0.15s ease-in-out; + &__full-height { + margin-top: 2rem; + } + @media (hover: hover) { &:hover, &:focus { @@ -108,17 +141,12 @@ } } - &__path-pay { - &__select { - margin-top: 1rem; - } - &__copy { - text-align: center; - font-size: 0.875rem; - line-height: 1.375rem; - margin-top: 1rem; - color: var(--pal-text-tertiary); - } + &__asset-select-container { + margin-top: auto; + } + + &__btn-continue { + margin-top: auto; } } @@ -221,7 +249,7 @@ } } - &__nav-btn { + &__clickable { cursor: pointer; } } diff --git a/extension/src/popup/components/signTransaction/Operations/styles.scss b/extension/src/popup/components/signTransaction/Operations/styles.scss index c270565a74..617ea9a61b 100644 --- a/extension/src/popup/components/signTransaction/Operations/styles.scss +++ b/extension/src/popup/components/signTransaction/Operations/styles.scss @@ -2,6 +2,7 @@ display: flex; flex-direction: column; gap: 2rem; + margin-bottom: 1.5rem; &--header { color: var(--pal-text-primary); diff --git a/extension/src/popup/components/signTransaction/Transaction/index.tsx b/extension/src/popup/components/signTransaction/Transaction/index.tsx index c5732f0314..3cb539693e 100644 --- a/extension/src/popup/components/signTransaction/Transaction/index.tsx +++ b/extension/src/popup/components/signTransaction/Transaction/index.tsx @@ -2,6 +2,8 @@ import React from "react"; import { FlaggedKeys } from "types/transactions"; +import { TransactionHeading } from "popup/basics/TransactionHeading"; + import { Operations } from "popup/components/signTransaction/Operations"; import "./styles.scss"; @@ -20,13 +22,13 @@ export const Transaction = ({ const { _operations } = transaction; const operationText = - _operations && _operations.length > 1 ? "Operations:" : "Operation:"; + _operations && _operations.length > 1 ? "Operations" : "Operation"; return (
    {_operations ? ( <> -
    {operationText}
    + {operationText} ( -
    -
    -
    - Source account: -
    -
    - -
    -
    - +}: TransactionInfoProps) => ( +
    {_fee ? (
    - Base fee: + Base fee
    {stroopToXlm(_fee).toString()} XLM
    @@ -69,7 +56,7 @@ export const TransactionHeader = ({ {memo ? (
    - Memo: + Memo
    {getMemoDisplay({ memo, isMemoRequired })}
    @@ -78,7 +65,7 @@ export const TransactionHeader = ({ {_sequence ? (
    - Transaction sequence number: + Transaction sequence number
    {_sequence}
    @@ -86,7 +73,7 @@ export const TransactionHeader = ({ {isFeeBump ? (
    - Inner Transaction: + Inner Transaction
    ) : null} diff --git a/extension/src/popup/components/signTransaction/TransactionHeader/styles.scss b/extension/src/popup/components/signTransaction/TransactionInfo/styles.scss similarity index 91% rename from extension/src/popup/components/signTransaction/TransactionHeader/styles.scss rename to extension/src/popup/components/signTransaction/TransactionInfo/styles.scss index 1862021acc..d5fafee58d 100644 --- a/extension/src/popup/components/signTransaction/TransactionHeader/styles.scss +++ b/extension/src/popup/components/signTransaction/TransactionInfo/styles.scss @@ -1,4 +1,4 @@ -.TransactionHeader { +.TransactionInfo { display: flex; flex-direction: column; gap: 1rem; diff --git a/extension/src/popup/constants/metricsNames.ts b/extension/src/popup/constants/metricsNames.ts index 90618daf5c..bf0e7116b4 100644 --- a/extension/src/popup/constants/metricsNames.ts +++ b/extension/src/popup/constants/metricsNames.ts @@ -11,6 +11,7 @@ export const METRIC_NAMES = { viewMnemonicPhrase: "loaded screen: mnemonic phrase", viewMnemonicPhraseConfirm: "loaded screen: confirm mnemonic phrase", viewMnemonicPhraseConfirmed: "loaded screen: account creator finished", + viewPinExtension: "loaded screen: pix extension", viewRecoverAccount: "loaded screen: recover account", viewRecoverAccountSuccess: "loaded screen: recover account: success", viewSignTransaction: "loaded screen: sign transaction", @@ -41,8 +42,16 @@ export const METRIC_NAMES = { sendPaymentPathPaymentSuccess: "send payment: path payment success", sendPaymentError: "send payment: error", + viewSwap: "loaded screen: swap", + swapAmount: "loaded screen: swap amount", + swapSettings: "loaded screen: swap settings", + swapSettingsFee: "loaded screen: swap settings fee", + swapSettingsSlippage: "loaded screen: swap settings slippage", + swapConfirm: "loaded screen: swap confirm", + viewManageAssets: "loaded screen: manage assets", viewAddAsset: "loaded screen: add asset", + viewSearchAsset: "loaded screen: search asset", viewTrustlineError: "loaded screen: trustline error", manageAssetAddAsset: "manage asset: add asset", diff --git a/extension/src/popup/constants/routes.ts b/extension/src/popup/constants/routes.ts index b40534e082..0c71903d78 100644 --- a/extension/src/popup/constants/routes.ts +++ b/extension/src/popup/constants/routes.ts @@ -13,12 +13,19 @@ export enum ROUTES { sendPaymentSettingsFee = "/account/sendPayment/settings/fee", sendPaymentSettingsSlippage = "/account/sendPayment/settings/slippage", sendPaymentConfirm = "/account/sendPayment/confirm", + swap = "/swap", + swapAmount = "/swap/amount", + swapSettings = "/swap/settings", + swapSettingsFee = "/swap/settings/fee", + swapSettingsSlippage = "/swap/settings/slippage", + swapConfirm = "/swap/confirm", addAccount = "/add-account", signTransaction = "/sign-transaction", grantAccess = "/grant-access", mnemonicPhrase = "/mnemonic-phrase", mnemonicPhraseConfirm = "/mnemonic-phrase/confirm", mnemonicPhraseConfirmed = "/mnemonic-phrase-confirmed", + pinExtension = "/pin-extension", unlockAccount = "/unlock-account", verifyAccount = "/verify-account", accountCreator = "/account-creator", @@ -31,5 +38,6 @@ export enum ROUTES { security = "/settings/security", manageAssets = "/manage-assets", addAsset = "/manage-assets/add-asset", + searchAsset = "/manage-assets/search-asset", trustlineError = "/manage-assets/trustline-error", } diff --git a/extension/src/popup/ducks/transactionSubmission.ts b/extension/src/popup/ducks/transactionSubmission.ts index b0b8939a91..0f550aa1db 100644 --- a/extension/src/popup/ducks/transactionSubmission.ts +++ b/extension/src/popup/ducks/transactionSubmission.ts @@ -217,6 +217,10 @@ const transactionSubmissionSlice = createSlice({ initialState, reducers: { resetSubmission: () => initialState, + resetDestinationAmount: (state) => { + state.transactionData.destinationAmount = + initialState.transactionData.destinationAmount; + }, saveDestination: (state, action) => { state.transactionData.destination = action.payload; }, @@ -306,6 +310,7 @@ const transactionSubmissionSlice = createSlice({ export const { resetSubmission, + resetDestinationAmount, saveDestination, saveFederationAddress, saveAmount, diff --git a/extension/src/popup/helpers/useIsSwap.ts b/extension/src/popup/helpers/useIsSwap.ts new file mode 100644 index 0000000000..2373533a8e --- /dev/null +++ b/extension/src/popup/helpers/useIsSwap.ts @@ -0,0 +1,6 @@ +import { useLocation } from "react-router-dom"; + +export const useIsSwap = () => { + const location = useLocation(); + return location.pathname ? location.pathname.includes("swap") : false; +}; diff --git a/extension/src/popup/metrics/views.ts b/extension/src/popup/metrics/views.ts index f07d7e9a39..f87de6b0e1 100644 --- a/extension/src/popup/metrics/views.ts +++ b/extension/src/popup/metrics/views.ts @@ -21,6 +21,7 @@ const routeToEventName = { [ROUTES.unlockAccount]: METRIC_NAMES.viewUnlockAccount, [ROUTES.verifyAccount]: METRIC_NAMES.viewVerifyAccount, [ROUTES.mnemonicPhraseConfirmed]: METRIC_NAMES.viewMnemonicPhraseConfirmed, + [ROUTES.pinExtension]: METRIC_NAMES.viewPinExtension, [ROUTES.accountCreator]: METRIC_NAMES.viewAccountCreator, [ROUTES.recoverAccount]: METRIC_NAMES.viewRecoverAccount, [ROUTES.recoverAccountSuccess]: METRIC_NAMES.viewRecoverAccountSuccess, @@ -41,8 +42,15 @@ const routeToEventName = { METRIC_NAMES.sendPaymentSettingsSlippage, [ROUTES.sendPaymentConfirm]: METRIC_NAMES.sendPaymentConfirm, [ROUTES.manageAssets]: METRIC_NAMES.viewManageAssets, - [ROUTES.addAsset]: METRIC_NAMES.viewManageAssets, + [ROUTES.addAsset]: METRIC_NAMES.viewAddAsset, + [ROUTES.searchAsset]: METRIC_NAMES.viewSearchAsset, [ROUTES.trustlineError]: METRIC_NAMES.viewTrustlineError, + [ROUTES.swap]: METRIC_NAMES.viewSwap, + [ROUTES.swapAmount]: METRIC_NAMES.swapAmount, + [ROUTES.swapSettings]: METRIC_NAMES.swapSettings, + [ROUTES.swapSettingsFee]: METRIC_NAMES.swapSettingsFee, + [ROUTES.swapSettingsSlippage]: METRIC_NAMES.swapSettingsSlippage, + [ROUTES.swapConfirm]: METRIC_NAMES.swapConfirm, }; registerHandler(navigate, (_, a) => { diff --git a/extension/src/popup/styles/global.scss b/extension/src/popup/styles/global.scss index 80a2fa8611..a2186e888c 100644 --- a/extension/src/popup/styles/global.scss +++ b/extension/src/popup/styles/global.scss @@ -21,6 +21,8 @@ --button-bg--tertiary: #303448; --button-bg--primary: #6432f1; --secondary-text: #797a7f; + + --dropdown-animation: 0.3s ease-out; } body { @@ -45,6 +47,10 @@ a { height: 100%; } +.simplebar-content { + height: 100%; +} + // TODO: Update in SDS .Tooltip__content__container { padding: 0.5rem !important; diff --git a/extension/src/popup/views/Account/index.tsx b/extension/src/popup/views/Account/index.tsx index 2cda8730a7..00da63ebac 100644 --- a/extension/src/popup/views/Account/index.tsx +++ b/extension/src/popup/views/Account/index.tsx @@ -19,6 +19,7 @@ import { getAccountBalances, getAssetIcons, transactionSubmissionSelector, + resetSubmission, } from "popup/ducks/transactionSubmission"; import { ROUTES } from "popup/constants/routes"; import { sortBalances } from "popup/helpers/account"; @@ -57,6 +58,9 @@ export const Account = () => { const { balances, isFunded } = accountBalances; useEffect(() => { + // reset to avoid any residual data eg switching between send and swap or + // previous stale sends + dispatch(resetSubmission()); dispatch( getAccountBalances({ publicKey, diff --git a/extension/src/popup/views/FullscreenSuccessMessage/index.tsx b/extension/src/popup/views/FullscreenSuccessMessage/index.tsx index 4c6c49153d..53e6fb5e95 100644 --- a/extension/src/popup/views/FullscreenSuccessMessage/index.tsx +++ b/extension/src/popup/views/FullscreenSuccessMessage/index.tsx @@ -5,10 +5,9 @@ import { emitMetric } from "helpers/metrics"; import { ROUTES } from "popup/constants/routes"; import { METRIC_NAMES } from "popup/constants/metricsNames"; - +import { navigateTo } from "popup/helpers/navigate"; import { InfoBlock } from "popup/basics/InfoBlock"; import { SubmitButtonWrapper } from "popup/basics/Forms"; - import { FullscreenStyle } from "popup/components/FullscreenStyle"; import { Header } from "popup/components/Header"; import { OnboardingHeader } from "popup/components/Onboarding"; @@ -18,6 +17,37 @@ import ExtensionIllo from "popup/assets/illo-extension.png"; import "./styles.scss"; +// userAgent sniffing is not foolproof so shouldn't expect this method +// to be 100% accurate. +const isChrome = () => + navigator.userAgent.toLowerCase().indexOf("chrome") > -1 && !!window.chrome; + +const AvoidScamsWarningBlock = () => ( +
    + +
    +
    + Avoid scams and keep your account safe +
    +
      +
    • + Freighter will never ask for your recovery phrase unless you're + actively importing your account using the browser extension - never + on an external website. +
    • +
    • + Always check the domain of websites you’re using Freighter with +
    • +
    • + Freighter cannot recover your account if you lose your recovery + phrase, so keep it safe +
    • +
    +
    +
    +
    +); + const MnemonicPhraseConfirmedMessage = () => ( <>
    @@ -26,38 +56,20 @@ const MnemonicPhraseConfirmedMessage = () => ( responsibility.

    -
    - -
    -
    - Avoid scams and keep your account safe: -
    -
      -
    • - Freighter will never ask for your recovery phrase unless you're - actively importing your account using the browser extension - - never on an external website. -
    • -
    • - Always check the domain of websites you’re using Freighter with -
    • -
    • - Freighter cannot recover your account if you lose your recovery - phrase, so keep it safe -
    • -
    -
    -
    -
    + @@ -66,28 +78,41 @@ const MnemonicPhraseConfirmedMessage = () => ( const RecoverAccountSuccessMessage = () => ( <>
    -

    You successfully imported your account.

    - Check your account details by clicking on the Freighter icon on your - browser. + You successfully imported your account. Keep your recovery phrase safe, + it’s your responsibility

    + {!isChrome() && ( +

    + Check your account details by clicking on the Freighter icon on your + browser. +

    + )}
    -
    - Extension -
    + {isChrome() ? ( + + ) : ( +
    + Extension +
    + )} diff --git a/extension/src/popup/views/FullscreenSuccessMessage/styles.scss b/extension/src/popup/views/FullscreenSuccessMessage/styles.scss index 286574b527..6631093816 100644 --- a/extension/src/popup/views/FullscreenSuccessMessage/styles.scss +++ b/extension/src/popup/views/FullscreenSuccessMessage/styles.scss @@ -50,6 +50,31 @@ &__infoBlock { margin-bottom: 0.5rem; + + &__list { + margin-top: -0.25rem; + } + + .BasicInfoBlock .InfoBlock--warning { + border: 1px solid rgba(255, 153, 0, 1); + padding: 1rem; + + .InfoBlock { + &__icon { + display: none; + } + + &__header { + font-size: 1rem; + font-weight: var(--font-weight-medium); + line-height: 1.375rem; + margin-bottom: -0.25rem; + padding: 0; + text-transform: uppercase; + white-space: nowrap; + } + } + } } &__illo-container { diff --git a/extension/src/popup/views/ManageAssets/index.tsx b/extension/src/popup/views/ManageAssets/index.tsx index 546bbc1051..f2b87546e5 100644 --- a/extension/src/popup/views/ManageAssets/index.tsx +++ b/extension/src/popup/views/ManageAssets/index.tsx @@ -1,19 +1,33 @@ import React, { useState } from "react"; import { useSelector } from "react-redux"; -import { Redirect, Route, Switch } from "react-router-dom"; +import { Redirect, Route, Switch, useLocation } from "react-router-dom"; import { transactionSubmissionSelector } from "popup/ducks/transactionSubmission"; - import { AddAsset } from "popup/components/manageAssets/AddAsset"; import { ChooseAsset } from "popup/components/manageAssets/ChooseAsset"; +import { SearchAsset } from "popup/components/manageAssets/SearchAsset"; import { TrustlineError } from "popup/components/manageAssets/TrustlineError"; - import { PrivateKeyRoute } from "popup/Router"; import { ROUTES } from "popup/constants/routes"; +import { ASSET_SELECT } from "popup/components/sendPayment/SendAmount/AssetSelect"; export const ManageAssets = () => { const { accountBalances } = useSelector(transactionSubmissionSelector); const [errorAsset, setErrorAsset] = useState(""); + const { search } = useLocation(); + + // find if from asset select input + let selectingAssetType = ""; + const params = new URLSearchParams(search); + switch (params.get(ASSET_SELECT.QUERY_PARAM)) { + case ASSET_SELECT.SOURCE: + selectingAssetType = ASSET_SELECT.SOURCE; + break; + case ASSET_SELECT.DEST: + selectingAssetType = ASSET_SELECT.DEST; + break; + default: + } const { balances } = accountBalances; @@ -31,7 +45,14 @@ export const ManageAssets = () => { <> - + + + + diff --git a/extension/src/popup/views/PinExtension/index.tsx b/extension/src/popup/views/PinExtension/index.tsx new file mode 100644 index 0000000000..6cb49300cb --- /dev/null +++ b/extension/src/popup/views/PinExtension/index.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +import ExtensionsMenu from "popup/assets/extensions-menu.png"; +import ExtensionsPin from "popup/assets/extensions-pin.png"; +import { Header } from "popup/components/Header"; +import { FullscreenStyle } from "popup/components/FullscreenStyle"; + +import "./styles.scss"; + +export const PinExtension = () => ( + <> +
    + +
    +
    +
    + Pin the extension in your browser to access it easily. +
    +
    + 1. Click on the extensions button at the top of your browser’s bar +
    +
    + Extensions Menu +
    +
    + 2. Click on Freighter’s pin button to have it always visibile +
    +
    + Extensions Pin +
    +
    +
    + +); diff --git a/extension/src/popup/views/PinExtension/styles.scss b/extension/src/popup/views/PinExtension/styles.scss new file mode 100644 index 0000000000..42c188199d --- /dev/null +++ b/extension/src/popup/views/PinExtension/styles.scss @@ -0,0 +1,30 @@ +.PinExtension { + margin-top: 4.5rem; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + + &__wrapper { + max-width: 24rem; + } + + &__title { + font-size: 1.5rem; + line-height: 2.25rem; + text-align: center; + color: var(--pal-text-primary); + } + + &__caption { + line-height: 1.5rem; + margin: 2rem 0; + color: var(--pal-text-tertiary); + } + + &__img { + img { + width: 100%; + } + } +} diff --git a/extension/src/popup/views/SendPayment/index.tsx b/extension/src/popup/views/SendPayment/index.tsx index 0af9cc450c..35374e024f 100644 --- a/extension/src/popup/views/SendPayment/index.tsx +++ b/extension/src/popup/views/SendPayment/index.tsx @@ -1,16 +1,8 @@ import React from "react"; -import { useSelector } from "react-redux"; -import { - Switch, - Redirect, - Route, - useLocation, - RouteProps, -} from "react-router-dom"; -import { PublicKeyRoute } from "popup/Router"; +import { Switch, Redirect } from "react-router-dom"; +import { PublicKeyRoute, VerifiedAccountRoute } from "popup/Router"; import { ROUTES } from "popup/constants/routes"; - import { SendTo } from "popup/components/sendPayment/SendTo"; import { SendAmount } from "popup/components/sendPayment/SendAmount"; import { SendType } from "popup/components/sendPayment/SendAmount/SendType"; @@ -18,24 +10,6 @@ import { SendSettings } from "popup/components/sendPayment/SendSettings"; import { SendSettingsFee } from "popup/components/sendPayment/SendSettings/TransactionFee"; import { SendSettingsSlippage } from "popup/components/sendPayment/SendSettings/Slippage"; import { SendConfirm } from "popup/components/sendPayment/SendConfirm"; -import { hasPrivateKeySelector } from "popup/ducks/accountServices"; - -const VerifiedAccountRoute = (props: RouteProps) => { - const location = useLocation(); - const hasPrivateKey = useSelector(hasPrivateKeySelector); - - if (!hasPrivateKey) { - return ( - - ); - } - return ; -}; export const SendPayment = () => ( @@ -46,19 +20,25 @@ export const SendPayment = () => ( - + - + - + - + diff --git a/extension/src/popup/views/SignTransaction/index.tsx b/extension/src/popup/views/SignTransaction/index.tsx index 9e507263f1..637f9cb49f 100644 --- a/extension/src/popup/views/SignTransaction/index.tsx +++ b/extension/src/popup/views/SignTransaction/index.tsx @@ -1,14 +1,31 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { useLocation } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; +import { Card, Icon } from "@stellar/design-system"; +import { FederationServer, MuxedAccount } from "stellar-sdk"; import { TRANSACTION_WARNING } from "constants/transaction"; import { emitMetric } from "helpers/metrics"; -import { getTransactionInfo } from "helpers/stellar"; +import { + getTransactionInfo, + isFederationAddress, + isMuxedAccount, + truncatedPublicKey, +} from "helpers/stellar"; import { decodeMemo } from "popup/helpers/parseTransaction"; import { Button } from "popup/basics/buttons/Button"; +import { InfoBlock } from "popup/basics/InfoBlock"; +import { LoadingBackground } from "popup/basics/LoadingBackground"; +import { TransactionHeading } from "popup/basics/TransactionHeading"; import { rejectTransaction, signTransaction } from "popup/ducks/access"; +import { + allAccountsSelector, + confirmPassword, + hasPrivateKeySelector, + makeAccountActive, + publicKeySelector, +} from "popup/ducks/accountServices"; import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; import { @@ -19,24 +36,31 @@ import { import { METRIC_NAMES } from "popup/constants/metricsNames"; -import { ModalInfo } from "popup/components/ModalInfo"; +import { AccountListIdenticon } from "popup/components/identicons/AccountListIdenticon"; +import { AccountList, ImportedTag } from "popup/components/account/AccountList"; +import { PunycodedDomain } from "popup/components/PunycodedDomain"; import { WarningMessage, FirstTimeWarningMessage, FlaggedWarningMessage, } from "popup/components/WarningMessages"; import { Transaction } from "popup/components/signTransaction/Transaction"; -import { TransactionHeader } from "popup/components/signTransaction/TransactionHeader"; +import { TransactionInfo } from "popup/components/signTransaction/TransactionInfo"; + +import { VerifyAccount } from "popup/views/VerifyAccount"; + +import { Account } from "@shared/api/types"; +import { AppDispatch } from "popup/App"; import "./styles.scss"; export const SignTransaction = () => { const location = useLocation(); - const dispatch = useDispatch(); + const dispatch: AppDispatch = useDispatch(); const { + accountToSign: _accountToSign, transaction, domain, - domainTitle, isDomainListedAllowed, flaggedKeys, } = getTransactionInfo(location.search); @@ -47,11 +71,18 @@ export const SignTransaction = () => { _networkPassphrase, _sequence, } = transaction; + const isFeeBump = !!_innerTransaction; - const source = isFeeBump ? _innerTransaction._source : transaction._source; const memo = decodeMemo(_memo); + let accountToSign = _accountToSign; const [isConfirming, setIsConfirming] = useState(false); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [currentAccount, setCurrentAccount] = useState({} as Account); + const [accountNotFound, setAccountNotFound] = useState(false); + const [isPasswordRequired, setIsPasswordRequired] = useState(false); + + const accountSelectorRef = useRef(null); const rejectAndClose = () => { dispatch(rejectTransaction()); @@ -59,11 +90,30 @@ export const SignTransaction = () => { }; const signAndClose = async () => { - setIsConfirming(true); await dispatch(signTransaction({ transaction })); window.close(); }; + const verifyPasswordThenSign = async (password: string) => { + const confirmPasswordResp = await dispatch(confirmPassword(password)); + + if (confirmPassword.fulfilled.match(confirmPasswordResp)) { + await signAndClose(); + } + }; + + const handleApprove = async () => { + setIsConfirming(true); + + if (hasPrivateKey) { + await signAndClose(); + } else { + setIsPasswordRequired(true); + } + + setIsConfirming(false); + }; + const flaggedKeyValues = Object.values(flaggedKeys); const isUnsafe = flaggedKeyValues.some(({ tags }) => tags.includes(TRANSACTION_WARNING.unsafe), @@ -75,6 +125,42 @@ export const SignTransaction = () => { ({ tags }) => tags.includes(TRANSACTION_WARNING.memoRequired) && !memo, ); + const { networkName, otherNetworkName, networkPassphrase } = useSelector( + settingsNetworkDetailsSelector, + ); + const allAccounts = useSelector(allAccountsSelector); + const publicKey = useSelector(publicKeySelector); + const hasPrivateKey = useSelector(hasPrivateKeySelector); + + // the public key the user had selected before starting this flow + const defaultPublicKey = useRef(publicKey); + const allAccountsMap = useRef({} as { [key: string]: Account }); + + const resolveFederatedAddress = useCallback(async (inputDest) => { + let resolvedPublicKey; + try { + const fedResp = await FederationServer.resolve(inputDest); + resolvedPublicKey = fedResp.account_id; + } catch (e) { + console.error(e); + } + + return resolvedPublicKey; + }, []); + + const decodeAccountToSign = async () => { + if (_accountToSign) { + if (isMuxedAccount(_accountToSign)) { + const mAccount = MuxedAccount.fromAddress(_accountToSign, "0"); + accountToSign = mAccount.baseAccount().accountId(); + } + if (isFederationAddress(_accountToSign)) { + accountToSign = await resolveFederatedAddress(accountToSign); + } + } + }; + decodeAccountToSign(); + useEffect(() => { if (isMemoRequired) { emitMetric(METRIC_NAMES.signTransactionMemoRequired); @@ -87,11 +173,39 @@ export const SignTransaction = () => { } }, [isMemoRequired, isMalicious, isUnsafe]); - const isSubmitDisabled = isMemoRequired || isMalicious; + useEffect(() => { + // handle auto selecting the right account based on `accountToSign` + let autoSelectedAccountDetails; - const { networkName, otherNetworkName, networkPassphrase } = useSelector( - settingsNetworkDetailsSelector, - ); + allAccounts.forEach((account) => { + if (accountToSign) { + // does the user have the `accountToSign` somewhere in the accounts list? + if (account.publicKey === accountToSign) { + // if the `accountToSign` is found, but it isn't active, make it active + if (defaultPublicKey.current !== account.publicKey) { + dispatch(makeAccountActive(account.publicKey)); + } + + // save the details of the `accountToSign` + autoSelectedAccountDetails = account; + } + } + + // create an object so we don't need to keep iterating over allAccounts when we switch accounts + allAccountsMap.current[account.publicKey] = account; + }); + + if (!autoSelectedAccountDetails) { + setAccountNotFound(true); + } + }, [accountToSign, allAccounts, dispatch]); + + useEffect(() => { + // handle any changes to the current acct - whether by auto select or manual select + setCurrentAccount(allAccountsMap.current[publicKey] || ({} as Account)); + }, [allAccounts, publicKey]); + + const isSubmitDisabled = isMemoRequired || isMalicious; if (_networkPassphrase !== networkPassphrase) { return ( @@ -108,8 +222,14 @@ export const SignTransaction = () => { ); } - return ( - <> + return isPasswordRequired ? ( + setIsPasswordRequired(false)} + customSubmit={verifyPasswordThenSign} + /> + ) : ( +
    Confirm Transaction @@ -124,20 +244,47 @@ export const SignTransaction = () => { {!isDomainListedAllowed && !isSubmitDisabled ? ( ) : null} - - - +
    + + +
    + is requesting approval to a {isFeeBump ? "fee bump " : ""} + transaction: +
    +
    +
    + Approve using: +
    +
    setIsDropdownOpen(true)} + > + + {currentAccount.imported ? : null} + +
    + +
    +
    +
    +
    + {accountNotFound && accountToSign ? ( +
    + + The application is requesting a specific account ( + {truncatedPublicKey(accountToSign)}), which is not available on + Freighter. If you own this account, you can import it into + Freighter to complete this transaction. + +
    + ) : null} +
    {isFeeBump ? (
    @@ -154,6 +301,13 @@ export const SignTransaction = () => { transaction={transaction} /> )} + Transaction Info + - +
    + +
    + setIsDropdownOpen(false)} + isActive={isDropdownOpen} + /> +
    ); }; diff --git a/extension/src/popup/views/SignTransaction/styles.scss b/extension/src/popup/views/SignTransaction/styles.scss index b2bf41b329..a9ac3d3b11 100644 --- a/extension/src/popup/views/SignTransaction/styles.scss +++ b/extension/src/popup/views/SignTransaction/styles.scss @@ -1,4 +1,8 @@ .SignTransaction { + height: var(--popup--height); + overflow: hidden; + position: relative; + &__inner-transaction { border: 1px solid var(--pal-border-primary); border-radius: 0.5rem; @@ -8,4 +12,55 @@ padding: 1rem 2rem; zoom: 0.7; } + + &__info { + margin-bottom: 2rem; + } + + &__subject { + border-bottom: 1px solid var(--pal-border-secondary); + color: var(--pal-text-primary); + font-size: 0.875rem; + line-height: 1.5rem; + margin-bottom: 1rem; + padding-bottom: 1rem; + } + + &__approval { + margin-bottom: -1rem; + + &__title { + color: rgba(255, 255, 255, 0.6); + font-size: 0.875rem; + line-height: 1.5rem; + } + } + + &__current-account { + align-items: flex-start; + display: flex; + justify-content: space-between; + margin: 1rem 0 0.5rem 0; + width: 100%; + + &__chevron { + display: flex; + width: 0.75rem; + } + } + + &__account-selector { + background: var(--background-color); + border-radius: 1.5rem 1.5rem 0 0; + box-shadow: 0 1rem 1.5rem rgba(0, 0, 0, 0.24); + padding: 0.5rem 0 0.5rem 1.5rem; + position: absolute; + transition: all var(--dropdown-animation); + width: var(--popup--width); + z-index: var(--z-index-modal); + } + + &__account-not-found { + margin-top: 1rem; + } } diff --git a/extension/src/popup/views/Swap/index.tsx b/extension/src/popup/views/Swap/index.tsx new file mode 100644 index 0000000000..d4a4a5f6d5 --- /dev/null +++ b/extension/src/popup/views/Swap/index.tsx @@ -0,0 +1,71 @@ +import React, { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { Switch, Redirect } from "react-router-dom"; + +import { PublicKeyRoute, VerifiedAccountRoute } from "popup/Router"; +import { ROUTES } from "popup/constants/routes"; +import { SendAmount } from "popup/components/sendPayment/SendAmount"; +import { SendSettings } from "popup/components/sendPayment/SendSettings"; +import { SendSettingsFee } from "popup/components/sendPayment/SendSettings/TransactionFee"; +import { SendSettingsSlippage } from "popup/components/sendPayment/SendSettings/Slippage"; +import { SendConfirm } from "popup/components/sendPayment/SendConfirm"; +import { + getAccountBalances, + getAssetIcons, + transactionSubmissionSelector, +} from "popup/ducks/transactionSubmission"; +import { publicKeySelector } from "popup/ducks/accountServices"; +import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; + +export const Swap = () => { + const dispatch = useDispatch(); + const { accountBalances, assetIcons } = useSelector( + transactionSubmissionSelector, + ); + const publicKey = useSelector(publicKeySelector); + const networkDetails = useSelector(settingsNetworkDetailsSelector); + + // load needed swap data here in case didn't go to home screen first + useEffect(() => { + if (!accountBalances.balances) { + dispatch( + getAccountBalances({ + publicKey, + networkDetails, + }), + ); + } + }, [dispatch, publicKey, networkDetails, accountBalances]); + + useEffect(() => { + if (!accountBalances.balances) return; + if (!Object.keys(assetIcons).length) { + dispatch( + getAssetIcons({ balances: accountBalances.balances, networkDetails }), + ); + } + }, [dispatch, accountBalances, networkDetails, assetIcons]); + + return ( + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/extension/src/popup/views/VerifyAccount/index.tsx b/extension/src/popup/views/VerifyAccount/index.tsx index 6f431816c9..6b863c6831 100644 --- a/extension/src/popup/views/VerifyAccount/index.tsx +++ b/extension/src/popup/views/VerifyAccount/index.tsx @@ -18,23 +18,39 @@ import { SubviewHeader } from "popup/components/SubviewHeader"; import "./styles.scss"; -export const VerifyAccount = () => { +interface VerifyAccountProps { + isApproval?: boolean; + customBackAction?: () => void; + customSubmit?: (password: string) => Promise; +} + +export const VerifyAccount = ({ + isApproval, + customBackAction, + customSubmit, +}: VerifyAccountProps) => { const location = useLocation(); const dispatch = useDispatch(); const authError = useSelector(authErrorSelector); const from = get(location, "state.from.pathname", "") as ROUTES; const handleSubmit = async (values: { password: string }) => { - await dispatch(confirmPassword(values.password)); - navigateTo(from || ROUTES.account); + if (customSubmit) { + await customSubmit(values.password); + } else { + await dispatch(confirmPassword(values.password)); + navigateTo(from || ROUTES.account); + } }; return ( - +
    - Enter your account password to authorize this transaction. You won’t be - asked to do this for the next 24 hours. + {isApproval + ? "Enter your account password to verify your account." + : "Enter your account password to authorize this transaction."}{" "} + You won’t be asked to do this for the next 24 hours.
    {({ dirty, isValid, isSubmitting, errors, touched }) => ( @@ -59,7 +75,7 @@ export const VerifyAccount = () => { isLoading={isSubmitting} disabled={!(dirty && isValid)} > - Submit + {isApproval ? "Approve" : "Submit"}
    diff --git a/extension/src/types/transactions.ts b/extension/src/types/transactions.ts index fc0334274b..a39d9d4bff 100644 --- a/extension/src/types/transactions.ts +++ b/extension/src/types/transactions.ts @@ -12,5 +12,6 @@ export interface TransactionInfo { transaction: { [key: string]: any }; isDomainListedAllowed: boolean; flaggedKeys: FlaggedKeys; - title: string; + title?: string; + accountToSign?: string; }