From 3ce61a10051e7ff89137c36158ed0b2862d60b0f Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 23 Nov 2020 12:56:04 +0100 Subject: [PATCH 01/23] Add '@stellarguard/stellar-uri' dependency --- package-lock.json | 16 ++++++++++++++++ package.json | 1 + 2 files changed, 17 insertions(+) diff --git a/package-lock.json b/package-lock.json index a1ed92408..11bd2e4d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3094,6 +3094,22 @@ "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", "dev": true }, + "@stellarguard/stellar-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@stellarguard/stellar-uri/-/stellar-uri-2.0.0.tgz", + "integrity": "sha512-IICEJ6xZKaKpL+cnFsqnS/X+r64ryapeepunyxltWwHoGUD2Y2fTCwY0xsNgM8cHtYPTGQUMp2LLIlTfunnlSw==", + "requires": { + "@stellarguard/txrep": "^2.0.0" + } + }, + "@stellarguard/txrep": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@stellarguard/txrep/-/txrep-2.0.0.tgz", + "integrity": "sha512-gVtlMnxSB4QUCM3r0c0SvlWCptPssnWxXSlepOlEK1y5/EAHNdYdectrUi4CcZHB1wpoJuY7x8atxhCFeCAS/Q==", + "requires": { + "bignumber.js": "^4.0.0" + } + }, "@storybook/addon-actions": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-5.2.0.tgz", diff --git a/package.json b/package.json index a30d1fe53..04dbe0cf5 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ } }, "dependencies": { + "@stellarguard/stellar-uri": "^2.0.0", "electron-context-menu": "^0.15.0", "electron-debug": "^3.0.1", "electron-is-dev": "^1.1.0", From 9dd12c445f11d1a8655dc691d74c066173fb83ff Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 23 Nov 2020 15:21:54 +0100 Subject: [PATCH 02/23] Save settings to localStorage in web build --- src/Platform/ipc/web.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Platform/ipc/web.ts b/src/Platform/ipc/web.ts index fef6cc8b8..8367daaaf 100644 --- a/src/Platform/ipc/web.ts +++ b/src/Platform/ipc/web.ts @@ -149,15 +149,18 @@ function initKeyStore() { callHandlers[Messages.SignTransaction] = signTransaction } +const defaultSettings: Platform.SettingsData = { + agreedToTermsAt: "2019-01-17T07:34:05.688Z", + biometricLock: false, + multisignature: true, + testnet: true, + trustedServices: [], + hideMemos: false +} + function initSettings() { - let settings: Platform.SettingsData = { - agreedToTermsAt: "2019-01-17T07:34:05.688Z", - biometricLock: false, - multisignature: true, - testnet: true, - trustedServices: [], - hideMemos: false - } + const storedSettings = localStorage.getItem("solar:settings") + let settings = storedSettings ? JSON.parse(storedSettings) : defaultSettings callHandlers[Messages.BioAuthAvailable] = () => ({ available: false, enrolled: false }) @@ -167,6 +170,8 @@ function initSettings() { ...settings, ...updatedSettings } + + localStorage.setItem("solar:settings", JSON.stringify(settings)) } callHandlers[Messages.ReadIgnoredSignatureRequestHashes] = () => { From 1116cbb04f71468006dd09ecb19b7342a33ae03b Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 25 Nov 2020 16:17:47 +0100 Subject: [PATCH 03/23] Add transactionRequest context --- i18n/locales/en/generic.json | 2 + src/App/bootstrap/context.tsx | 13 +++-- src/App/contexts/transactionRequest.tsx | 67 +++++++++++++++++++++++++ src/Transaction/lib/stellar-uri.ts | 21 ++++++++ 4 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 src/App/contexts/transactionRequest.tsx create mode 100644 src/Transaction/lib/stellar-uri.ts diff --git a/i18n/locales/en/generic.json b/i18n/locales/en/generic.json index e7e0c7100..764dbaee1 100644 --- a/i18n/locales/en/generic.json +++ b/i18n/locales/en/generic.json @@ -31,12 +31,14 @@ "request-failed-error": "Request to {{target}} failed with status {{status}}: {{message}}", "stellar-address-not-found-error": "Stellar address not found: {{address}}", "stellar-address-request-failed-error": "Stellar address resolution of {{address}} failed.", + "stellar-uri-verification-error": "Stellar URI's signature could not be verified.", "submission-failed-error": "Submitting transaction to {{endpoint}} failed with status {{status}}: {{message}}", "testnet-endpoint-not-available-error": "{{service}} does not provide a testnet endpoint.", "timeout-error": "Request timed out", "unexpected-action-error": "Unexpected action: {{action}}", "unexpected-state-error": "Encountered unexpected state: {{state}}", "unexpected-response-type-error": "Unexpected response type: {{type}} / ${dataType}", + "unexpected-stellar-uri-type-error": "Incoming uri {{incomingURI}} does not match any expected type.", "unknown-error": "An unknown error occured.", "update-already-running-error": "Update is already running!", "wrong-password-error": "Wrong password.", diff --git a/src/App/bootstrap/context.tsx b/src/App/bootstrap/context.tsx index 474acb9e8..37331b2f0 100644 --- a/src/App/bootstrap/context.tsx +++ b/src/App/bootstrap/context.tsx @@ -5,17 +5,20 @@ import { NotificationsProvider } from "../contexts/notifications" import { SettingsProvider } from "../contexts/settings" import { SignatureDelegationProvider } from "../contexts/signatureDelegation" import { StellarProvider } from "../contexts/stellar" +import { TransactionRequestProvider } from "../contexts/transactionRequest" export function ContextProviders(props: { children: React.ReactNode }) { return ( - - - {props.children} - - + + + + {props.children} + + + diff --git a/src/App/contexts/transactionRequest.tsx b/src/App/contexts/transactionRequest.tsx new file mode 100644 index 000000000..08d4ef08d --- /dev/null +++ b/src/App/contexts/transactionRequest.tsx @@ -0,0 +1,67 @@ +import React from "react" +import { StellarUri, StellarUriType } from "@stellarguard/stellar-uri" +import { CustomError } from "~Generic/lib/errors" +import { subscribeToDeepLinkURLs } from "~Platform/protocol-handler" +import { verifyTransactionRequest } from "~Transaction/lib/stellar-uri" +import { trackError } from "./notifications" + +const allowUnsafeTestnetURIs = Boolean(process.env.ALLOW_UNSAFE_TESTNET_URIS) + +interface Props { + children: React.ReactNode +} + +interface ContextType { + uri: StellarUri | null + clearURI: () => void +} + +const initialValues: ContextType = { + uri: null, + clearURI: () => undefined +} + +const TransactionRequestContext = React.createContext(initialValues) + +export function TransactionRequestProvider(props: Props) { + const [uri, setURI] = React.useState(null) + + const clearURI = React.useCallback(() => setURI(null), []) + + const verifyStellarURI = React.useCallback(async (incomingURI: string) => { + try { + const parsedURI = await verifyTransactionRequest(incomingURI, { allowUnsafeTestnetURIs }) + setURI(parsedURI) + } catch (error) { + trackError(error) + } + }, []) + + React.useEffect(() => { + const unsubscribe = subscribeToDeepLinkURLs(async incomingURI => { + const url = new URL(incomingURI) + switch (url.pathname) { + case StellarUriType.Transaction: + case StellarUriType.Pay: + verifyStellarURI(incomingURI) + break + default: + trackError( + CustomError( + "UnexpectedStellarUriTypeError", + `Incoming uri ${incomingURI} does not match any expected type.`, + { incomingURI } + ) + ) + break + } + }) + return unsubscribe + }, [verifyStellarURI]) + + return ( + {props.children} + ) +} + +export { ContextType as TransactionRequestContextType, TransactionRequestContext } diff --git a/src/Transaction/lib/stellar-uri.ts b/src/Transaction/lib/stellar-uri.ts new file mode 100644 index 000000000..355109e7f --- /dev/null +++ b/src/Transaction/lib/stellar-uri.ts @@ -0,0 +1,21 @@ +import { parseStellarUri } from "@stellarguard/stellar-uri" +import { CustomError } from "~Generic/lib/errors" + +export interface VerificationOptions { + allowUnsafeTestnetURIs?: boolean +} + +export async function verifyTransactionRequest(request: string, options: VerificationOptions = {}) { + const parsedURI = parseStellarUri(request) + const isSignatureValid = await parsedURI.verifySignature() + + if (!isSignatureValid) { + if (parsedURI.isTestNetwork && options.allowUnsafeTestnetURIs) { + // ignore + } else { + throw CustomError("StellarUriVerificationError", "Stellar URI's signature could not be verified.") + } + } + + return parsedURI +} From 9e31652540a1503163dc206c27bdf2c2ac6c860f Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 25 Nov 2020 16:19:51 +0100 Subject: [PATCH 04/23] Add TransactionRequestHandler --- shared/types/platform.d.ts | 2 +- src/App/bootstrap/app-stage2.tsx | 2 + .../components/TransactionRequestHandler.tsx | 69 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 src/Transaction/components/TransactionRequestHandler.tsx diff --git a/shared/types/platform.d.ts b/shared/types/platform.d.ts index d37c60846..857f6fe77 100644 --- a/shared/types/platform.d.ts +++ b/shared/types/platform.d.ts @@ -1,6 +1,6 @@ interface TrustedService { domain: string - signingKey: string + signingKey?: string } declare namespace Platform { diff --git a/src/App/bootstrap/app-stage2.tsx b/src/App/bootstrap/app-stage2.tsx index c362fce59..6a8d9457c 100644 --- a/src/App/bootstrap/app-stage2.tsx +++ b/src/App/bootstrap/app-stage2.tsx @@ -7,6 +7,7 @@ import { VerticalLayout } from "~Layout/components/Box" import { appIsLoaded } from "~SplashScreen/splash-screen" import ConnectionErrorListener from "~Toasts/components/ConnectionErrorListener" import NotificationContainer from "~Toasts/components/NotificationContainer" +import TransactionRequestHandler from "~Transaction/components/TransactionRequestHandler" import AllAccountsPage from "../components/AccountListView" import AndroidBackButton from "../components/AndroidBackButton" import DesktopNotifications from "../components/DesktopNotifications" @@ -72,6 +73,7 @@ function Stage2() { {/* Notifications need to come after the -webkit-overflow-scrolling element on iOS */} + {process.env.PLATFORM === "android" ? : null} {process.env.PLATFORM === "android" || process.env.PLATFORM === "ios" ? : null} diff --git a/src/Transaction/components/TransactionRequestHandler.tsx b/src/Transaction/components/TransactionRequestHandler.tsx new file mode 100644 index 000000000..7a6334ae1 --- /dev/null +++ b/src/Transaction/components/TransactionRequestHandler.tsx @@ -0,0 +1,69 @@ +import React from "react" +import Fade from "@material-ui/core/Fade" +import { TransitionProps } from "@material-ui/core/transitions/transition" +import { Dialog } from "@material-ui/core" +import { StellarUriType, StellarUri, PayStellarUri } from "@stellarguard/stellar-uri" +import { TransactionRequestContext } from "~App/contexts/transactionRequest" +import { SettingsContext } from "~App/contexts/settings" +import VerifyTrustedServiceDialog from "./VerifyTrustedServiceDialog" +import PaymentAccountSelectionDialog from "./PaymentAccountSelectionDialog" + +const Transition = React.forwardRef((props: TransitionProps, ref) => ) + +function TransactionRequestHandler() { + const { uri, clearURI } = React.useContext(TransactionRequestContext) + const { trustedServices, setSetting } = React.useContext(SettingsContext) + const [closedStellarURI, setClosedStellarURI] = React.useState(null) + + // We need that so we still know what to render when we fade out the dialog + const renderedURI = uri || closedStellarURI + + const closeDialog = React.useCallback(() => { + setClosedStellarURI(uri) + clearURI() + + // Clear location href, since it might contain secret search params + window.history.pushState({}, "Solar Wallet", window.location.href.replace(window.location.search, "")) + }, [clearURI, uri]) + + if (!renderedURI) { + return null + } + + const trustedService = trustedServices.find(service => renderedURI.originDomain === service.domain) + if (renderedURI.originDomain && !trustedService) { + const onTrust = () => { + const newTrustedServices: TrustedService[] = [ + ...trustedServices, + { domain: renderedURI.originDomain!, signingKey: renderedURI.pubkey } + ] + setSetting("trustedServices", newTrustedServices) + } + const onDeny = () => { + closeDialog() + } + + return ( + + + + ) + } + + if (renderedURI.operation === StellarUriType.Pay) { + const payStellarUri = renderedURI as PayStellarUri + const onDismiss = () => { + closeDialog() + } + + return ( + + + + ) + } + + return null +} + +export default React.memo(TransactionRequestHandler) From 3d83dc0427386b8978a0f9cb04e4afc681330a87 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 25 Nov 2020 16:25:25 +0100 Subject: [PATCH 05/23] Add VerifyTrustedServiceDialog --- i18n/locales/en/transaction-request.json | 14 +++++ .../components/VerifyTrustedServiceDialog.tsx | 52 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 i18n/locales/en/transaction-request.json create mode 100644 src/Transaction/components/VerifyTrustedServiceDialog.tsx diff --git a/i18n/locales/en/transaction-request.json b/i18n/locales/en/transaction-request.json new file mode 100644 index 000000000..1c7925351 --- /dev/null +++ b/i18n/locales/en/transaction-request.json @@ -0,0 +1,14 @@ +{ + "verify-trusted-service": { + "action": { + "trust": "Trust", + "cancel": "Cancel" + }, + "title": "Verify Trusted Service", + "info": { + "1": "You opened a Stellar URI originating from an untrusted service", + "2": "If you trust this domain you can add this service to your list of trusted services.", + "3": "You can view and edit your list of trusted services anytime in the application settings." + } + } +} diff --git a/src/Transaction/components/VerifyTrustedServiceDialog.tsx b/src/Transaction/components/VerifyTrustedServiceDialog.tsx new file mode 100644 index 000000000..a38623b1b --- /dev/null +++ b/src/Transaction/components/VerifyTrustedServiceDialog.tsx @@ -0,0 +1,52 @@ +import React from "react" +import { useTranslation } from "react-i18next" +import Box from "@material-ui/core/Box" +import Typography from "@material-ui/core/Typography" +import TrustIcon from "@material-ui/icons/Check" +import DenyIcon from "@material-ui/icons/Cancel" +import WarnIcon from "@material-ui/icons/Warning" +import { ActionButton, DialogActionsBox } from "~Generic/components/DialogActions" +import MainTitle from "~Generic/components/MainTitle" +import DialogBody from "~Layout/components/DialogBody" + +interface VerifyTrustedServiceDialogProps { + onTrust: () => void + onCancel: () => void + domain: string +} + +function VerifyTrustedServiceDialog(props: VerifyTrustedServiceDialogProps) { + const { t } = useTranslation() + + const { onTrust, onCancel } = props + + return ( + } + noMaxWidth + preventNotchSpacing + top={} + actions={ + + } onClick={onTrust} type="secondary"> + {t("transaction-request.verify-trusted-service.action.trust")} + + } onClick={onCancel} type="primary"> + {t("transaction-request.verify-trusted-service.action.cancel")} + + + } + > + + {t("transaction-request.verify-trusted-service.info.1")}: + + {props.domain} + + {t("transaction-request.verify-trusted-service.info.2")}: + {t("transaction-request.verify-trusted-service.info.3")}: + + + ) +} + +export default VerifyTrustedServiceDialog From fac8b0271a9a56be48dd391ab472903ca6e3c731 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 25 Nov 2020 16:51:11 +0100 Subject: [PATCH 06/23] Add PaymentAccountSelectionDialog --- i18n/locales/en/transaction-request.json | 20 ++ .../PaymentAccountSelectionDialog.tsx | 180 ++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 src/Transaction/components/PaymentAccountSelectionDialog.tsx diff --git a/i18n/locales/en/transaction-request.json b/i18n/locales/en/transaction-request.json index 1c7925351..5c934adfc 100644 --- a/i18n/locales/en/transaction-request.json +++ b/i18n/locales/en/transaction-request.json @@ -1,4 +1,24 @@ { + "payment-account-selection": { + "action": { + "dismiss": "Dismiss", + "select": "Select" + }, + "header": { + "origin-domain": "You opened the following payment request from <1>{{originDomain}} <3>", + "no-origin-domain": "You opened the following payment request from an unknown origin" + }, + "footer": "Please select the account that you want to use for this payment.", + "uri-content": { + "any": "Any", + "pay": "Pay", + "to": "To", + "memo": "Memo", + "message": "Message" + }, + "title": "Select Account for Payment", + "warning": "This request did not include a signature! Make sure that you trust the source of this link!" + }, "verify-trusted-service": { "action": { "trust": "Trust", diff --git a/src/Transaction/components/PaymentAccountSelectionDialog.tsx b/src/Transaction/components/PaymentAccountSelectionDialog.tsx new file mode 100644 index 000000000..8d53ea39f --- /dev/null +++ b/src/Transaction/components/PaymentAccountSelectionDialog.tsx @@ -0,0 +1,180 @@ +import React from "react" +import { Trans, useTranslation } from "react-i18next" +import { Asset } from "stellar-sdk" +import { PayStellarUri } from "@stellarguard/stellar-uri" +import Box from "@material-ui/core/Box" +import Grid from "@material-ui/core/Grid" +import makeStyles from "@material-ui/core/styles/makeStyles" +import Typography from "@material-ui/core/Typography" +import CancelIcon from "@material-ui/icons/Cancel" +import SelectIcon from "@material-ui/icons/Check" +import WarningIcon from "@material-ui/icons/Warning" +import { Account, AccountsContext } from "~App/contexts/accounts" +import { warningColor } from "~App/theme" +import AccountSelectionList from "~Account/components/AccountSelectionList" +import AssetLogo from "~Assets/components/AssetLogo" +import { ActionButton, DialogActionsBox } from "~Generic/components/DialogActions" +import { CopyableAddress } from "~Generic/components/PublicKey" +import MainTitle from "~Generic/components/MainTitle" +import { useIsMobile } from "~Generic/hooks/userinterface" +import DialogBody from "~Layout/components/DialogBody" + +const useStyles = makeStyles(theme => ({ + assetContainer: { + alignSelf: "center", + display: "flex", + margin: "0px 8px" + }, + assetLogo: { + width: 28, + height: 28, + margin: "0px 4px" + }, + root: { + display: "flex", + flexDirection: "column", + padding: "12px 0 0" + }, + keyTypography: { + alignSelf: "center", + textAlign: "right" + }, + valueTypography: { + textAlign: "left" + }, + uriContainer: { + paddingTop: 16, + paddingBottom: 16 + }, + warningContainer: { + alignItems: "center", + alignSelf: "center", + background: warningColor, + display: "flex", + justifyContent: "center", + padding: "6px 16px", + marginBottom: 16, + width: "fit-content" + } +})) + +interface PaymentAccountSelectionDialogProps { + payStellarUri: PayStellarUri + onDismiss: () => void +} + +function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps) { + const { onDismiss } = props + const { + amount, + assetCode, + assetIssuer, + destination, + memo, + msg, + originDomain, + signature, + isTestNetwork: testnet + } = props.payStellarUri + + const classes = useStyles() + const isSmallScreen = useIsMobile() + const { t } = useTranslation() + + const { accounts } = React.useContext(AccountsContext) + const [selectedAccount, setSelectedAccount] = React.useState(null) + + const asset = React.useMemo(() => (assetCode && assetIssuer ? new Asset(assetCode, assetIssuer) : Asset.native()), [ + assetCode, + assetIssuer + ]) + const keyItemXS = React.useMemo(() => (isSmallScreen ? 4 : 5), [isSmallScreen]) + const selectableAccounts = React.useMemo(() => accounts.filter(acc => acc.testnet === testnet), [accounts, testnet]) + + return ( + + } + actions={ + + } onClick={onDismiss} type="secondary"> + {t("transaction-request.payment-account-selection.action.dismiss")} + + } onClick={undefined} type="primary"> + {t("transaction-request.payment-account-selection.action.select")} + + + } + > + + {!signature && ( + + + {t("transaction-request.payment-account-selection.warning")} + + + )} + + {originDomain ? ( + + You opened the following payment request from {{ originDomain }}: + + ) : ( + t("transaction-request.payment-account-selection.header.no-origin-domain") + )} + + + + + {t("transaction-request.payment-account-selection.uri-content.pay")} + + + {amount ? amount : t("transaction-request.payment-account-selection.uri-content.any")} +
+ {asset.getCode()} + +
+
+
+ + + {t("transaction-request.payment-account-selection.uri-content.to")} + + + + + + {memo && ( + + + {t("transaction-request.payment-account-selection.uri-content.memo")} + + + {memo} + + + )} + {msg && ( + + + {t("transaction-request.payment-account-selection.uri-content.message")} + + + {msg} + + + )} +
+ + {t("transaction-request.payment-account-selection.footer")} + + +
+
+ ) +} + +export default PaymentAccountSelectionDialog From 09c84b12d8e65de1c6c3b7344385bc30d9d03dee Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 25 Nov 2020 16:51:56 +0100 Subject: [PATCH 07/23] Add transaction-request.json to i18n translations --- i18n/en.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/i18n/en.ts b/i18n/en.ts index d0a0e4a45..6357174ab 100644 --- a/i18n/en.ts +++ b/i18n/en.ts @@ -7,6 +7,7 @@ import Generic from "./locales/en/generic.json" import Operations from "./locales/en/operations.json" import Payment from "./locales/en/payment.json" import Trading from "./locales/en/trading.json" +import TransactionRequest from "./locales/en/transaction-request.json" import TransferService from "./locales/en/transfer-service.json" const translations = { @@ -19,6 +20,7 @@ const translations = { operations: Operations, payment: Payment, trading: Trading, + "transaction-request": TransactionRequest, "transfer-service": TransferService } as const From fcd49b7c226c3f7e6dfb7e4152f09e974edeb371 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 30 Nov 2020 11:48:13 +0100 Subject: [PATCH 08/23] Support filling in payment form from search params --- src/Payment/components/PaymentDialog.tsx | 37 ++++++++++++++++++++++-- src/Payment/components/PaymentForm.tsx | 34 ++++++++++++++++++++-- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/Payment/components/PaymentDialog.tsx b/src/Payment/components/PaymentDialog.tsx index d00ef629a..c16f2ca8c 100644 --- a/src/Payment/components/PaymentDialog.tsx +++ b/src/Payment/components/PaymentDialog.tsx @@ -1,12 +1,12 @@ import React from "react" import { useTranslation } from "react-i18next" -import { Asset, Server, Transaction } from "stellar-sdk" +import { Asset, MemoType, Server, Transaction } from "stellar-sdk" import { Account } from "~App/contexts/accounts" import { trackError } from "~App/contexts/notifications" import { useLiveAccountData, useLiveAccountOffers } from "~Generic/hooks/stellar-subscriptions" -import { useDialogActions } from "~Generic/hooks/userinterface" +import { useDialogActions, useRouter } from "~Generic/hooks/userinterface" import { AccountData } from "~Generic/lib/account" -import { getAssetsFromBalances } from "~Generic/lib/stellar" +import { getAssetsFromBalances, parseAssetID } from "~Generic/lib/stellar" import DialogBody from "~Layout/components/DialogBody" import TestnetBadge from "~Generic/components/TestnetBadge" import { Box } from "~Layout/components/Box" @@ -15,6 +15,14 @@ import MainTitle from "~Generic/components/MainTitle" import TransactionSender from "~Transaction/components/TransactionSender" import PaymentForm from "./PaymentForm" +export interface PaymentQueryParams { + amount: string | null + asset: Asset | null + destination: string | null + memo: string | null + memoType: MemoType | null +} + interface Props { account: Account accountData: AccountData @@ -30,6 +38,28 @@ function PaymentDialog(props: Props) { const { t } = useTranslation() const [txCreationPending, setTxCreationPending] = React.useState(false) + const router = useRouter() + + const query = React.useMemo(() => new URLSearchParams(router.location.search), [router.location.search]) + const [queryParams, setQueryParams] = React.useState({ + amount: null, + asset: null, + destination: null, + memo: null, + memoType: null + }) + + React.useEffect(() => { + const amount = query.get("amount") + const assetString = query.get("asset") + const asset = assetString ? parseAssetID(assetString) : null + const destination = query.get("destination") + const memo = query.get("memo") + const memoType = query.get("memoType") ? (query.get("memoType") as MemoType) : null + + setQueryParams({ amount, asset, destination, memo, memoType }) + }, [query]) + const handleSubmit = React.useCallback( async (createTx: (horizon: Server, account: Account) => Promise) => { try { @@ -75,6 +105,7 @@ function PaymentDialog(props: Props) { onCancel={props.onClose} onSubmit={handleSubmit} openOrdersCount={props.openOrdersCount} + preselectedParams={queryParams} testnet={props.account.testnet} trustedAssets={trustedAssets} txCreationPending={txCreationPending} diff --git a/src/Payment/components/PaymentForm.tsx b/src/Payment/components/PaymentForm.tsx index d9ca3fc23..be9f64db4 100644 --- a/src/Payment/components/PaymentForm.tsx +++ b/src/Payment/components/PaymentForm.tsx @@ -22,6 +22,7 @@ import { PriceInput, QRReader } from "~Generic/components/FormFields" import { formatBalance } from "~Generic/lib/balances" import { HorizontalLayout } from "~Layout/components/Box" import Portal from "~Generic/components/Portal" +import { PaymentQueryParams } from "./PaymentDialog" export interface PaymentFormValues { amount: string @@ -58,6 +59,7 @@ interface PaymentFormProps { wellknownAccount?: AccountRecord ) => void openOrdersCount: number + preselectedParams: PaymentQueryParams testnet: boolean trustedAssets: Asset[] txCreationPending?: boolean @@ -86,6 +88,7 @@ const PaymentForm = React.memo(function PaymentForm(props: PaymentFormProps) { }) const formValues = form.watch() + const { preselectedParams } = props const { setValue } = form const spendableBalance = getSpendableBalance( @@ -93,6 +96,13 @@ const PaymentForm = React.memo(function PaymentForm(props: PaymentFormProps) { findMatchingBalanceLine(props.accountData.balances, formValues.asset) ) + React.useEffect(() => { + if (preselectedParams.amount) setValue("amount", preselectedParams.amount) + if (preselectedParams.asset) setValue("asset", preselectedParams.asset) + if (preselectedParams.destination) setValue("destination", preselectedParams.destination) + if (preselectedParams.memo) setValue("memoValue", preselectedParams.memo) + }, [preselectedParams, setValue]) + React.useEffect(() => { if (!isPublicKey(formValues.destination) && !isStellarAddress(formValues.destination)) { if (matchingWellknownAccount) { @@ -104,7 +114,17 @@ const PaymentForm = React.memo(function PaymentForm(props: PaymentFormProps) { const knownAccount = wellknownAccounts.lookup(formValues.destination) setMatchingWellknownAccount(knownAccount) - if (knownAccount && knownAccount.tags.indexOf("exchange") !== -1) { + if (preselectedParams.memo && preselectedParams.memoType) { + setMemoType(preselectedParams.memoType) + setMemoMetadata({ + label: + preselectedParams.memoType === "id" + ? t("payment.memo-metadata.label.id") + : t("payment.memo-metadata.label.text"), + placeholder: t("payment.memo-metadata.placeholder.mandatory"), + requiredType: preselectedParams.memoType + }) + } else if (knownAccount && knownAccount.tags.indexOf("exchange") !== -1) { const acceptedMemoType = knownAccount.accepts && knownAccount.accepts.memo const requiredType = acceptedMemoType === "MEMO_ID" ? "id" : "text" setMemoType(requiredType) @@ -122,7 +142,16 @@ const PaymentForm = React.memo(function PaymentForm(props: PaymentFormProps) { requiredType: undefined }) } - }, [formValues.destination, formValues.memoValue, matchingWellknownAccount, memoType, t, wellknownAccounts]) + }, [ + formValues.destination, + formValues.memoValue, + matchingWellknownAccount, + memoType, + preselectedParams.memo, + preselectedParams.memoType, + t, + wellknownAccounts + ]) const handleFormSubmission = () => { props.onSubmit({ memoType, ...form.getValues() }, spendableBalance, matchingWellknownAccount) @@ -321,6 +350,7 @@ interface Props { accountData: AccountData actionsRef: RefStateObject openOrdersCount: number + preselectedParams: PaymentQueryParams testnet: boolean trustedAssets: Asset[] txCreationPending?: boolean From daf84796a4561d5695e6413cd6c9aaba4cb18634 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 30 Nov 2020 11:51:21 +0100 Subject: [PATCH 09/23] Go to payment form on payment account selection --- .../PaymentAccountSelectionDialog.tsx | 30 +++++++++++++++---- .../components/TransactionRequestHandler.tsx | 4 +-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/Transaction/components/PaymentAccountSelectionDialog.tsx b/src/Transaction/components/PaymentAccountSelectionDialog.tsx index 8d53ea39f..a35749212 100644 --- a/src/Transaction/components/PaymentAccountSelectionDialog.tsx +++ b/src/Transaction/components/PaymentAccountSelectionDialog.tsx @@ -10,13 +10,15 @@ import CancelIcon from "@material-ui/icons/Cancel" import SelectIcon from "@material-ui/icons/Check" import WarningIcon from "@material-ui/icons/Warning" import { Account, AccountsContext } from "~App/contexts/accounts" +import * as routes from "~App/routes" import { warningColor } from "~App/theme" import AccountSelectionList from "~Account/components/AccountSelectionList" import AssetLogo from "~Assets/components/AssetLogo" import { ActionButton, DialogActionsBox } from "~Generic/components/DialogActions" import { CopyableAddress } from "~Generic/components/PublicKey" import MainTitle from "~Generic/components/MainTitle" -import { useIsMobile } from "~Generic/hooks/userinterface" +import { stringifyAsset } from "~Generic/lib/stellar" +import { useIsMobile, useRouter } from "~Generic/hooks/userinterface" import DialogBody from "~Layout/components/DialogBody" const useStyles = makeStyles(theme => ({ @@ -60,17 +62,18 @@ const useStyles = makeStyles(theme => ({ interface PaymentAccountSelectionDialogProps { payStellarUri: PayStellarUri - onDismiss: () => void + onClose: () => void } function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps) { - const { onDismiss } = props + const { onClose } = props const { amount, assetCode, assetIssuer, destination, memo, + memoType, msg, originDomain, signature, @@ -79,6 +82,7 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps const classes = useStyles() const isSmallScreen = useIsMobile() + const router = useRouter() const { t } = useTranslation() const { accounts } = React.useContext(AccountsContext) @@ -91,19 +95,33 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps const keyItemXS = React.useMemo(() => (isSmallScreen ? 4 : 5), [isSmallScreen]) const selectableAccounts = React.useMemo(() => accounts.filter(acc => acc.testnet === testnet), [accounts, testnet]) + const onSelect = React.useCallback(() => { + if (!selectedAccount) return + + const params = new URLSearchParams() + if (amount) params.append("amount", amount) + if (asset) params.append("asset", stringifyAsset(asset)) + if (destination) params.append("destination", destination) + if (memo) params.append("memo", memo) + if (memoType) params.append("memoType", memoType) + + onClose() + router.history.push(routes.createPayment(selectedAccount.id) + "?" + params.toString()) + }, [amount, asset, destination, memo, memoType, router.history, selectedAccount, onClose]) + return ( + } actions={ - } onClick={onDismiss} type="secondary"> + } onClick={onClose} type="secondary"> {t("transaction-request.payment-account-selection.action.dismiss")} - } onClick={undefined} type="primary"> + } onClick={onSelect} type="primary"> {t("transaction-request.payment-account-selection.action.select")} diff --git a/src/Transaction/components/TransactionRequestHandler.tsx b/src/Transaction/components/TransactionRequestHandler.tsx index 7a6334ae1..c0493baa3 100644 --- a/src/Transaction/components/TransactionRequestHandler.tsx +++ b/src/Transaction/components/TransactionRequestHandler.tsx @@ -52,13 +52,13 @@ function TransactionRequestHandler() { if (renderedURI.operation === StellarUriType.Pay) { const payStellarUri = renderedURI as PayStellarUri - const onDismiss = () => { + const onClose = () => { closeDialog() } return ( - + ) } From 776cffb4a8b28c5185c21770d7c60fbe4d9d33ae Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 30 Nov 2020 12:12:32 +0100 Subject: [PATCH 10/23] Only show accounts which have trustline for asset --- .../PaymentAccountSelectionDialog.tsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Transaction/components/PaymentAccountSelectionDialog.tsx b/src/Transaction/components/PaymentAccountSelectionDialog.tsx index a35749212..13713aaad 100644 --- a/src/Transaction/components/PaymentAccountSelectionDialog.tsx +++ b/src/Transaction/components/PaymentAccountSelectionDialog.tsx @@ -17,7 +17,8 @@ import AssetLogo from "~Assets/components/AssetLogo" import { ActionButton, DialogActionsBox } from "~Generic/components/DialogActions" import { CopyableAddress } from "~Generic/components/PublicKey" import MainTitle from "~Generic/components/MainTitle" -import { stringifyAsset } from "~Generic/lib/stellar" +import { balancelineToAsset, stringifyAsset } from "~Generic/lib/stellar" +import { useLiveAccountDataSet } from "~Generic/hooks/stellar-subscriptions" import { useIsMobile, useRouter } from "~Generic/hooks/userinterface" import DialogBody from "~Layout/components/DialogBody" @@ -93,7 +94,24 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps assetIssuer ]) const keyItemXS = React.useMemo(() => (isSmallScreen ? 4 : 5), [isSmallScreen]) - const selectableAccounts = React.useMemo(() => accounts.filter(acc => acc.testnet === testnet), [accounts, testnet]) + + const accountDataSet = useLiveAccountDataSet( + accounts.map(acc => acc.publicKey), + testnet + ) + + const selectableAccounts = React.useMemo( + () => + accounts.filter(acc => { + if (acc.testnet !== testnet) return false + const matchingAccountData = accountDataSet.find(accData => accData.account_id === acc.publicKey) + if (!matchingAccountData) return false + const trustlines = matchingAccountData.balances.map(balancelineToAsset) + // only show accounts that have trustline for specified asset + return Boolean(trustlines.find(trustline => trustline.code === asset.code && trustline.issuer === asset.issuer)) + }), + [accountDataSet, accounts, asset, testnet] + ) const onSelect = React.useCallback(() => { if (!selectedAccount) return From c26250547f5bf357e508921074662e6ae126971f Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 2 Dec 2020 12:27:51 +0100 Subject: [PATCH 11/23] Add proper backnavigation from payment dialog when it is opened by the PaymentAccountSelectionDialog --- src/Payment/components/PaymentDialog.tsx | 47 ++-- src/Payment/components/PaymentForm.tsx | 13 +- .../PaymentAccountSelectionDialog.tsx | 203 ++++++++++-------- 3 files changed, 135 insertions(+), 128 deletions(-) diff --git a/src/Payment/components/PaymentDialog.tsx b/src/Payment/components/PaymentDialog.tsx index c16f2ca8c..fb3668035 100644 --- a/src/Payment/components/PaymentDialog.tsx +++ b/src/Payment/components/PaymentDialog.tsx @@ -4,9 +4,9 @@ import { Asset, MemoType, Server, Transaction } from "stellar-sdk" import { Account } from "~App/contexts/accounts" import { trackError } from "~App/contexts/notifications" import { useLiveAccountData, useLiveAccountOffers } from "~Generic/hooks/stellar-subscriptions" -import { useDialogActions, useRouter } from "~Generic/hooks/userinterface" +import { useDialogActions } from "~Generic/hooks/userinterface" import { AccountData } from "~Generic/lib/account" -import { getAssetsFromBalances, parseAssetID } from "~Generic/lib/stellar" +import { getAssetsFromBalances } from "~Generic/lib/stellar" import DialogBody from "~Layout/components/DialogBody" import TestnetBadge from "~Generic/components/TestnetBadge" import { Box } from "~Layout/components/Box" @@ -15,12 +15,12 @@ import MainTitle from "~Generic/components/MainTitle" import TransactionSender from "~Transaction/components/TransactionSender" import PaymentForm from "./PaymentForm" -export interface PaymentQueryParams { - amount: string | null - asset: Asset | null - destination: string | null - memo: string | null - memoType: MemoType | null +export interface PaymentParams { + amount?: string + asset?: Asset + destination?: string + memo?: string + memoType?: MemoType } interface Props { @@ -29,6 +29,7 @@ interface Props { horizon: Server onClose: () => void openOrdersCount: number + paymentParams?: PaymentParams sendTransaction: (transaction: Transaction) => Promise } @@ -38,28 +39,6 @@ function PaymentDialog(props: Props) { const { t } = useTranslation() const [txCreationPending, setTxCreationPending] = React.useState(false) - const router = useRouter() - - const query = React.useMemo(() => new URLSearchParams(router.location.search), [router.location.search]) - const [queryParams, setQueryParams] = React.useState({ - amount: null, - asset: null, - destination: null, - memo: null, - memoType: null - }) - - React.useEffect(() => { - const amount = query.get("amount") - const assetString = query.get("asset") - const asset = assetString ? parseAssetID(assetString) : null - const destination = query.get("destination") - const memo = query.get("memo") - const memoType = query.get("memoType") ? (query.get("memoType") as MemoType) : null - - setQueryParams({ amount, asset, destination, memo, memoType }) - }, [query]) - const handleSubmit = React.useCallback( async (createTx: (horizon: Server, account: Account) => Promise) => { try { @@ -105,7 +84,7 @@ function PaymentDialog(props: Props) { onCancel={props.onClose} onSubmit={handleSubmit} openOrdersCount={props.openOrdersCount} - preselectedParams={queryParams} + preselectedParams={props.paymentParams} testnet={props.account.testnet} trustedAssets={trustedAssets} txCreationPending={txCreationPending} @@ -114,12 +93,14 @@ function PaymentDialog(props: Props) { ) } -function ConnectedPaymentDialog(props: Pick) { +function ConnectedPaymentDialog( + props: Pick & { onSubmissionCompleted?: () => void } +) { const accountData = useLiveAccountData(props.account.publicKey, props.account.testnet) const { offers: openOrders } = useLiveAccountOffers(props.account.publicKey, props.account.testnet) return ( - + {({ horizon, sendTransaction }) => ( void openOrdersCount: number - preselectedParams: PaymentQueryParams + preselectedParams?: PaymentParams testnet: boolean trustedAssets: Asset[] txCreationPending?: boolean @@ -97,6 +97,8 @@ const PaymentForm = React.memo(function PaymentForm(props: PaymentFormProps) { ) React.useEffect(() => { + if (!preselectedParams) return + if (preselectedParams.amount) setValue("amount", preselectedParams.amount) if (preselectedParams.asset) setValue("asset", preselectedParams.asset) if (preselectedParams.destination) setValue("destination", preselectedParams.destination) @@ -114,7 +116,7 @@ const PaymentForm = React.memo(function PaymentForm(props: PaymentFormProps) { const knownAccount = wellknownAccounts.lookup(formValues.destination) setMatchingWellknownAccount(knownAccount) - if (preselectedParams.memo && preselectedParams.memoType) { + if (preselectedParams && preselectedParams.memo && preselectedParams.memoType) { setMemoType(preselectedParams.memoType) setMemoMetadata({ label: @@ -147,8 +149,7 @@ const PaymentForm = React.memo(function PaymentForm(props: PaymentFormProps) { formValues.memoValue, matchingWellknownAccount, memoType, - preselectedParams.memo, - preselectedParams.memoType, + preselectedParams, t, wellknownAccounts ]) @@ -350,7 +351,7 @@ interface Props { accountData: AccountData actionsRef: RefStateObject openOrdersCount: number - preselectedParams: PaymentQueryParams + preselectedParams?: PaymentParams testnet: boolean trustedAssets: Asset[] txCreationPending?: boolean diff --git a/src/Transaction/components/PaymentAccountSelectionDialog.tsx b/src/Transaction/components/PaymentAccountSelectionDialog.tsx index 13713aaad..c0cf0eab8 100644 --- a/src/Transaction/components/PaymentAccountSelectionDialog.tsx +++ b/src/Transaction/components/PaymentAccountSelectionDialog.tsx @@ -3,6 +3,7 @@ import { Trans, useTranslation } from "react-i18next" import { Asset } from "stellar-sdk" import { PayStellarUri } from "@stellarguard/stellar-uri" import Box from "@material-ui/core/Box" +import Dialog from "@material-ui/core/Dialog" import Grid from "@material-ui/core/Grid" import makeStyles from "@material-ui/core/styles/makeStyles" import Typography from "@material-ui/core/Typography" @@ -10,17 +11,18 @@ import CancelIcon from "@material-ui/icons/Cancel" import SelectIcon from "@material-ui/icons/Check" import WarningIcon from "@material-ui/icons/Warning" import { Account, AccountsContext } from "~App/contexts/accounts" -import * as routes from "~App/routes" -import { warningColor } from "~App/theme" +import { FullscreenDialogTransition, warningColor } from "~App/theme" import AccountSelectionList from "~Account/components/AccountSelectionList" import AssetLogo from "~Assets/components/AssetLogo" import { ActionButton, DialogActionsBox } from "~Generic/components/DialogActions" import { CopyableAddress } from "~Generic/components/PublicKey" import MainTitle from "~Generic/components/MainTitle" -import { balancelineToAsset, stringifyAsset } from "~Generic/lib/stellar" +import ViewLoading from "~Generic/components/ViewLoading" +import { balancelineToAsset } from "~Generic/lib/stellar" import { useLiveAccountDataSet } from "~Generic/hooks/stellar-subscriptions" -import { useIsMobile, useRouter } from "~Generic/hooks/userinterface" +import { useIsMobile } from "~Generic/hooks/userinterface" import DialogBody from "~Layout/components/DialogBody" +import PaymentDialog from "~Payment/components/PaymentDialog" const useStyles = makeStyles(theme => ({ assetContainer: { @@ -83,11 +85,11 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps const classes = useStyles() const isSmallScreen = useIsMobile() - const router = useRouter() const { t } = useTranslation() const { accounts } = React.useContext(AccountsContext) const [selectedAccount, setSelectedAccount] = React.useState(null) + const [showPaymentDialog, setShowPaymentDialog] = React.useState(false) const asset = React.useMemo(() => (assetCode && assetIssuer ? new Asset(assetCode, assetIssuer) : Asset.native()), [ assetCode, @@ -113,103 +115,126 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps [accountDataSet, accounts, asset, testnet] ) - const onSelect = React.useCallback(() => { - if (!selectedAccount) return - - const params = new URLSearchParams() - if (amount) params.append("amount", amount) - if (asset) params.append("asset", stringifyAsset(asset)) - if (destination) params.append("destination", destination) - if (memo) params.append("memo", memo) - if (memoType) params.append("memoType", memoType) + const paymentParams = React.useMemo(() => { + return { + amount, + asset, + destination, + memo, + memoType + } + }, [amount, asset, destination, memo, memoType]) - onClose() - router.history.push(routes.createPayment(selectedAccount.id) + "?" + params.toString()) - }, [amount, asset, destination, memo, memoType, router.history, selectedAccount, onClose]) + const onSelect = React.useCallback(() => { + if (selectedAccount) { + setShowPaymentDialog(true) + } + }, [selectedAccount]) return ( - - } - actions={ - - } onClick={onClose} type="secondary"> - {t("transaction-request.payment-account-selection.action.dismiss")} - - } onClick={onSelect} type="primary"> - {t("transaction-request.payment-account-selection.action.select")} - - - } - > - - {!signature && ( - - - {t("transaction-request.payment-account-selection.warning")} - - - )} - - {originDomain ? ( - - You opened the following payment request from {{ originDomain }}: - - ) : ( - t("transaction-request.payment-account-selection.header.no-origin-domain") + <> + + } + actions={ + + } onClick={onClose} type="secondary"> + {t("transaction-request.payment-account-selection.action.dismiss")} + + } onClick={onSelect} type="primary"> + {t("transaction-request.payment-account-selection.action.select")} + + + } + > + + {!signature && ( + + + + {t("transaction-request.payment-account-selection.warning")} + + + )} - - - - - {t("transaction-request.payment-account-selection.uri-content.pay")} - - - {amount ? amount : t("transaction-request.payment-account-selection.uri-content.any")} -
- {asset.getCode()} - -
-
-
- - - {t("transaction-request.payment-account-selection.uri-content.to")} - - - - - - {memo && ( + + {originDomain ? ( + + You opened the following payment request from {{ originDomain }}: + + ) : ( + t("transaction-request.payment-account-selection.header.no-origin-domain") + )} + + - {t("transaction-request.payment-account-selection.uri-content.memo")} + {t("transaction-request.payment-account-selection.uri-content.pay")} - - {memo} + + {amount ? amount : t("transaction-request.payment-account-selection.uri-content.any")} +
+ {asset.getCode()} + +
- )} - {msg && ( - + - {t("transaction-request.payment-account-selection.uri-content.message")} + {t("transaction-request.payment-account-selection.uri-content.to")} - - {msg} + + - )} -
- - {t("transaction-request.payment-account-selection.footer")} - - -
-
+ {memo && ( + + + {t("transaction-request.payment-account-selection.uri-content.memo")} + + + {memo} + + + )} + {msg && ( + + + {t("transaction-request.payment-account-selection.uri-content.message")} + + + {msg} + + + )} + + + {t("transaction-request.payment-account-selection.footer")} + + + +
+ {selectedAccount && ( + setShowPaymentDialog(false)} + TransitionComponent={FullscreenDialogTransition} + > + }> + setShowPaymentDialog(false)} + onSubmissionCompleted={props.onClose} + paymentParams={paymentParams} + /> + + + )} + ) } From c5a0579046f2c7abdcf85efa6f03bf3bdf13f745 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 2 Dec 2020 13:02:01 +0100 Subject: [PATCH 12/23] Enable trusted services by default --- src/AppSettings/components/AppSettings.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/AppSettings/components/AppSettings.tsx b/src/AppSettings/components/AppSettings.tsx index 0cb5e25b0..5c6d367a4 100644 --- a/src/AppSettings/components/AppSettings.tsx +++ b/src/AppSettings/components/AppSettings.tsx @@ -34,7 +34,6 @@ function AppSettings() { const { accounts } = React.useContext(AccountsContext) const settings = React.useContext(SettingsContext) - const trustedServicesEnabled = process.env.TRUSTED_SERVICES && process.env.TRUSTED_SERVICES === "enabled" const getEffectiveLanguage = (lang: L, fallback: F) => { return availableLanguages.indexOf(lang as any) > -1 ? lang : fallback @@ -73,7 +72,7 @@ function AppSettings() { /> - {trustedServicesEnabled ? : undefined} + From 714943dbea5335929e12964683635ee6d1b50466 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 2 Dec 2020 13:02:24 +0100 Subject: [PATCH 13/23] Disable form fields if preselected params exist --- src/Generic/components/AssetSelector.tsx | 2 ++ src/Payment/components/PaymentForm.tsx | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Generic/components/AssetSelector.tsx b/src/Generic/components/AssetSelector.tsx index d7e534b45..ae59d0eaf 100644 --- a/src/Generic/components/AssetSelector.tsx +++ b/src/Generic/components/AssetSelector.tsx @@ -72,6 +72,7 @@ interface AssetSelectorProps { assets: Array children?: React.ReactNode className?: string + disabled?: boolean disabledAssets?: Asset[] disableUnderline?: boolean helperText?: TextFieldProps["helperText"] @@ -123,6 +124,7 @@ function AssetSelector(props: AssetSelectorProps) { ( ("payment.validation.no-destination"), @@ -211,7 +212,7 @@ const PaymentForm = React.memo(function PaymentForm(props: PaymentFormProps) { placeholder={t("payment.inputs.destination.placeholder")} /> ), - [form, qrReaderAdornment, setValue, t] + [form, qrReaderAdornment, preselectedParams, setValue, t] ) const assetSelector = React.useMemo( @@ -221,6 +222,7 @@ const PaymentForm = React.memo(function PaymentForm(props: PaymentFormProps) { ), - [form, formValues.asset, props.accountData.balances, props.testnet] + [form, formValues.asset, preselectedParams, props.accountData.balances, props.testnet] ) const priceInput = React.useMemo( () => ( ("payment.validation.no-price"), @@ -259,12 +262,13 @@ const PaymentForm = React.memo(function PaymentForm(props: PaymentFormProps) { }} /> ), - [assetSelector, form, isSmallScreen, spendableBalance, t] + [assetSelector, form, isSmallScreen, preselectedParams, spendableBalance, t] ) const memoInput = React.useMemo( () => ( Date: Wed, 2 Dec 2020 13:22:02 +0100 Subject: [PATCH 14/23] Show error when no account is selectable --- i18n/locales/en/transaction-request.json | 4 ++++ .../components/PaymentAccountSelectionDialog.tsx | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/i18n/locales/en/transaction-request.json b/i18n/locales/en/transaction-request.json index 5c934adfc..2b63bce1c 100644 --- a/i18n/locales/en/transaction-request.json +++ b/i18n/locales/en/transaction-request.json @@ -4,6 +4,10 @@ "dismiss": "Dismiss", "select": "Select" }, + "error": { + "no-activated-accounts": "No activated accounts found.", + "no-accounts-with-trustline": "No accounts with matching trustline found." + }, "header": { "origin-domain": "You opened the following payment request from <1>{{originDomain}} <3>", "no-origin-domain": "You opened the following payment request from an unknown origin" diff --git a/src/Transaction/components/PaymentAccountSelectionDialog.tsx b/src/Transaction/components/PaymentAccountSelectionDialog.tsx index c0cf0eab8..4999bc2ff 100644 --- a/src/Transaction/components/PaymentAccountSelectionDialog.tsx +++ b/src/Transaction/components/PaymentAccountSelectionDialog.tsx @@ -214,7 +214,15 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps {t("transaction-request.payment-account-selection.footer")} - + {selectableAccounts.length > 0 ? ( + + ) : ( + + {asset.code === "XLM" + ? t("transaction-request.payment-account-selection.error.no-activated-accounts") + : t("transaction-request.payment-account-selection.error.no-accounts-with-trustline")} + + )} {selectedAccount && ( From 77d5a458d76f2c089260b70f4afa50966add1655 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 2 Dec 2020 15:43:33 +0100 Subject: [PATCH 15/23] Remove colons and change warning text --- i18n/locales/en/transaction-request.json | 2 +- src/Transaction/components/VerifyTrustedServiceDialog.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/i18n/locales/en/transaction-request.json b/i18n/locales/en/transaction-request.json index 2b63bce1c..a0b4a2f48 100644 --- a/i18n/locales/en/transaction-request.json +++ b/i18n/locales/en/transaction-request.json @@ -30,7 +30,7 @@ }, "title": "Verify Trusted Service", "info": { - "1": "You opened a Stellar URI originating from an untrusted service", + "1": "You opened a Stellar URI originating from an unknown origin", "2": "If you trust this domain you can add this service to your list of trusted services.", "3": "You can view and edit your list of trusted services anytime in the application settings." } diff --git a/src/Transaction/components/VerifyTrustedServiceDialog.tsx b/src/Transaction/components/VerifyTrustedServiceDialog.tsx index a38623b1b..39ca7e107 100644 --- a/src/Transaction/components/VerifyTrustedServiceDialog.tsx +++ b/src/Transaction/components/VerifyTrustedServiceDialog.tsx @@ -42,8 +42,8 @@ function VerifyTrustedServiceDialog(props: VerifyTrustedServiceDialogProps) { {props.domain} - {t("transaction-request.verify-trusted-service.info.2")}: - {t("transaction-request.verify-trusted-service.info.3")}: + {t("transaction-request.verify-trusted-service.info.2")} + {t("transaction-request.verify-trusted-service.info.3")} ) From ebf347e0229bb06cb0c68751f25738b71f09612d Mon Sep 17 00:00:00 2001 From: Andy Wermke Date: Thu, 3 Dec 2020 19:18:01 +0100 Subject: [PATCH 16/23] Some small-ish UI changes affecting the tx request review dialog --- i18n/locales/en/transaction-request.json | 8 ++-- .../PaymentAccountSelectionDialog.tsx | 48 +++++++++---------- .../components/TransactionRequestHandler.tsx | 1 + .../components/VerifyTrustedServiceDialog.tsx | 3 +- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/i18n/locales/en/transaction-request.json b/i18n/locales/en/transaction-request.json index a0b4a2f48..5c90584ab 100644 --- a/i18n/locales/en/transaction-request.json +++ b/i18n/locales/en/transaction-request.json @@ -1,5 +1,6 @@ { "payment-account-selection": { + "account-selector": "Select the account to use", "action": { "dismiss": "Dismiss", "select": "Select" @@ -9,10 +10,9 @@ "no-accounts-with-trustline": "No accounts with matching trustline found." }, "header": { - "origin-domain": "You opened the following payment request from <1>{{originDomain}} <3>", - "no-origin-domain": "You opened the following payment request from an unknown origin" + "origin-domain": "The following transaction has been proposed by <1>{{originDomain}}<3>", + "no-origin-domain": "You opened the following payment request from an unknown origin." }, - "footer": "Please select the account that you want to use for this payment.", "uri-content": { "any": "Any", "pay": "Pay", @@ -20,7 +20,7 @@ "memo": "Memo", "message": "Message" }, - "title": "Select Account for Payment", + "title": "Transaction proposed", "warning": "This request did not include a signature! Make sure that you trust the source of this link!" }, "verify-trusted-service": { diff --git a/src/Transaction/components/PaymentAccountSelectionDialog.tsx b/src/Transaction/components/PaymentAccountSelectionDialog.tsx index 4999bc2ff..3409a68b7 100644 --- a/src/Transaction/components/PaymentAccountSelectionDialog.tsx +++ b/src/Transaction/components/PaymentAccountSelectionDialog.tsx @@ -11,7 +11,7 @@ import CancelIcon from "@material-ui/icons/Cancel" import SelectIcon from "@material-ui/icons/Check" import WarningIcon from "@material-ui/icons/Warning" import { Account, AccountsContext } from "~App/contexts/accounts" -import { FullscreenDialogTransition, warningColor } from "~App/theme" +import { FullscreenDialogTransition, warningColor, breakpoints } from "~App/theme" import AccountSelectionList from "~Account/components/AccountSelectionList" import AssetLogo from "~Assets/components/AssetLogo" import { ActionButton, DialogActionsBox } from "~Generic/components/DialogActions" @@ -24,7 +24,7 @@ import { useIsMobile } from "~Generic/hooks/userinterface" import DialogBody from "~Layout/components/DialogBody" import PaymentDialog from "~Payment/components/PaymentDialog" -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles(() => ({ assetContainer: { alignSelf: "center", display: "flex", @@ -40,6 +40,9 @@ const useStyles = makeStyles(theme => ({ flexDirection: "column", padding: "12px 0 0" }, + row: { + lineHeight: 1.2 + }, keyTypography: { alignSelf: "center", textAlign: "right" @@ -48,8 +51,8 @@ const useStyles = makeStyles(theme => ({ textAlign: "left" }, uriContainer: { - paddingTop: 16, - paddingBottom: 16 + paddingTop: 32, + paddingBottom: 32 }, warningContainer: { alignItems: "center", @@ -58,8 +61,11 @@ const useStyles = makeStyles(theme => ({ display: "flex", justifyContent: "center", padding: "6px 16px", - marginBottom: 16, - width: "fit-content" + width: "fit-content", + + [breakpoints.up(600)]: { + width: "100%" + } } })) @@ -134,7 +140,6 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps return ( <> @@ -151,7 +156,11 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps } > - {!signature && ( + {signature ? ( + + The following transaction has been proposed by {{ originDomain }}. + + ) : ( @@ -160,21 +169,12 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps )} - - {originDomain ? ( - - You opened the following payment request from {{ originDomain }}: - - ) : ( - t("transaction-request.payment-account-selection.header.no-origin-domain") - )} - - + {t("transaction-request.payment-account-selection.uri-content.pay")} - + {amount ? amount : t("transaction-request.payment-account-selection.uri-content.any")}
{asset.getCode()} @@ -182,7 +182,7 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps
- + {t("transaction-request.payment-account-selection.uri-content.to")} @@ -191,7 +191,7 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps {memo && ( - + {t("transaction-request.payment-account-selection.uri-content.memo")} @@ -201,7 +201,7 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps )} {msg && ( - + {t("transaction-request.payment-account-selection.uri-content.message")} @@ -211,8 +211,8 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps )}
- - {t("transaction-request.payment-account-selection.footer")} + + {t("transaction-request.payment-account-selection.account-selector")} {selectableAccounts.length > 0 ? ( diff --git a/src/Transaction/components/TransactionRequestHandler.tsx b/src/Transaction/components/TransactionRequestHandler.tsx index c0493baa3..d080ba5d3 100644 --- a/src/Transaction/components/TransactionRequestHandler.tsx +++ b/src/Transaction/components/TransactionRequestHandler.tsx @@ -31,6 +31,7 @@ function TransactionRequestHandler() { } const trustedService = trustedServices.find(service => renderedURI.originDomain === service.domain) + if (renderedURI.originDomain && !trustedService) { const onTrust = () => { const newTrustedServices: TrustedService[] = [ diff --git a/src/Transaction/components/VerifyTrustedServiceDialog.tsx b/src/Transaction/components/VerifyTrustedServiceDialog.tsx index 39ca7e107..5fb652d10 100644 --- a/src/Transaction/components/VerifyTrustedServiceDialog.tsx +++ b/src/Transaction/components/VerifyTrustedServiceDialog.tsx @@ -23,7 +23,6 @@ function VerifyTrustedServiceDialog(props: VerifyTrustedServiceDialogProps) { return ( } - noMaxWidth preventNotchSpacing top={} actions={ @@ -39,7 +38,7 @@ function VerifyTrustedServiceDialog(props: VerifyTrustedServiceDialogProps) { > {t("transaction-request.verify-trusted-service.info.1")}: - + {props.domain} {t("transaction-request.verify-trusted-service.info.2")} From 1adf3f22c691069af65de49d754d972a2ae71dbb Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 7 Dec 2020 10:14:47 +0100 Subject: [PATCH 17/23] Change wording of warning message Co-authored-by: Andy Wermke --- i18n/locales/en/transaction-request.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/locales/en/transaction-request.json b/i18n/locales/en/transaction-request.json index 5c90584ab..bfbf8461c 100644 --- a/i18n/locales/en/transaction-request.json +++ b/i18n/locales/en/transaction-request.json @@ -21,7 +21,7 @@ "message": "Message" }, "title": "Transaction proposed", - "warning": "This request did not include a signature! Make sure that you trust the source of this link!" + "warning": "The origin of this request cannot be verified! Decline when in doubt." }, "verify-trusted-service": { "action": { From b106196eb7def44d7b6aaf8639bccd2d16c8bb32 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 20 Jan 2021 15:33:50 +0100 Subject: [PATCH 18/23] Show dismiss button if onCancel prop present The onCancel prop was not used before so now it indicates whether the payment form should show a dismiss button or not --- i18n/locales/en/payment.json | 1 + src/Payment/components/PaymentDialog.tsx | 1 - src/Payment/components/PaymentForm.tsx | 11 +++++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/i18n/locales/en/payment.json b/i18n/locales/en/payment.json index cffbdba23..6ebe8c5fe 100644 --- a/i18n/locales/en/payment.json +++ b/i18n/locales/en/payment.json @@ -1,5 +1,6 @@ { "actions": { + "dismiss": "Dismiss", "submit": "Send now" }, "memo-metadata": { diff --git a/src/Payment/components/PaymentDialog.tsx b/src/Payment/components/PaymentDialog.tsx index fb3668035..23ad4e6b8 100644 --- a/src/Payment/components/PaymentDialog.tsx +++ b/src/Payment/components/PaymentDialog.tsx @@ -81,7 +81,6 @@ function PaymentDialog(props: Props) { void onSubmit: ( formValues: ExtendedPaymentFormValues, spendableBalance: BigNumber, @@ -326,6 +328,11 @@ const PaymentForm = React.memo(function PaymentForm(props: PaymentFormProps) { const dialogActions = React.useMemo( () => ( + {props.onCancel && ( + } onClick={props.onCancel}> + {t("payment.actions.dismiss")} + + )} } @@ -337,7 +344,7 @@ const PaymentForm = React.memo(function PaymentForm(props: PaymentFormProps) { ), - [formID, props.txCreationPending, t] + [formID, props.onCancel, props.txCreationPending, t] ) return ( @@ -360,7 +367,7 @@ interface Props { testnet: boolean trustedAssets: Asset[] txCreationPending?: boolean - onCancel: () => void + onCancel?: () => void onSubmit: (createTx: (horizon: Server, account: Account) => Promise) => any } From 90f3e3cef4f8e241e75eb43a0463c259d51b62c8 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 20 Jan 2021 15:34:12 +0100 Subject: [PATCH 19/23] Add 'selectedAccount' prop to AccountSelectionList --- src/Account/components/AccountSelectionList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Account/components/AccountSelectionList.tsx b/src/Account/components/AccountSelectionList.tsx index 58341727a..80008692f 100644 --- a/src/Account/components/AccountSelectionList.tsx +++ b/src/Account/components/AccountSelectionList.tsx @@ -15,6 +15,7 @@ const isMobileDevice = process.env.PLATFORM === "android" || process.env.PLATFOR interface AccountSelectionListProps { accounts: Account[] disabled?: boolean + selectedAccount?: Account testnet: boolean onChange?: (account: Account) => void } @@ -38,7 +39,7 @@ function AccountSelectionList(props: AccountSelectionListProps) { index={index} key={account.id} onClick={handleListItemClick} - selected={index === selectedIndex} + selected={props.selectedAccount ? account === props.selectedAccount : index === selectedIndex} /> ))} {props.accounts.length === 0 ? ( From 2c107d01ffa58efca32b4772ec7e0e73568942d5 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 20 Jan 2021 15:34:39 +0100 Subject: [PATCH 20/23] Add NoAccountsDialog --- i18n/locales/en/transaction-request.json | 10 ++++ .../components/ NoAccountsDialog.tsx | 49 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/TransactionRequest/components/ NoAccountsDialog.tsx diff --git a/i18n/locales/en/transaction-request.json b/i18n/locales/en/transaction-request.json index bfbf8461c..a95f6ec96 100644 --- a/i18n/locales/en/transaction-request.json +++ b/i18n/locales/en/transaction-request.json @@ -34,5 +34,15 @@ "2": "If you trust this domain you can add this service to your list of trusted services.", "3": "You can view and edit your list of trusted services anytime in the application settings." } + }, + "no-accounts": { + "action": { + "dismiss": "Dismiss" + }, + "info": { + "1": "No accounts found for the specified network.", + "2": "You must import an account before you can sign transaction requests." + }, + "title": "Transaction proposed" } } diff --git a/src/TransactionRequest/components/ NoAccountsDialog.tsx b/src/TransactionRequest/components/ NoAccountsDialog.tsx new file mode 100644 index 000000000..b660fc1d0 --- /dev/null +++ b/src/TransactionRequest/components/ NoAccountsDialog.tsx @@ -0,0 +1,49 @@ +import Box from "@material-ui/core/Box" +import Typography from "@material-ui/core/Typography" +import CancelIcon from "@material-ui/icons/Close" +import React from "react" +import { useTranslation } from "react-i18next" +import { ActionButton, DialogActionsBox } from "~Generic/components/DialogActions" +import MainTitle from "~Generic/components/MainTitle" +import TestnetBadge from "~Generic/components/TestnetBadge" +import DialogBody from "~Layout/components/DialogBody" + +interface Props { + onClose: () => void + testnet: boolean +} + +function NoAccountsDialog(props: Props) { + const { t } = useTranslation() + return ( + + {t("transaction-request.no-accounts.title")} + {props.testnet ? : null} + + } + /> + } + actions={ + + } onClick={props.onClose} type="secondary"> + {t("transaction-request.no-accounts.action.dismiss")} + + + } + > + + {t("transaction-request.no-accounts.info.1")} + {t("transaction-request.no-accounts.info.2")} + + + ) +} + +export default NoAccountsDialog From d6dff6502afa449b24f1c97aaefe07ba254d0765 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 20 Jan 2021 15:35:05 +0100 Subject: [PATCH 21/23] Move components --- src/App/bootstrap/app-stage2.tsx | 2 +- .../components/TransactionRequestHandler.tsx | 0 .../components/VerifyTrustedServiceDialog.tsx | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/{Transaction => TransactionRequest}/components/TransactionRequestHandler.tsx (100%) rename src/{Transaction => TransactionRequest}/components/VerifyTrustedServiceDialog.tsx (100%) diff --git a/src/App/bootstrap/app-stage2.tsx b/src/App/bootstrap/app-stage2.tsx index 6a8d9457c..83a55574f 100644 --- a/src/App/bootstrap/app-stage2.tsx +++ b/src/App/bootstrap/app-stage2.tsx @@ -7,7 +7,7 @@ import { VerticalLayout } from "~Layout/components/Box" import { appIsLoaded } from "~SplashScreen/splash-screen" import ConnectionErrorListener from "~Toasts/components/ConnectionErrorListener" import NotificationContainer from "~Toasts/components/NotificationContainer" -import TransactionRequestHandler from "~Transaction/components/TransactionRequestHandler" +import TransactionRequestHandler from "~TransactionRequest/components/TransactionRequestHandler" import AllAccountsPage from "../components/AccountListView" import AndroidBackButton from "../components/AndroidBackButton" import DesktopNotifications from "../components/DesktopNotifications" diff --git a/src/Transaction/components/TransactionRequestHandler.tsx b/src/TransactionRequest/components/TransactionRequestHandler.tsx similarity index 100% rename from src/Transaction/components/TransactionRequestHandler.tsx rename to src/TransactionRequest/components/TransactionRequestHandler.tsx diff --git a/src/Transaction/components/VerifyTrustedServiceDialog.tsx b/src/TransactionRequest/components/VerifyTrustedServiceDialog.tsx similarity index 100% rename from src/Transaction/components/VerifyTrustedServiceDialog.tsx rename to src/TransactionRequest/components/VerifyTrustedServiceDialog.tsx From f6b3c6769eee4cea6d5c2b419152d2f343698c3a Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 20 Jan 2021 15:35:15 +0100 Subject: [PATCH 22/23] Refactor PaymentAccountSelectionDialog --- .../PaymentAccountSelectionDialog.tsx | 249 ----------------- .../PaymentAccountSelectionDialog.tsx | 261 ++++++++++++++++++ 2 files changed, 261 insertions(+), 249 deletions(-) delete mode 100644 src/Transaction/components/PaymentAccountSelectionDialog.tsx create mode 100644 src/TransactionRequest/components/PaymentAccountSelectionDialog.tsx diff --git a/src/Transaction/components/PaymentAccountSelectionDialog.tsx b/src/Transaction/components/PaymentAccountSelectionDialog.tsx deleted file mode 100644 index 3409a68b7..000000000 --- a/src/Transaction/components/PaymentAccountSelectionDialog.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import React from "react" -import { Trans, useTranslation } from "react-i18next" -import { Asset } from "stellar-sdk" -import { PayStellarUri } from "@stellarguard/stellar-uri" -import Box from "@material-ui/core/Box" -import Dialog from "@material-ui/core/Dialog" -import Grid from "@material-ui/core/Grid" -import makeStyles from "@material-ui/core/styles/makeStyles" -import Typography from "@material-ui/core/Typography" -import CancelIcon from "@material-ui/icons/Cancel" -import SelectIcon from "@material-ui/icons/Check" -import WarningIcon from "@material-ui/icons/Warning" -import { Account, AccountsContext } from "~App/contexts/accounts" -import { FullscreenDialogTransition, warningColor, breakpoints } from "~App/theme" -import AccountSelectionList from "~Account/components/AccountSelectionList" -import AssetLogo from "~Assets/components/AssetLogo" -import { ActionButton, DialogActionsBox } from "~Generic/components/DialogActions" -import { CopyableAddress } from "~Generic/components/PublicKey" -import MainTitle from "~Generic/components/MainTitle" -import ViewLoading from "~Generic/components/ViewLoading" -import { balancelineToAsset } from "~Generic/lib/stellar" -import { useLiveAccountDataSet } from "~Generic/hooks/stellar-subscriptions" -import { useIsMobile } from "~Generic/hooks/userinterface" -import DialogBody from "~Layout/components/DialogBody" -import PaymentDialog from "~Payment/components/PaymentDialog" - -const useStyles = makeStyles(() => ({ - assetContainer: { - alignSelf: "center", - display: "flex", - margin: "0px 8px" - }, - assetLogo: { - width: 28, - height: 28, - margin: "0px 4px" - }, - root: { - display: "flex", - flexDirection: "column", - padding: "12px 0 0" - }, - row: { - lineHeight: 1.2 - }, - keyTypography: { - alignSelf: "center", - textAlign: "right" - }, - valueTypography: { - textAlign: "left" - }, - uriContainer: { - paddingTop: 32, - paddingBottom: 32 - }, - warningContainer: { - alignItems: "center", - alignSelf: "center", - background: warningColor, - display: "flex", - justifyContent: "center", - padding: "6px 16px", - width: "fit-content", - - [breakpoints.up(600)]: { - width: "100%" - } - } -})) - -interface PaymentAccountSelectionDialogProps { - payStellarUri: PayStellarUri - onClose: () => void -} - -function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps) { - const { onClose } = props - const { - amount, - assetCode, - assetIssuer, - destination, - memo, - memoType, - msg, - originDomain, - signature, - isTestNetwork: testnet - } = props.payStellarUri - - const classes = useStyles() - const isSmallScreen = useIsMobile() - const { t } = useTranslation() - - const { accounts } = React.useContext(AccountsContext) - const [selectedAccount, setSelectedAccount] = React.useState(null) - const [showPaymentDialog, setShowPaymentDialog] = React.useState(false) - - const asset = React.useMemo(() => (assetCode && assetIssuer ? new Asset(assetCode, assetIssuer) : Asset.native()), [ - assetCode, - assetIssuer - ]) - const keyItemXS = React.useMemo(() => (isSmallScreen ? 4 : 5), [isSmallScreen]) - - const accountDataSet = useLiveAccountDataSet( - accounts.map(acc => acc.publicKey), - testnet - ) - - const selectableAccounts = React.useMemo( - () => - accounts.filter(acc => { - if (acc.testnet !== testnet) return false - const matchingAccountData = accountDataSet.find(accData => accData.account_id === acc.publicKey) - if (!matchingAccountData) return false - const trustlines = matchingAccountData.balances.map(balancelineToAsset) - // only show accounts that have trustline for specified asset - return Boolean(trustlines.find(trustline => trustline.code === asset.code && trustline.issuer === asset.issuer)) - }), - [accountDataSet, accounts, asset, testnet] - ) - - const paymentParams = React.useMemo(() => { - return { - amount, - asset, - destination, - memo, - memoType - } - }, [amount, asset, destination, memo, memoType]) - - const onSelect = React.useCallback(() => { - if (selectedAccount) { - setShowPaymentDialog(true) - } - }, [selectedAccount]) - - return ( - <> - - } - actions={ - - } onClick={onClose} type="secondary"> - {t("transaction-request.payment-account-selection.action.dismiss")} - - } onClick={onSelect} type="primary"> - {t("transaction-request.payment-account-selection.action.select")} - - - } - > - - {signature ? ( - - The following transaction has been proposed by {{ originDomain }}. - - ) : ( - - - - {t("transaction-request.payment-account-selection.warning")} - - - - )} - - - - {t("transaction-request.payment-account-selection.uri-content.pay")} - - - {amount ? amount : t("transaction-request.payment-account-selection.uri-content.any")} -
- {asset.getCode()} - -
-
-
- - - {t("transaction-request.payment-account-selection.uri-content.to")} - - - - - - {memo && ( - - - {t("transaction-request.payment-account-selection.uri-content.memo")} - - - {memo} - - - )} - {msg && ( - - - {t("transaction-request.payment-account-selection.uri-content.message")} - - - {msg} - - - )} -
- - {t("transaction-request.payment-account-selection.account-selector")} - - {selectableAccounts.length > 0 ? ( - - ) : ( - - {asset.code === "XLM" - ? t("transaction-request.payment-account-selection.error.no-activated-accounts") - : t("transaction-request.payment-account-selection.error.no-accounts-with-trustline")} - - )} -
-
- {selectedAccount && ( - setShowPaymentDialog(false)} - TransitionComponent={FullscreenDialogTransition} - > - }> - setShowPaymentDialog(false)} - onSubmissionCompleted={props.onClose} - paymentParams={paymentParams} - /> - - - )} - - ) -} - -export default PaymentAccountSelectionDialog diff --git a/src/TransactionRequest/components/PaymentAccountSelectionDialog.tsx b/src/TransactionRequest/components/PaymentAccountSelectionDialog.tsx new file mode 100644 index 000000000..c928edf3e --- /dev/null +++ b/src/TransactionRequest/components/PaymentAccountSelectionDialog.tsx @@ -0,0 +1,261 @@ +import Box from "@material-ui/core/Box" +import makeStyles from "@material-ui/core/styles/makeStyles" +import Typography from "@material-ui/core/Typography" +import WarningIcon from "@material-ui/icons/Warning" +import { PayStellarUri } from "@stellarguard/stellar-uri" +import React from "react" +import { Trans, useTranslation } from "react-i18next" +import { Asset, Server, Transaction } from "stellar-sdk" +import AccountSelectionList from "~Account/components/AccountSelectionList" +import { Account, AccountsContext } from "~App/contexts/accounts" +import { trackError } from "~App/contexts/notifications" +import { breakpoints, warningColor } from "~App/theme" +import MainTitle from "~Generic/components/MainTitle" +import ViewLoading from "~Generic/components/ViewLoading" +import { useLiveAccountDataSet, useLiveAccountOffers } from "~Generic/hooks/stellar-subscriptions" +import { RefStateObject, useDialogActions } from "~Generic/hooks/userinterface" +import { AccountData } from "~Generic/lib/account" +import { getAssetsFromBalances } from "~Generic/lib/stellar" +import DialogBody from "~Layout/components/DialogBody" +import { PaymentParams } from "~Payment/components/PaymentDialog" +import PaymentForm from "~Payment/components/PaymentForm" +import TransactionSender, { SendTransaction } from "../../Transaction/components/TransactionSender" +import NoAccountsDialog from "./ NoAccountsDialog" + +interface ConnectedPaymentFormProps { + accountData: AccountData + actionsRef: RefStateObject + horizon: Server + onClose: () => void + preselectedParams: PaymentParams + selectedAccount: Account + sendTransaction: SendTransaction +} + +function ConnectedPaymentForm(props: ConnectedPaymentFormProps) { + const { sendTransaction } = props + const testnet = props.selectedAccount.testnet + + const [txCreationPending, setTxCreationPending] = React.useState(false) + const { offers: openOrders } = useLiveAccountOffers(props.selectedAccount.publicKey, testnet) + const trustedAssets = React.useMemo(() => getAssetsFromBalances(props.accountData.balances) || [Asset.native()], [ + props.accountData.balances + ]) + + const handleSubmit = React.useCallback( + async (createTx: (horizon: Server, account: Account) => Promise) => { + try { + setTxCreationPending(true) + const tx = await createTx(props.horizon, props.selectedAccount) + setTxCreationPending(false) + await sendTransaction(tx) + } catch (error) { + trackError(error) + } finally { + setTxCreationPending(false) + } + }, + [props.selectedAccount, props.horizon, sendTransaction] + ) + + return ( + + ) +} + +const useStyles = makeStyles(() => ({ + assetContainer: { + alignSelf: "center", + display: "flex", + margin: "0px 8px" + }, + assetLogo: { + width: 28, + height: 28, + margin: "0px 4px" + }, + root: { + display: "flex", + flexDirection: "column", + padding: "12px 0 0" + }, + row: { + lineHeight: 1.2 + }, + keyTypography: { + alignSelf: "center", + textAlign: "right" + }, + valueTypography: { + textAlign: "left" + }, + uriContainer: { + paddingTop: 32, + paddingBottom: 32 + }, + warningContainer: { + alignItems: "center", + alignSelf: "center", + background: warningColor, + display: "flex", + justifyContent: "center", + padding: "6px 16px", + width: "fit-content", + + [breakpoints.up(600)]: { + width: "100%" + } + } +})) + +interface PaymentAccountSelectionDialogProps { + accounts: Account[] + horizon: Server + onAccountChange: (account: Account) => void + onClose: () => void + payStellarUri: PayStellarUri + selectedAccount: Account | null + sendTransaction: SendTransaction +} + +function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps) { + const { onClose, onAccountChange } = props + const { + amount, + assetCode, + assetIssuer, + destination, + memo, + memoType, + msg, + originDomain, + signature, + isTestNetwork: testnet + } = props.payStellarUri + + const classes = useStyles() + const { t } = useTranslation() + const accountDataSet = useLiveAccountDataSet( + props.accounts.map(acc => acc.publicKey), + testnet + ) + const accountData = accountDataSet.find(acc => acc.account_id === props.selectedAccount?.publicKey) + const dialogActionsRef = useDialogActions() + + const asset = React.useMemo(() => (assetCode && assetIssuer ? new Asset(assetCode, assetIssuer) : Asset.native()), [ + assetCode, + assetIssuer + ]) + const paymentParams = React.useMemo(() => { + return { + amount, + asset, + destination, + memo, + memoType + } + }, [amount, asset, destination, memo, memoType]) + + return ( + + } + actions={dialogActionsRef} + > + + {signature ? ( + + + The following transaction has been proposed by {{ originDomain }}. + + + ) : ( + + + {t("transaction-request.payment-account-selection.warning")} + + + )} + }> + {props.selectedAccount && accountData && ( + <> + {msg && ( + + {t("transaction-request.payment.uri-content.message")}: {msg} + + )} + + + )} + + + {t("transaction-request.payment-account-selection.account-selector")} + + {props.accounts.length > 0 ? ( + + ) : ( + + {asset.code === "XLM" + ? t("transaction-request.payment-account-selection.error.no-activated-accounts") + : t("transaction-request.payment-account-selection.error.no-accounts-with-trustline")} + + )} + + + ) +} + +function ConnectedPaymentAccountSelectionDialog( + props: Pick +) { + const { accounts } = React.useContext(AccountsContext) + const testnet = props.payStellarUri.isTestNetwork + const accountsForNetwork = React.useMemo(() => accounts.filter(acc => acc.testnet === testnet), [accounts, testnet]) + const [selectedAccount, setSelectedAccount] = React.useState( + accountsForNetwork.length > 0 ? accountsForNetwork[0] : null + ) + + return accountsForNetwork.length > 0 ? ( + + {({ horizon, sendTransaction }) => ( + + )} + + ) : ( + + ) +} + +export default ConnectedPaymentAccountSelectionDialog From abc70ab2e935cf213546084c060e3a93b1b5c773 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 20 Jan 2021 15:36:12 +0100 Subject: [PATCH 23/23] Change key of i18n strings --- i18n/locales/en/transaction-request.json | 26 ++++++++----------- .../PaymentAccountSelectionDialog.tsx | 14 +++++----- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/i18n/locales/en/transaction-request.json b/i18n/locales/en/transaction-request.json index a95f6ec96..5f3fa41e9 100644 --- a/i18n/locales/en/transaction-request.json +++ b/i18n/locales/en/transaction-request.json @@ -1,5 +1,15 @@ { - "payment-account-selection": { + "no-accounts": { + "action": { + "dismiss": "Dismiss" + }, + "info": { + "1": "No accounts found for the specified network.", + "2": "You must import an account before you can sign transaction requests." + }, + "title": "Transaction proposed" + }, + "payment": { "account-selector": "Select the account to use", "action": { "dismiss": "Dismiss", @@ -14,10 +24,6 @@ "no-origin-domain": "You opened the following payment request from an unknown origin." }, "uri-content": { - "any": "Any", - "pay": "Pay", - "to": "To", - "memo": "Memo", "message": "Message" }, "title": "Transaction proposed", @@ -34,15 +40,5 @@ "2": "If you trust this domain you can add this service to your list of trusted services.", "3": "You can view and edit your list of trusted services anytime in the application settings." } - }, - "no-accounts": { - "action": { - "dismiss": "Dismiss" - }, - "info": { - "1": "No accounts found for the specified network.", - "2": "You must import an account before you can sign transaction requests." - }, - "title": "Transaction proposed" } } diff --git a/src/TransactionRequest/components/PaymentAccountSelectionDialog.tsx b/src/TransactionRequest/components/PaymentAccountSelectionDialog.tsx index c928edf3e..a853346d3 100644 --- a/src/TransactionRequest/components/PaymentAccountSelectionDialog.tsx +++ b/src/TransactionRequest/components/PaymentAccountSelectionDialog.tsx @@ -169,22 +169,20 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps return ( - } + top={} actions={dialogActionsRef} > {signature ? ( - + The following transaction has been proposed by {{ originDomain }}. ) : ( - {t("transaction-request.payment-account-selection.warning")} + {t("transaction-request.payment.warning")} )} @@ -209,7 +207,7 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps )} - {t("transaction-request.payment-account-selection.account-selector")} + {t("transaction-request.payment.account-selector")} {props.accounts.length > 0 ? ( {asset.code === "XLM" - ? t("transaction-request.payment-account-selection.error.no-activated-accounts") - : t("transaction-request.payment-account-selection.error.no-accounts-with-trustline")} + ? t("transaction-request.payment.error.no-activated-accounts") + : t("transaction-request.payment.error.no-accounts-with-trustline")}
)}