From f3ef09744661c46c008da90d1445bb0146d235d6 Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Fri, 29 Apr 2022 16:45:12 -0400 Subject: [PATCH 01/22] update stellar-sdk for Protocol 19 (#466) --- @shared/api/package.json | 2 +- @shared/constants/package.json | 2 +- @shared/helpers/package.json | 2 +- extension/package.json | 2 +- .../popup/views/SignTransaction/styles.scss | 2 +- yarn.lock | 34 +++++++++++++------ 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/@shared/api/package.json b/@shared/api/package.json index 2b8565d701..8da79c00f2 100644 --- a/@shared/api/package.json +++ b/@shared/api/package.json @@ -6,7 +6,7 @@ "dependencies": { "@stellar/wallet-sdk": "^0.6.0-rc.1", "prettier": "^2.0.5", - "stellar-sdk": "^10.0.1", + "stellar-sdk": "^10.1.0", "typescript": "~3.7.2", "webextension-polyfill-ts": "^0.22.0" }, diff --git a/@shared/constants/package.json b/@shared/constants/package.json index f0728f2d98..7a161c9d00 100644 --- a/@shared/constants/package.json +++ b/@shared/constants/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "dependencies": { "typescript": "~3.7.2", - "stellar-sdk": "^10.0.1" + "stellar-sdk": "^10.1.0" }, "devDependencies": { "@stellar/prettier-config": "^1.0.1" diff --git a/@shared/helpers/package.json b/@shared/helpers/package.json index 8304a93d27..86e76735a3 100644 --- a/@shared/helpers/package.json +++ b/@shared/helpers/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "dependencies": { "typescript": "~3.7.2", - "stellar-sdk": "^10.0.1" + "stellar-sdk": "^10.1.0" }, "devDependencies": { "@stellar/prettier-config": "^1.0.1" diff --git a/extension/package.json b/extension/package.json index 98b0f221d1..6b79517e0e 100755 --- a/extension/package.json +++ b/extension/package.json @@ -62,7 +62,7 @@ "simplebar-react": "^2.3.6", "stellar-hd-wallet": "^0.0.10", "stellar-identicon-js": "^1.0.0", - "stellar-sdk": "^10.0.1", + "stellar-sdk": "^10.1.0", "svg-url-loader": "^5.0.0", "tsconfig-paths-webpack-plugin": "^3.2.0", "tslib": "2.0.0", diff --git a/extension/src/popup/views/SignTransaction/styles.scss b/extension/src/popup/views/SignTransaction/styles.scss index 5799279557..b2bf41b329 100644 --- a/extension/src/popup/views/SignTransaction/styles.scss +++ b/extension/src/popup/views/SignTransaction/styles.scss @@ -4,7 +4,7 @@ border-radius: 0.5rem; height: 10rem; opacity: 0.7; - overflow: scroll; + overflow: auto; padding: 1rem 2rem; zoom: 0.7; } diff --git a/yarn.lock b/yarn.lock index 03eb1cfdcb..f236e37d40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10908,6 +10908,11 @@ node-gyp-build@^4.1.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739" integrity sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg== +node-gyp-build@^4.3.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.4.0.tgz#42e99687ce87ddeaf3a10b99dc06abc11021f3f4" + integrity sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -14156,6 +14161,13 @@ sodium-native@^2.3.0: nan "^2.14.0" node-gyp-build "^4.1.0" +sodium-native@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-3.3.0.tgz#50ee52ac843315866cce3d0c08ab03eb78f22361" + integrity sha512-rg6lCDM/qa3p07YGqaVD+ciAbUqm6SoO4xmlcfkbU5r1zIGrguXztLiEtaLYTV5U6k8KSIUFmnU3yQUSKmf6DA== + dependencies: + node-gyp-build "^4.3.0" + sort-css-media-queries@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-1.5.0.tgz#8f605ad372caad0b81be010311882c046e738093" @@ -14379,10 +14391,10 @@ stellar-base@^0.13.1: optionalDependencies: sodium-native "^2.3.0" -stellar-base@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/stellar-base/-/stellar-base-7.0.0.tgz#6ebb359fc0a5e8fe6ed855c62de78cbec779b463" - integrity sha512-Sfk/u/6HT+8xSQ4HvTI3XgthTS3fhv/ie6Jx4OzLvg81pt09bDDN5YvRbG6v3gZdiRA0pwg2RRz5YS3ph5ePEg== +stellar-base@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/stellar-base/-/stellar-base-8.0.0.tgz#619e5fb7951fa8abb26322e51e611169a5d1cf62" + integrity sha512-MH0V94Hn/Ekq/yZxd+EQijkvfU+21zuqJAyEP8X+fhRR0IQYwGNByY003oLqIkgwqXotyofEQ76W5MoNFZmlpw== dependencies: base32.js "^0.1.0" bignumber.js "^4.0.0" @@ -14390,9 +14402,9 @@ stellar-base@^7.0.0: js-xdr "^1.1.3" lodash "^4.17.21" sha.js "^2.3.6" - tweetnacl "^1.0.0" + tweetnacl "^1.0.3" optionalDependencies: - sodium-native "^2.3.0" + sodium-native "^3.3.0" stellar-hd-wallet@^0.0.10: version "0.0.10" @@ -14411,10 +14423,10 @@ stellar-identicon-js@^1.0.0: dependencies: html-webpack-plugin "^3.2.0" -stellar-sdk@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/stellar-sdk/-/stellar-sdk-10.0.1.tgz#0a5ec3cd913c2a32897c1ef13abf41017b91634f" - integrity sha512-NWvDAldw4GVFqWo1wGoR9lCyJ6xIcO2ab4YX8BQYp1Z45/Rogv3Kj3RERsso/9m+MR+d2OfTmAhIy0vruRGv/Q== +stellar-sdk@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/stellar-sdk/-/stellar-sdk-10.1.0.tgz#91cf9ed3c84c9a9d4c8aaa6cca29bd997b7c778a" + integrity sha512-8PTKuyrQ5bIbsyrLav+JE/c+rrLynBygiB4dpvG30ou7K8P6a5ainmGyVMbCzlz1OkKWoaAW5rJAkl3RhlTZkA== dependencies: "@types/eventsource" "^1.1.2" "@types/node" ">= 8" @@ -14427,7 +14439,7 @@ stellar-sdk@^10.0.1: eventsource "^1.0.7" lodash "^4.17.21" randombytes "^2.1.0" - stellar-base "^7.0.0" + stellar-base "^8.0.0" toml "^2.3.0" tslib "^1.10.0" urijs "^1.19.1" From 4426737efc885b179468a2b2352da799606f548d Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Wed, 11 May 2022 18:00:31 -0400 Subject: [PATCH 02/22] add additional tracking for payments and manage assets (#482) * add additional tracking for payments * undo formatting * add manage asset tracking * emitMetric only when submission succeeds --- .../manageAssets/ManageAssetRows/index.tsx | 8 ++++++++ .../components/manageAssets/TrustlineError/index.tsx | 7 +++++++ .../sendPayment/SendAmount/SendType/index.tsx | 7 +++++++ .../components/sendPayment/SendAmount/index.tsx | 3 +++ .../sendPayment/SendConfirm/SubmitResult/index.tsx | 8 +++++++- .../SendConfirm/TransactionDetails/index.tsx | 12 ++++++++++++ .../popup/components/sendPayment/SendTo/index.tsx | 3 +++ extension/src/popup/constants/metricsNames.ts | 12 ++++++++++++ 8 files changed, 59 insertions(+), 1 deletion(-) diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx index eb5f34ca3a..c26ca9c2b5 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx @@ -6,11 +6,13 @@ import "simplebar-react/dist/simplebar.min.css"; import { AppDispatch } from "popup/App"; +import { emitMetric } from "helpers/metrics"; import { navigateTo } from "popup/helpers/navigate"; import { getCanonicalFromAsset } from "helpers/stellar"; import { PillButton } from "popup/basics/buttons/PillButton"; +import { METRIC_NAMES } from "popup/constants/metricsNames"; import { ROUTES } from "popup/constants/routes"; import { publicKeySelector } from "popup/ducks/accountServices"; @@ -108,6 +110,12 @@ export const ManageAssetRows = ({ }), ); dispatch(resetSubmission()); + emitMetric( + addTrustline + ? METRIC_NAMES.manageAssetAddAsset + : METRIC_NAMES.manageAssetRemoveAsset, + { assetCode, assetIssuer }, + ); navigateTo(ROUTES.account); } diff --git a/extension/src/popup/components/manageAssets/TrustlineError/index.tsx b/extension/src/popup/components/manageAssets/TrustlineError/index.tsx index 20e2263818..fcd635b84a 100644 --- a/extension/src/popup/components/manageAssets/TrustlineError/index.tsx +++ b/extension/src/popup/components/manageAssets/TrustlineError/index.tsx @@ -10,8 +10,11 @@ import { transactionSubmissionSelector, } from "popup/ducks/transactionSubmission"; +import { emitMetric } from "helpers/metrics"; import { getResultCodes, RESULT_CODES } from "popup/helpers/parseTransaction"; +import { METRIC_NAMES } from "popup/constants/metricsNames"; + import { Balances } from "@shared/api/types"; import "./styles.scss"; @@ -117,6 +120,10 @@ export const TrustlineError = ({ const { error } = useSelector(transactionSubmissionSelector); const [assetBalance, setAssetBalance] = useState(""); + useEffect(() => { + emitMetric(METRIC_NAMES.manageAssetError, { error }); + }, [error]); + useEffect(() => { if (!balances) return; const balance = balances[errorAsset]; diff --git a/extension/src/popup/components/sendPayment/SendAmount/SendType/index.tsx b/extension/src/popup/components/sendPayment/SendAmount/SendType/index.tsx index 3b3b4fcb75..0599aa6eaa 100644 --- a/extension/src/popup/components/sendPayment/SendAmount/SendType/index.tsx +++ b/extension/src/popup/components/sendPayment/SendAmount/SendType/index.tsx @@ -5,6 +5,8 @@ import { DetailsTooltip, Icon, TextLink } from "@stellar/design-system"; import { Button } from "popup/basics/buttons/Button"; import { navigateTo } from "popup/helpers/navigate"; +import { emitMetric } from "helpers/metrics"; +import { METRIC_NAMES } from "popup/constants/metricsNames"; import { ROUTES } from "popup/constants/routes"; import { PopupWrapper } from "popup/basics/PopupWrapper"; import { SubviewHeader } from "popup/components/SubviewHeader"; @@ -83,6 +85,11 @@ export const SendType = () => { values.paymentType === PAYMENT_TYPES.PATH_PAYMENT ? "native" : "", ), ); + emitMetric( + values.paymentType === PAYMENT_TYPES.PATH_PAYMENT + ? METRIC_NAMES.sendPaymentTypePathPayment + : METRIC_NAMES.sendPaymentTypePayment, + ); navigateTo(ROUTES.sendPaymentAmount); }; diff --git a/extension/src/popup/components/sendPayment/SendAmount/index.tsx b/extension/src/popup/components/sendPayment/SendAmount/index.tsx index a52ad44ca5..322c526afe 100644 --- a/extension/src/popup/components/sendPayment/SendAmount/index.tsx +++ b/extension/src/popup/components/sendPayment/SendAmount/index.tsx @@ -12,10 +12,12 @@ 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 { emitMetric } from "helpers/metrics"; import { SubviewHeader } from "popup/components/SubviewHeader"; import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; import { @@ -303,6 +305,7 @@ export const SendAmount = ({ previous }: { previous: ROUTES }) => {
{ + emitMetric(METRIC_NAMES.sendPaymentSetMax); formik.setFieldValue( "amount", calculateAvailBalance(formik.values.asset), diff --git a/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx b/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx index eb7c169abc..0be16d60d0 100644 --- a/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx +++ b/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; import { Asset } from "stellar-sdk"; import get from "lodash/get"; @@ -24,6 +24,8 @@ import { import { FedOrGAddress } from "popup/basics/sendPayment/FedOrGAddress"; import "./styles.scss"; +import { emitMetric } from "helpers/metrics"; +import { METRIC_NAMES } from "popup/constants/metricsNames"; export const SubmitSuccess = ({ viewDetails }: { viewDetails: () => void }) => { const dispatch = useDispatch(); @@ -75,6 +77,10 @@ export const SubmitFail = () => { const horizonAsset = getAssetFromCanonical(asset); + useEffect(() => { + emitMetric(METRIC_NAMES.sendPaymentError, { error }); + }, [error]); + const decideError = () => { // unfunded destination if ( diff --git a/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/index.tsx b/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/index.tsx index 8e312a08ef..a8fcd21366 100644 --- a/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/index.tsx +++ b/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/index.tsx @@ -30,6 +30,8 @@ import { import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; import { publicKeySelector } from "popup/ducks/accountServices"; import { navigateTo, openTab } from "popup/helpers/navigate"; +import { emitMetric } from "helpers/metrics"; +import { METRIC_NAMES } from "popup/constants/metricsNames"; import { SubviewHeader } from "popup/components/SubviewHeader"; import { FedOrGAddress } from "popup/basics/sendPayment/FedOrGAddress"; import { AccountAssets } from "popup/components/account/AccountAssets"; @@ -157,6 +159,16 @@ export const TransactionDetails = ({ goBack }: { goBack: () => void }) => { await dispatch( addRecentAddress({ publicKey: federationAddress || destination }), ); + + if (isPathPayment) { + emitMetric(METRIC_NAMES.sendPaymentPathPaymentSuccess, { + sourceAsset, + destAsset, + allowedSlippage, + }); + } else { + emitMetric(METRIC_NAMES.sendPaymentSuccess, { sourceAsset }); + } } } } catch (e) { diff --git a/extension/src/popup/components/sendPayment/SendTo/index.tsx b/extension/src/popup/components/sendPayment/SendTo/index.tsx index c11750ccf9..717aa97ade 100644 --- a/extension/src/popup/components/sendPayment/SendTo/index.tsx +++ b/extension/src/popup/components/sendPayment/SendTo/index.tsx @@ -13,7 +13,9 @@ import { SubviewHeader } from "popup/components/SubviewHeader"; import { IdenticonImg } from "popup/components/identicons/IdenticonImg"; import { InfoBlock } from "popup/basics/InfoBlock"; import { FormRows } from "popup/basics/Forms"; +import { emitMetric } from "helpers/metrics"; import { navigateTo } from "popup/helpers/navigate"; +import { METRIC_NAMES } from "popup/constants/metricsNames"; import { ROUTES } from "popup/constants/routes"; import { PopupWrapper } from "popup/basics/PopupWrapper"; import { Button } from "popup/basics/buttons/Button"; @@ -224,6 +226,7 @@ export const SendTo = ({ previous }: { previous: ROUTES }) => {
  • From dcb5f15674ad3819895fc481c1f406d06e3b91ac Mon Sep 17 00:00:00 2001 From: Alec Charbonneau Date: Thu, 9 Jun 2022 10:43:04 -0700 Subject: [PATCH 08/22] merging 2.3.0 into 2.2.0 - swaps, removing network from onboarding screens (#523) * add swap routes, and make SendAmount changes (#515) * first cut * add swap icon * make SendAmount changes * cleanup * cleanup * finish sendAmount styling * cleanup * use consts * remove important * add swap support pt2 (#520) * first cut, with todos * finish swaps * add comment * edge case * create the TwoAssetCard * dont show warning if successful * remove network from onboarding screens (#522) --- extension/src/popup/Router.tsx | 21 ++ extension/src/popup/assets/icon-swap.svg | 3 + .../src/popup/components/BottomNav/index.tsx | 5 +- .../src/popup/components/Header/index.tsx | 32 +- .../src/popup/components/Header/styles.scss | 27 -- .../sendPayment/SendAmount/index.tsx | 275 ++++++++++-------- .../SendConfirm/SubmitResult/index.tsx | 112 +++++-- .../SendConfirm/SubmitResult/styles.scss | 14 + .../SendConfirm/TransactionDetails/index.tsx | 192 ++++++++---- .../TransactionDetails/styles.scss | 59 ++++ .../SendSettings/Slippage/index.tsx | 6 +- .../SendSettings/TransactionFee/index.tsx | 7 +- .../sendPayment/SendSettings/index.tsx | 34 ++- .../popup/components/sendPayment/styles.scss | 42 ++- extension/src/popup/constants/metricsNames.ts | 7 + extension/src/popup/constants/routes.ts | 6 + extension/src/popup/helpers/useIsSwap.ts | 6 + extension/src/popup/metrics/views.ts | 6 + extension/src/popup/styles/global.scss | 4 + extension/src/popup/views/Account/index.tsx | 4 + .../src/popup/views/SendPayment/index.tsx | 44 +-- extension/src/popup/views/Swap/index.tsx | 58 ++++ 22 files changed, 666 insertions(+), 298 deletions(-) create mode 100644 extension/src/popup/assets/icon-swap.svg create mode 100644 extension/src/popup/helpers/useIsSwap.ts create mode 100644 extension/src/popup/views/Swap/index.tsx diff --git a/extension/src/popup/Router.tsx b/extension/src/popup/Router.tsx index 5d4f2584b6..c31c294a5d 100644 --- a/extension/src/popup/Router.tsx +++ b/extension/src/popup/Router.tsx @@ -54,6 +54,7 @@ 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 "popup/metrics/views"; import { DEV_SERVER } from "@shared/constants/services"; @@ -143,6 +144,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); @@ -281,6 +299,9 @@ export const Router = () => { + + + {DEV_SERVER && ( 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/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/sendPayment/SendAmount/index.tsx b/extension/src/popup/components/sendPayment/SendAmount/index.tsx index 9073ae40fe..e8993299e2 100644 --- a/extension/src/popup/components/sendPayment/SendAmount/index.tsx +++ b/extension/src/popup/components/sendPayment/SendAmount/index.tsx @@ -3,7 +3,10 @@ import { useDispatch, useSelector } from "react-redux"; import debounce from "lodash/debounce"; import { BigNumber } from "bignumber.js"; import { useFormik } from "formik"; +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, @@ -12,13 +15,13 @@ import { 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"; @@ -34,6 +37,7 @@ import { AccountDoesntExistWarning, shouldAccountDoesntExistWarning, } from "popup/components/sendPayment/SendTo"; +import { BottomNav } from "popup/components/BottomNav"; import "../styles.scss"; @@ -79,7 +83,13 @@ const ConversionRate = ({ // 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); @@ -93,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) => { @@ -127,8 +139,6 @@ export const SendAmount = ({ previous }: { previous: ROUTES }) => { calculateAvailBalance(asset), ); - const [loadingRate, setLoadingRate] = useState(false); - const handleContinue = (values: { amount: string; asset: string; @@ -139,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 }) => { @@ -157,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( @@ -198,6 +216,13 @@ export const SendAmount = ({ previous }: { previous: ROUTES }) => { dispatch, ]); + // for swaps we're loading the destinationAsset here + useEffect(() => { + if (isSwap && !destinationAsset) { + dispatch(saveDestinationAsset(StellarSdk.Asset.native().toString())); + } + }, [isSwap, dispatch, destinationAsset]); + const getAmountFontSize = () => { const length = formik.values.amount.length; if (length <= 9) { @@ -231,6 +256,7 @@ export const SendAmount = ({ previous }: { previous: ROUTES }) => { const DecideWarning = () => { // unfunded destination if ( + !isSwap && shouldAccountDoesntExistWarning( destinationBalances.isFunded || false, asset, @@ -257,127 +283,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 && ( - - )} - {destinationAsset && ( - <> - - + + formik.setFieldValue("amount", formatAmount(e.target.value)) } + autoFocus + autoComplete="off" /> - - )} -
    - -
    - -
    - +
    + {parsedSourceAsset.code} +
    + {showSourceAndDestAsset && formik.values.amount !== "0" && ( + + )} +
    + +
    +
    + {!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..e968d5bfcc 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..627367e06b 100644 --- a/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/styles.scss +++ b/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/styles.scss @@ -107,3 +107,62 @@ } } } + +.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); + + img { + width: 1.5rem; + height: 1.5rem; + margin-right: 0.5rem; + } + } + + &__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..5609d9f928 100644 --- a/extension/src/popup/components/sendPayment/SendSettings/index.tsx +++ b/extension/src/popup/components/sendPayment/SendSettings/index.tsx @@ -11,6 +11,7 @@ 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 { ROUTES } from "popup/constants/routes"; import { PopupWrapper } from "popup/basics/PopupWrapper"; import { SubviewHeader } from "popup/components/SubviewHeader"; @@ -24,12 +25,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 +47,15 @@ export const SendSettings = ({ previous }: { previous: ROUTES }) => { } }, [dispatch, recommendedFee, transactionFee]); + // dont show memo for regular sends to Muxed, or for swaps + const showMemo = !isSwap && !destination.startsWith("M"); + const showSlippage = isPathPayment || isSwap; + return (
    navigateTo(previous)} /> { className="SendSettings__nav-btn" onClick={() => { submitForm(); - navigateTo(ROUTES.sendPaymentSettingsFee); + navigateTo( + isSwap + ? ROUTES.swapSettingsFee + : ROUTES.sendPaymentSettingsFee, + ); }} > @@ -94,7 +110,7 @@ export const SendSettings = ({ previous }: { previous: ROUTES }) => {
    - {isPathPayment && ( + {showSlippage && (
    @@ -126,7 +142,11 @@ export const SendSettings = ({ previous }: { previous: ROUTES }) => { className="SendSettings__nav-btn" onClick={() => { submitForm(); - navigateTo(ROUTES.sendPaymentSettingsSlippage); + navigateTo( + isSwap + ? ROUTES.swapSettingsSlippage + : ROUTES.sendPaymentSettingsSlippage, + ); }} > @@ -135,7 +155,7 @@ export const SendSettings = ({ previous }: { previous: ROUTES }) => {
    )} - {!destination.startsWith("M") && ( + {showMemo && ( <>
    @@ -182,7 +202,7 @@ export const SendSettings = ({ previous }: { previous: ROUTES }) => { fullWidth type="submit" variant={Button.variant.tertiary} - onClick={() => navigateTo(ROUTES.sendPaymentConfirm)} + onClick={() => navigateTo(next)} > Review Send diff --git a/extension/src/popup/components/sendPayment/styles.scss b/extension/src/popup/components/sendPayment/styles.scss index 17e37ae28e..0b31b61926 100644 --- a/extension/src/popup/components/sendPayment/styles.scss +++ b/extension/src/popup/components/sendPayment/styles.scss @@ -17,11 +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; - height: calc(100% - 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; @@ -41,7 +67,6 @@ } &__form { - margin-top: 3.5rem; height: 100%; width: 100%; display: flex; @@ -62,6 +87,7 @@ } &__input-amount { + margin-top: 5rem; font-size: 2.5rem; line-height: 4.5rem; font-weight: var(--font-weight-light); @@ -75,6 +101,10 @@ width: 100%; transition: opacity 0.15s ease-in-out; + &__full-height { + margin-top: 2rem; + } + @media (hover: hover) { &:hover, &:focus { @@ -111,6 +141,10 @@ &__asset-select-container { margin-top: auto; } + + &__btn-continue { + margin-top: auto; + } } .SendTo { diff --git a/extension/src/popup/constants/metricsNames.ts b/extension/src/popup/constants/metricsNames.ts index 90618daf5c..ac89f5817c 100644 --- a/extension/src/popup/constants/metricsNames.ts +++ b/extension/src/popup/constants/metricsNames.ts @@ -41,6 +41,13 @@ 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", viewTrustlineError: "loaded screen: trustline error", diff --git a/extension/src/popup/constants/routes.ts b/extension/src/popup/constants/routes.ts index b40534e082..59c535bc3d 100644 --- a/extension/src/popup/constants/routes.ts +++ b/extension/src/popup/constants/routes.ts @@ -13,6 +13,12 @@ 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", 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..2453872bbc 100644 --- a/extension/src/popup/metrics/views.ts +++ b/extension/src/popup/metrics/views.ts @@ -43,6 +43,12 @@ const routeToEventName = { [ROUTES.manageAssets]: METRIC_NAMES.viewManageAssets, [ROUTES.addAsset]: METRIC_NAMES.viewManageAssets, [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..5b23bbd7ae 100644 --- a/extension/src/popup/styles/global.scss +++ b/extension/src/popup/styles/global.scss @@ -45,6 +45,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/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/Swap/index.tsx b/extension/src/popup/views/Swap/index.tsx new file mode 100644 index 0000000000..c27238ab6b --- /dev/null +++ b/extension/src/popup/views/Swap/index.tsx @@ -0,0 +1,58 @@ +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, + 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 } = useSelector(transactionSubmissionSelector); + const publicKey = useSelector(publicKeySelector); + const networkDetails = useSelector(settingsNetworkDetailsSelector); + + useEffect(() => { + if (!accountBalances.balances) { + dispatch( + getAccountBalances({ + publicKey, + networkDetails, + }), + ); + } + }, [dispatch, publicKey, networkDetails, accountBalances]); + + return ( + + + + + + + + + + + + + + + + + + + + + ); +}; From 3a0a7b960394455b3d3524ebddc1e5e04ab68dc0 Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Fri, 10 Jun 2022 19:47:06 +0200 Subject: [PATCH 09/22] add a search asset view (#517) * add a search asset view * ux adjustments * move ResultsHeader outside component --- .../manageAssets/AddAsset/index.tsx | 2 +- .../manageAssets/ChooseAsset/index.tsx | 2 +- .../manageAssets/ManageAssetRows/index.tsx | 6 + .../manageAssets/SearchAsset/index.tsx | 205 ++++++++++++++++++ .../manageAssets/SearchAsset/styles.scss | 68 ++++++ extension/src/popup/constants/metricsNames.ts | 1 + extension/src/popup/constants/routes.ts | 1 + extension/src/popup/metrics/views.ts | 3 +- .../src/popup/views/ManageAssets/index.tsx | 4 + 9 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 extension/src/popup/components/manageAssets/SearchAsset/index.tsx create mode 100644 extension/src/popup/components/manageAssets/SearchAsset/styles.scss 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) => {
    - + diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx index 67e4a61df2..7394f05368 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx @@ -40,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, @@ -139,6 +143,7 @@ export const ManageAssetRows = ({ maxHeight: `${maxHeight}px`, }} > + {header}
    {assetRows.map(({ code, domain, image, issuer }) => { if (!balances) return null; @@ -179,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..bedab11708 --- /dev/null +++ b/extension/src/popup/components/manageAssets/SearchAsset/index.tsx @@ -0,0 +1,205 @@ +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..bf5bda576a --- /dev/null +++ b/extension/src/popup/components/manageAssets/SearchAsset/styles.scss @@ -0,0 +1,68 @@ +: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; + } + } + + &__overlay { + position: absolute; + height: var(--popup--height); + width: var(--popup--width); + display: flex; + flex-direction: column; + background: black; + z-index: calc(var(--back--button-z-index) + 1); + opacity: 0.8; + align-items: center; + justify-content: center; + top: 0; + left: 0; + } + + &__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/constants/metricsNames.ts b/extension/src/popup/constants/metricsNames.ts index ac89f5817c..41177ef9d0 100644 --- a/extension/src/popup/constants/metricsNames.ts +++ b/extension/src/popup/constants/metricsNames.ts @@ -50,6 +50,7 @@ export const METRIC_NAMES = { 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 59c535bc3d..f393569124 100644 --- a/extension/src/popup/constants/routes.ts +++ b/extension/src/popup/constants/routes.ts @@ -37,5 +37,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/metrics/views.ts b/extension/src/popup/metrics/views.ts index 2453872bbc..c4ff49c165 100644 --- a/extension/src/popup/metrics/views.ts +++ b/extension/src/popup/metrics/views.ts @@ -41,7 +41,8 @@ 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, diff --git a/extension/src/popup/views/ManageAssets/index.tsx b/extension/src/popup/views/ManageAssets/index.tsx index ade36195e4..f2b87546e5 100644 --- a/extension/src/popup/views/ManageAssets/index.tsx +++ b/extension/src/popup/views/ManageAssets/index.tsx @@ -5,6 +5,7 @@ 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"; @@ -50,6 +51,9 @@ export const ManageAssets = () => { selectingAssetType={selectingAssetType} /> + + + From c12e59d59a3bda4a4e7ba6d74b20535969e212f1 Mon Sep 17 00:00:00 2001 From: Alec Charbonneau Date: Fri, 10 Jun 2022 10:57:04 -0700 Subject: [PATCH 10/22] update the collectedTags array (#524) --- .../messageListener/freighterApiMessageListener.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extension/src/background/messageListener/freighterApiMessageListener.ts b/extension/src/background/messageListener/freighterApiMessageListener.ts index a6452d9750..4fe93b2633 100644 --- a/extension/src/background/messageListener/freighterApiMessageListener.ts +++ b/extension/src/background/messageListener/freighterApiMessageListener.ts @@ -115,7 +115,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 +124,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, ); } From c6cd00749bb145d25f32ef20dcacdc61c2d9039b Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Tue, 14 Jun 2022 16:52:16 +0100 Subject: [PATCH 11/22] adding default and loading icons (#525) * adding default and loading icons * PR comments --- extension/src/popup/assets/image-missing.svg | 3 ++ .../account/AccountAssets/index.tsx | 50 ++++++++++++------- .../account/AccountAssets/styles.scss | 49 ++++++++++++++++-- .../manageAssets/SearchAsset/index.tsx | 6 +-- .../manageAssets/SearchAsset/styles.scss | 15 ------ .../SendAmount/AssetSelect/styles.scss | 14 +++--- .../sendPayment/SendAmount/index.tsx | 2 +- .../SendConfirm/SubmitResult/styles.scss | 24 +++++++-- .../TransactionDetails/styles.scss | 13 +++-- 9 files changed, 117 insertions(+), 59 deletions(-) create mode 100644 extension/src/popup/assets/image-missing.svg 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/components/account/AccountAssets/index.tsx b/extension/src/popup/components/account/AccountAssets/index.tsx index ff0b5fb383..30db55d2e2 100644 --- a/extension/src/popup/components/account/AccountAssets/index.tsx +++ b/extension/src/popup/components/account/AccountAssets/index.tsx @@ -9,6 +9,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 +23,39 @@ 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 }); - } - }} - /> +}) => { + const isXlm = code === "XLM"; + const [hasError, setHasError] = useState(false); + const [isLoading, setIsLoading] = useState(!isXlm); + const canonicalAsset = assetIcons[getCanonicalFromAsset(code, issuerKey)]; + const imgSrc = hasError ? ImageMissingIcon : canonicalAsset || ""; + + return canonicalAsset || isXlm ? ( +
    + {`${code} { + if (retryAssetIconFetch) { + retryAssetIconFetch({ key: issuerKey, code }); + } + setHasError(true); + }} + onLoad={() => { + setIsLoading(false); + }} + /> +
    ) : ( -
    +
    + 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/manageAssets/SearchAsset/index.tsx b/extension/src/popup/components/manageAssets/SearchAsset/index.tsx index bedab11708..44b7c2489e 100644 --- a/extension/src/popup/components/manageAssets/SearchAsset/index.tsx +++ b/extension/src/popup/components/manageAssets/SearchAsset/index.tsx @@ -166,10 +166,8 @@ export const SearchAsset = ({ setErrorAsset }: SearchAssetProps) => { ref={ResultsRef} > {isSearching ? ( -
    -
    - -
    +
    +
    ) : null} diff --git a/extension/src/popup/components/manageAssets/SearchAsset/styles.scss b/extension/src/popup/components/manageAssets/SearchAsset/styles.scss index bf5bda576a..fea0e654d7 100644 --- a/extension/src/popup/components/manageAssets/SearchAsset/styles.scss +++ b/extension/src/popup/components/manageAssets/SearchAsset/styles.scss @@ -23,21 +23,6 @@ } } - &__overlay { - position: absolute; - height: var(--popup--height); - width: var(--popup--width); - display: flex; - flex-direction: column; - background: black; - z-index: calc(var(--back--button-z-index) + 1); - opacity: 0.8; - align-items: center; - justify-content: center; - top: 0; - left: 0; - } - &__loader { position: absolute; top: calc(50% - var(--SearchAsset--loader--dimension)); diff --git a/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/styles.scss b/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/styles.scss index b54e79d9c3..18b9d0b201 100644 --- a/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/styles.scss +++ b/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/styles.scss @@ -39,11 +39,6 @@ &__left { display: flex; align-items: center; - img { - height: 1.5rem; - width: 1.5rem; - margin: 0 0.5rem 0 0.75rem; - } svg { width: 1rem; @@ -51,10 +46,15 @@ margin: 0 0.5rem; } - .AccountAssets__asset--bullet { + .AccountAssets__asset--logo { + margin: 0 0.5rem 0 0.75rem; width: 1.5rem; height: 1.5rem; - margin: 0 0.5rem 0 0.75rem; + } + + .AccountAssets__asset--error img { + width: 0.75rem; + height: 0.75rem; } } diff --git a/extension/src/popup/components/sendPayment/SendAmount/index.tsx b/extension/src/popup/components/sendPayment/SendAmount/index.tsx index e8993299e2..dbde37c2e2 100644 --- a/extension/src/popup/components/sendPayment/SendAmount/index.tsx +++ b/extension/src/popup/components/sendPayment/SendAmount/index.tsx @@ -372,7 +372,7 @@ export const SendAmount = ({ + /> )} {showSourceAndDestAsset && ( <> diff --git a/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/styles.scss b/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/styles.scss index cc4f3768d6..c00ee53651 100644 --- a/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/styles.scss +++ b/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/styles.scss @@ -47,14 +47,30 @@ border-radius: 100px; padding: 0.25rem 0.5rem; - img { - width: 1rem; - height: 1rem; + %identiconImg { border-radius: 1rem; - background: var(--pal-brand-primary-on); + width: 0.875rem; + height: 0.875rem; margin-right: 0.5rem; } + .IdenticonImg { + @extend %identiconImg; + background: var(--pal-brand-primary-on); + } + + .AccountAssets__asset--logo { + @extend %identiconImg; + display: flex; + } + + .AccountAssets__asset--error { + img { + width: 0.4375rem; + height: 0.4375rem; + } + } + span { color: var(--pal-text-primary); font-size: 0.875rem; diff --git a/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/styles.scss b/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/styles.scss index 627367e06b..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 { @@ -132,12 +137,6 @@ align-items: center; font-weight: var(--font-weight-medium); color: var(--pal-text-primary); - - img { - width: 1.5rem; - height: 1.5rem; - margin-right: 0.5rem; - } } &__right { From 6819df5c84531616d4debc5a89610f03c428413d Mon Sep 17 00:00:00 2001 From: Alec Charbonneau Date: Tue, 14 Jun 2022 09:30:06 -0700 Subject: [PATCH 12/22] make sendSettings rows clickable, add pin extension view (#527) * sendsettings * add pin extension screen * cleanup * Update extension/src/popup/views/FullscreenSuccessMessage/index.tsx Co-authored-by: Piyal Basu * detect chrome, use old image/copy if not * update copy Co-authored-by: Piyal Basu --- extension/src/popup/Router.tsx | 4 + .../src/popup/assets/extensions-menu.png | Bin 0 -> 28879 bytes extension/src/popup/assets/extensions-pin.png | Bin 0 -> 9781 bytes .../sendPayment/SendSettings/index.tsx | 68 +++++++----- .../popup/components/sendPayment/styles.scss | 2 +- extension/src/popup/constants/metricsNames.ts | 1 + extension/src/popup/constants/routes.ts | 1 + extension/src/popup/metrics/views.ts | 1 + .../views/FullscreenSuccessMessage/index.tsx | 103 +++++++++++------- .../src/popup/views/PinExtension/index.tsx | 34 ++++++ .../src/popup/views/PinExtension/styles.scss | 30 +++++ 11 files changed, 174 insertions(+), 70 deletions(-) create mode 100644 extension/src/popup/assets/extensions-menu.png create mode 100644 extension/src/popup/assets/extensions-pin.png create mode 100644 extension/src/popup/views/PinExtension/index.tsx create mode 100644 extension/src/popup/views/PinExtension/styles.scss diff --git a/extension/src/popup/Router.tsx b/extension/src/popup/Router.tsx index c31c294a5d..c344dff6ea 100644 --- a/extension/src/popup/Router.tsx +++ b/extension/src/popup/Router.tsx @@ -55,6 +55,7 @@ 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"; @@ -281,6 +282,9 @@ export const Router = () => { + + + diff --git a/extension/src/popup/assets/extensions-menu.png b/extension/src/popup/assets/extensions-menu.png new file mode 100644 index 0000000000000000000000000000000000000000..9589b0dff31216774d0573fe1476c2960315610d GIT binary patch literal 28879 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 DJYn%M literal 0 HcmV?d00001 diff --git a/extension/src/popup/components/sendPayment/SendSettings/index.tsx b/extension/src/popup/components/sendPayment/SendSettings/index.tsx index 5609d9f928..c4cdc2780a 100644 --- a/extension/src/popup/components/sendPayment/SendSettings/index.tsx +++ b/extension/src/popup/components/sendPayment/SendSettings/index.tsx @@ -47,6 +47,14 @@ export const SendSettings = ({ } }, [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 && !destination.startsWith("M"); const showSlippage = isPathPayment || isSwap; @@ -69,7 +77,13 @@ export const SendSettings = ({
    - + { + submitForm(); + handleTxFeeNav(); + }} + > Transaction fee
    -
    +
    { + submitForm(); + handleTxFeeNav(); + }} + > {transactionFee} XLM
    -
    { - submitForm(); - navigateTo( - isSwap - ? ROUTES.swapSettingsFee - : ROUTES.sendPaymentSettingsFee, - ); - }} - > - -
    +
    {showSlippage && (
    - + { + submitForm(); + handleSlippageNav(); + }} + > Allowed slippage
    -
    +
    { + submitForm(); + handleSlippageNav(); + }} + > {allowedSlippage}%
    -
    { - submitForm(); - navigateTo( - isSwap - ? ROUTES.swapSettingsSlippage - : ROUTES.sendPaymentSettingsSlippage, - ); - }} - > - -
    +
    diff --git a/extension/src/popup/components/sendPayment/styles.scss b/extension/src/popup/components/sendPayment/styles.scss index 0b31b61926..192b647e91 100644 --- a/extension/src/popup/components/sendPayment/styles.scss +++ b/extension/src/popup/components/sendPayment/styles.scss @@ -246,7 +246,7 @@ } } - &__nav-btn { + &__clickable { cursor: pointer; } } diff --git a/extension/src/popup/constants/metricsNames.ts b/extension/src/popup/constants/metricsNames.ts index 41177ef9d0..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", diff --git a/extension/src/popup/constants/routes.ts b/extension/src/popup/constants/routes.ts index f393569124..0c71903d78 100644 --- a/extension/src/popup/constants/routes.ts +++ b/extension/src/popup/constants/routes.ts @@ -25,6 +25,7 @@ export enum ROUTES { mnemonicPhrase = "/mnemonic-phrase", mnemonicPhraseConfirm = "/mnemonic-phrase/confirm", mnemonicPhraseConfirmed = "/mnemonic-phrase-confirmed", + pinExtension = "/pin-extension", unlockAccount = "/unlock-account", verifyAccount = "/verify-account", accountCreator = "/account-creator", diff --git a/extension/src/popup/metrics/views.ts b/extension/src/popup/metrics/views.ts index c4ff49c165..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, diff --git a/extension/src/popup/views/FullscreenSuccessMessage/index.tsx b/extension/src/popup/views/FullscreenSuccessMessage/index.tsx index 4c6c49153d..653e9dc66e 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/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%; + } + } +} From eb62cfdccc7d2ec1077bc67ccf51c51136a98814 Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Wed, 15 Jun 2022 15:25:28 -0400 Subject: [PATCH 13/22] Feature/request public key (#528) * add first pass of request public key feature * handle account switching * handle password verification * fix selecting different account * better naming * simplify autoselect logic * fix comment * use map for allAccounts * update freighter-api version * PR comments * update freighter-api ver number --- @shared/api/external.ts | 2 + @shared/api/types.ts | 1 + @stellar/freighter-api/package.json | 2 +- @stellar/freighter-api/src/signTransaction.ts | 5 +- docs/docs/guide/usingFreighterNode.md | 4 +- .../components/SignTransactionDemo.tsx | 30 ++- docs/docs/playground/signTransaction.mdx | 2 +- docs/package.json | 2 +- .../freighterApiMessageListener.ts | 2 + extension/src/helpers/stellar.ts | 2 + extension/src/popup/Router.tsx | 4 +- .../src/popup/basics/InfoBlock/index.tsx | 2 +- .../src/popup/basics/InfoBlock/styles.scss | 1 + .../popup/basics/LoadingBackground/index.tsx | 20 ++ .../basics/LoadingBackground/styles.scss | 17 ++ .../popup/basics/TransactionHeading/index.tsx | 11 + .../basics/TransactionHeading/styles.scss | 9 + .../components/PunycodedDomain/index.tsx | 17 +- .../components/PunycodedDomain/styles.scss | 19 +- .../account/AccountHeader/index.tsx | 47 ++-- .../account/AccountHeader/styles.scss | 59 +---- .../components/account/AccountList/index.tsx | 74 +++++++ .../account/AccountList/styles.scss | 37 ++++ .../signTransaction/Operations/styles.scss | 1 + .../signTransaction/Transaction/index.tsx | 6 +- .../index.tsx | 29 +-- .../styles.scss | 2 +- extension/src/popup/styles/global.scss | 2 + .../src/popup/views/SignTransaction/index.tsx | 203 +++++++++++++++--- .../popup/views/SignTransaction/styles.scss | 55 +++++ .../src/popup/views/VerifyAccount/index.tsx | 30 ++- extension/src/types/transactions.ts | 3 +- 32 files changed, 527 insertions(+), 173 deletions(-) create mode 100644 extension/src/popup/basics/LoadingBackground/index.tsx create mode 100644 extension/src/popup/basics/LoadingBackground/styles.scss create mode 100644 extension/src/popup/basics/TransactionHeading/index.tsx create mode 100644 extension/src/popup/basics/TransactionHeading/styles.scss create mode 100644 extension/src/popup/components/account/AccountList/index.tsx create mode 100644 extension/src/popup/components/account/AccountList/styles.scss rename extension/src/popup/components/signTransaction/{TransactionHeader => TransactionInfo}/index.tsx (68%) rename extension/src/popup/components/signTransaction/{TransactionHeader => TransactionInfo}/styles.scss (91%) diff --git a/@shared/api/external.ts b/@shared/api/external.ts index 664aa82cfc..06cac6d611 100644 --- a/@shared/api/external.ts +++ b/@shared/api/external.ts @@ -23,6 +23,7 @@ export const requestPublicKey = async (): Promise => { export const submitTransaction = async ( transactionXdr: string, network?: string, + 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..0c707fac66 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", + accountToSign?: string +) => submitTransaction(transactionXdr, network, accountToSign); diff --git a/docs/docs/guide/usingFreighterNode.md b/docs/docs/guide/usingFreighterNode.md index aa08174738..6e5866dc14 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. @@ -129,6 +129,8 @@ The second parameter is an optional string that you may pass to indicate what ne 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, diff --git a/docs/docs/playground/components/SignTransactionDemo.tsx b/docs/docs/playground/components/SignTransactionDemo.tsx index 5badfe539d..31f53d71c1 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 : undefined, + 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..6531e28044 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", 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/src/background/messageListener/freighterApiMessageListener.ts b/extension/src/background/messageListener/freighterApiMessageListener.ts index 4fe93b2633..63f4790ba4 100644 --- a/extension/src/background/messageListener/freighterApiMessageListener.ts +++ b/extension/src/background/messageListener/freighterApiMessageListener.ts @@ -82,6 +82,7 @@ export const freighterApiMessageListener = ( const { transactionXdr, network = MAINNET_NETWORK_DETAILS.network, + accountToSign, } = request; const isTestnet = getIsTestnet(); const { networkUrl } = getNetworkDetails(isTestnet); @@ -157,6 +158,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 85f56dc02e..5c8f46e626 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, diff --git a/extension/src/popup/Router.tsx b/extension/src/popup/Router.tsx index c344dff6ea..b58a74e6b0 100644 --- a/extension/src/popup/Router.tsx +++ b/extension/src/popup/Router.tsx @@ -252,9 +252,9 @@ export const Router = () => { - + - + 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/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/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/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/styles/global.scss b/extension/src/popup/styles/global.scss index 5b23bbd7ae..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 { diff --git a/extension/src/popup/views/SignTransaction/index.tsx b/extension/src/popup/views/SignTransaction/index.tsx index 9e507263f1..8ccac87ea0 100644 --- a/extension/src/popup/views/SignTransaction/index.tsx +++ b/extension/src/popup/views/SignTransaction/index.tsx @@ -1,14 +1,25 @@ -import React, { useEffect, useState } from "react"; +import React, { 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 { TRANSACTION_WARNING } from "constants/transaction"; import { emitMetric } from "helpers/metrics"; -import { getTransactionInfo } from "helpers/stellar"; +import { getTransactionInfo, 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 +30,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, transaction, domain, - domainTitle, isDomainListedAllowed, flaggedKeys, } = getTransactionInfo(location.search); @@ -47,11 +65,17 @@ export const SignTransaction = () => { _networkPassphrase, _sequence, } = transaction; + const isFeeBump = !!_innerTransaction; - const source = isFeeBump ? _innerTransaction._source : transaction._source; const memo = decodeMemo(_memo); 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 +83,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 +118,17 @@ 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 }); + useEffect(() => { if (isMemoRequired) { emitMetric(METRIC_NAMES.signTransactionMemoRequired); @@ -87,11 +141,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 +190,14 @@ export const SignTransaction = () => { ); } - return ( - <> + return isPasswordRequired ? ( + setIsPasswordRequired(false)} + customSubmit={verifyPasswordThenSign} + /> + ) : ( +
    Confirm Transaction @@ -124,20 +212,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 +269,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..5e7e0dc502 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-left: 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/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; } From e6eec09106ad21f00a934ad64f02d083ac3ce6de Mon Sep 17 00:00:00 2001 From: Alec Charbonneau Date: Thu, 16 Jun 2022 14:55:55 -0700 Subject: [PATCH 14/22] fix sendAmount header moving (#532) --- extension/src/popup/components/sendPayment/styles.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extension/src/popup/components/sendPayment/styles.scss b/extension/src/popup/components/sendPayment/styles.scss index 192b647e91..65f9f2d240 100644 --- a/extension/src/popup/components/sendPayment/styles.scss +++ b/extension/src/popup/components/sendPayment/styles.scss @@ -56,6 +56,9 @@ display: flex; padding: 0; width: 0.9rem; + height: 1rem; + display: flex; + align-items: center; z-index: var(--back--button-z-index); } From 84a42bf3b0d20d2520c1c925f413b315a51b987f Mon Sep 17 00:00:00 2001 From: Alec Charbonneau Date: Wed, 22 Jun 2022 11:06:30 -0700 Subject: [PATCH 15/22] qa fixes (#533) --- .../components/sendPayment/SendConfirm/SubmitResult/index.tsx | 1 + .../src/popup/components/sendPayment/SendSettings/index.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx b/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx index e968d5bfcc..f9cade713e 100644 --- a/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx +++ b/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx @@ -142,6 +142,7 @@ export const SubmitFail = () => { const decideError = () => { // unfunded destination if ( + !isSwap && shouldAccountDoesntExistWarning( destinationBalances.isFunded || false, asset, diff --git a/extension/src/popup/components/sendPayment/SendSettings/index.tsx b/extension/src/popup/components/sendPayment/SendSettings/index.tsx index c4cdc2780a..c5976f6f71 100644 --- a/extension/src/popup/components/sendPayment/SendSettings/index.tsx +++ b/extension/src/popup/components/sendPayment/SendSettings/index.tsx @@ -212,7 +212,7 @@ export const SendSettings = ({ variant={Button.variant.tertiary} onClick={() => navigateTo(next)} > - Review Send + Review {isSwap ? "Swap" : "Send"}
    From a9f1fba5662e993c17c5378eb103685da1906fd9 Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Thu, 23 Jun 2022 14:29:00 -0400 Subject: [PATCH 16/22] show the loader while we're waiting for asset icons to be fetched; add comments (#534) --- .../account/AccountAssets/index.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/extension/src/popup/components/account/AccountAssets/index.tsx b/extension/src/popup/components/account/AccountAssets/index.tsx index 30db55d2e2..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"; @@ -24,12 +25,34 @@ export const AssetIcon = ({ issuerKey: string; retryAssetIconFetch?: (arg: { key: string; code: string }) => void; }) => { + /* + 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 ? (
    { + // 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
    From 6da9bf872daa68f2018cd5ab3805569015e9611d Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Thu, 23 Jun 2022 14:29:41 -0400 Subject: [PATCH 17/22] New styles for mnemonic phrase confirmed screen (#536) * new styles * additional text styling --- .../views/FullscreenSuccessMessage/index.tsx | 2 +- .../FullscreenSuccessMessage/styles.scss | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/extension/src/popup/views/FullscreenSuccessMessage/index.tsx b/extension/src/popup/views/FullscreenSuccessMessage/index.tsx index 653e9dc66e..53e6fb5e95 100644 --- a/extension/src/popup/views/FullscreenSuccessMessage/index.tsx +++ b/extension/src/popup/views/FullscreenSuccessMessage/index.tsx @@ -29,7 +29,7 @@ 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 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 { From 0b475eeb0790954e98b928a3f53cadf60b2c0c5e Mon Sep 17 00:00:00 2001 From: Alec Charbonneau Date: Fri, 24 Jun 2022 10:14:00 -0700 Subject: [PATCH 18/22] load assetIcons from swap screen, increase padding in accounst select (#535) * load asseticons from swap screen * increase account slide up padding * cleanup --- .../src/popup/views/SignTransaction/styles.scss | 2 +- extension/src/popup/views/Swap/index.tsx | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/views/SignTransaction/styles.scss b/extension/src/popup/views/SignTransaction/styles.scss index 5e7e0dc502..a9ac3d3b11 100644 --- a/extension/src/popup/views/SignTransaction/styles.scss +++ b/extension/src/popup/views/SignTransaction/styles.scss @@ -53,7 +53,7 @@ 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-left: 1.5rem; + padding: 0.5rem 0 0.5rem 1.5rem; position: absolute; transition: all var(--dropdown-animation); width: var(--popup--width); diff --git a/extension/src/popup/views/Swap/index.tsx b/extension/src/popup/views/Swap/index.tsx index c27238ab6b..d4a4a5f6d5 100644 --- a/extension/src/popup/views/Swap/index.tsx +++ b/extension/src/popup/views/Swap/index.tsx @@ -11,6 +11,7 @@ import { SendSettingsSlippage } from "popup/components/sendPayment/SendSettings/ import { SendConfirm } from "popup/components/sendPayment/SendConfirm"; import { getAccountBalances, + getAssetIcons, transactionSubmissionSelector, } from "popup/ducks/transactionSubmission"; import { publicKeySelector } from "popup/ducks/accountServices"; @@ -18,10 +19,13 @@ import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; export const Swap = () => { const dispatch = useDispatch(); - const { accountBalances } = useSelector(transactionSubmissionSelector); + 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( @@ -33,6 +37,15 @@ export const Swap = () => { } }, [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 ( From b7fa2f38dc0ac3e83927360a936ba44bb461cc1e Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Fri, 24 Jun 2022 20:19:38 -0400 Subject: [PATCH 19/22] adjust border radius for Path/Swap asset selects (#539) * adjust border radius for Path/Swap asset selects * better naming * better better naming --- .../sendPayment/SendAmount/AssetSelect/index.tsx | 5 ++++- .../sendPayment/SendAmount/AssetSelect/styles.scss | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx b/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx index 088eabd438..1340eb8966 100644 --- a/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx +++ b/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx @@ -85,7 +85,10 @@ export function PathPayAssetSelect({ }; return ( -
        +
        diff --git a/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/styles.scss b/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/styles.scss index 18b9d0b201..2c2a7f4f2c 100644 --- a/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/styles.scss +++ b/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/styles.scss @@ -10,6 +10,18 @@ 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 { From a752c2854806101e39dd845f1d2ffe0c01672246 Mon Sep 17 00:00:00 2001 From: Alec Charbonneau Date: Mon, 27 Jun 2022 09:30:06 -0700 Subject: [PATCH 20/22] use first non-xlm asset as default dest asset for swaps (#540) * use non xlm for default swap dest asset * use StellarSdk --- .../components/sendPayment/SendAmount/index.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/components/sendPayment/SendAmount/index.tsx b/extension/src/popup/components/sendPayment/SendAmount/index.tsx index dbde37c2e2..4aa9d1f729 100644 --- a/extension/src/popup/components/sendPayment/SendAmount/index.tsx +++ b/extension/src/popup/components/sendPayment/SendAmount/index.tsx @@ -219,9 +219,19 @@ export const SendAmount = ({ // for swaps we're loading the destinationAsset here useEffect(() => { if (isSwap && !destinationAsset) { - dispatch(saveDestinationAsset(StellarSdk.Asset.native().toString())); + // 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]); + }, [isSwap, dispatch, destinationAsset, accountBalances]); const getAmountFontSize = () => { const length = formik.values.amount.length; From e04a74984924d67b185ad3ac57349ef1eaa3199f Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Tue, 28 Jun 2022 13:46:17 -0400 Subject: [PATCH 21/22] Bugfix/allow null network (#545) * allow null since there is a 3rd possible param * allow `null` as network param --- @shared/api/external.ts | 2 +- @stellar/freighter-api/src/signTransaction.ts | 2 +- docs/docs/guide/usingFreighterBrowser.md | 26 +++++++++++++++---- docs/docs/guide/usingFreighterNode.md | 18 +++++++++---- .../components/SignTransactionDemo.tsx | 2 +- docs/docs/playground/signTransaction.mdx | 2 +- .../freighterApiMessageListener.ts | 7 ++--- 7 files changed, 40 insertions(+), 19 deletions(-) diff --git a/@shared/api/external.ts b/@shared/api/external.ts index 06cac6d611..b7eeade9d3 100644 --- a/@shared/api/external.ts +++ b/@shared/api/external.ts @@ -22,7 +22,7 @@ export const requestPublicKey = async (): Promise => { export const submitTransaction = async ( transactionXdr: string, - network?: string, + network?: string | null, accountToSign?: string, ): Promise => { let response = { signedTransaction: "", error: "" }; diff --git a/@stellar/freighter-api/src/signTransaction.ts b/@stellar/freighter-api/src/signTransaction.ts index 0c707fac66..655bd3f783 100644 --- a/@stellar/freighter-api/src/signTransaction.ts +++ b/@stellar/freighter-api/src/signTransaction.ts @@ -2,6 +2,6 @@ import { submitTransaction } from "@shared/api/external"; export const signTransaction = ( transactionXdr: string, - network?: "PUBLIC" | "TESTNET", + 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 6e5866dc14..1076cb6047 100644 --- a/docs/docs/guide/usingFreighterNode.md +++ b/docs/docs/guide/usingFreighterNode.md @@ -125,7 +125,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. @@ -161,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; } @@ -187,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 31f53d71c1..039b3afe87 100644 --- a/docs/docs/playground/components/SignTransactionDemo.tsx +++ b/docs/docs/playground/components/SignTransactionDemo.tsx @@ -25,7 +25,7 @@ export const SignTransactionDemo = () => { try { signedTransaction = await signTransaction( transactionXdr, - network === "PUBLIC" || network === "TESTNET" ? network : undefined, + network === "PUBLIC" || network === "TESTNET" ? network : null, publicKey ); } catch (e) { diff --git a/docs/docs/playground/signTransaction.mdx b/docs/docs/playground/signTransaction.mdx index 6531e28044..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, network?: "PUBLIC"|"TESTNET", publicKey?: string )` +#### `signTransaction( transactionXdr: string, network?: "PUBLIC"|"TESTNET"|null, publicKey?: string )` import { SignTransactionDemo } from "./components/SignTransactionDemo"; diff --git a/extension/src/background/messageListener/freighterApiMessageListener.ts b/extension/src/background/messageListener/freighterApiMessageListener.ts index 63f4790ba4..cdc998d22b 100644 --- a/extension/src/background/messageListener/freighterApiMessageListener.ts +++ b/extension/src/background/messageListener/freighterApiMessageListener.ts @@ -79,11 +79,8 @@ export const freighterApiMessageListener = ( }; const submitTransaction = async () => { - const { - transactionXdr, - network = MAINNET_NETWORK_DETAILS.network, - accountToSign, - } = 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( From 6a05f4059303251099920f4285ebf99b295f77a1 Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Tue, 28 Jun 2022 16:53:03 -0400 Subject: [PATCH 22/22] Bugfix/allow muxed federation in sign transaction (#546) * allow null since there is a 3rd possible param * allow `null` as network param * allow muxed and federation accounts in signTransation; standardize checks for muxed/federation --- extension/src/helpers/stellar.ts | 4 ++ .../SendConfirm/TransactionDetails/index.tsx | 3 +- .../sendPayment/SendSettings/index.tsx | 3 +- .../components/sendPayment/SendTo/index.tsx | 12 +++--- .../src/popup/views/SignTransaction/index.tsx | 38 +++++++++++++++++-- 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/extension/src/helpers/stellar.ts b/extension/src/helpers/stellar.ts index 5c8f46e626..f722503a94 100644 --- a/extension/src/helpers/stellar.ts +++ b/extension/src/helpers/stellar.ts @@ -100,3 +100,7 @@ export const formatDomain = (domain: string) => { } return "Stellar Network"; }; + +export const isMuxedAccount = (publicKey: string) => publicKey.startsWith("M"); + +export const isFederationAddress = (address: string) => address.includes("*"); diff --git a/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/index.tsx b/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/index.tsx index 03d35a10f9..9bbd921f85 100644 --- a/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/index.tsx +++ b/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/index.tsx @@ -9,6 +9,7 @@ import { Card, Loader, Icon } from "@stellar/design-system"; import { getAssetFromCanonical, getCanonicalFromAsset, + isMuxedAccount, xlmToStroop, getConversionRate, truncatedFedAddress, @@ -245,7 +246,7 @@ export const TransactionDetails = ({ goBack }: { goBack: () => void }) => { } }; - const showMemo = !isSwap && !destination.startsWith("M"); + const showMemo = !isSwap && !isMuxedAccount(destination); return (
        diff --git a/extension/src/popup/components/sendPayment/SendSettings/index.tsx b/extension/src/popup/components/sendPayment/SendSettings/index.tsx index c5976f6f71..75c460acfa 100644 --- a/extension/src/popup/components/sendPayment/SendSettings/index.tsx +++ b/extension/src/popup/components/sendPayment/SendSettings/index.tsx @@ -12,6 +12,7 @@ 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"; @@ -56,7 +57,7 @@ export const SendSettings = ({ ); // dont show memo for regular sends to Muxed, or for swaps - const showMemo = !isSwap && !destination.startsWith("M"); + const showMemo = !isSwap && !isMuxedAccount(destination); const showSlippage = isPathPayment || isSwap; return ( 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/views/SignTransaction/index.tsx b/extension/src/popup/views/SignTransaction/index.tsx index 8ccac87ea0..637f9cb49f 100644 --- a/extension/src/popup/views/SignTransaction/index.tsx +++ b/extension/src/popup/views/SignTransaction/index.tsx @@ -1,12 +1,18 @@ -import React, { useEffect, useRef, 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, truncatedPublicKey } 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"; @@ -52,7 +58,7 @@ export const SignTransaction = () => { const location = useLocation(); const dispatch: AppDispatch = useDispatch(); const { - accountToSign, + accountToSign: _accountToSign, transaction, domain, isDomainListedAllowed, @@ -68,6 +74,7 @@ export const SignTransaction = () => { const isFeeBump = !!_innerTransaction; const memo = decodeMemo(_memo); + let accountToSign = _accountToSign; const [isConfirming, setIsConfirming] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false); @@ -129,6 +136,31 @@ export const SignTransaction = () => { 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);