From 4c925e5c1a3435e5516c79bda45d855ab3befecc Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 2 Jun 2020 13:20:22 +0200 Subject: [PATCH 01/55] Add more IPC message types for protocol handler --- shared/ipc.ts | 3 +++ shared/types/ipc.d.ts | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/shared/ipc.ts b/shared/ipc.ts index a46a7db03..3a0859d50 100644 --- a/shared/ipc.ts +++ b/shared/ipc.ts @@ -16,6 +16,9 @@ export const Messages: IPC.MessageType = { OpenLink: "OpenLink", DeepLinkURL: "DeepLinkURL", + IsDefaultProtocolClient: "IsDefaultProtocolClient", + IsDifferentHandlerInstalled: "IsDifferentHandlerInstalled", + SetAsDefaultProtocolClient: "SetAsDefaultProtocolClient", CheckUpdateAvailability: "CheckUpdateAvailability", StartUpdate: "StartUpdate", diff --git a/shared/types/ipc.d.ts b/shared/types/ipc.d.ts index 1e864ae94..916d44073 100644 --- a/shared/types/ipc.d.ts +++ b/shared/types/ipc.d.ts @@ -24,6 +24,9 @@ declare namespace IPC { OpenLink: "OpenLink" DeepLinkURL: "DeepLinkURL" + IsDefaultProtocolClient: "IsDefaultProtocolClient" + IsDifferentHandlerInstalled: "IsDifferentHandlerInstalled" + SetAsDefaultProtocolClient: "SetAsDefaultProtocolClient" CheckUpdateAvailability: "CheckUpdateAvailability" StartUpdate: "StartUpdate" @@ -62,6 +65,9 @@ declare namespace IPC { [Messages.OpenLink]: (href: string) => void [Messages.DeepLinkURL]: () => string + [Messages.IsDefaultProtocolClient]: () => boolean + [Messages.IsDifferentHandlerInstalled]: () => boolean + [Messages.SetAsDefaultProtocolClient]: () => boolean [Messages.CheckUpdateAvailability]: () => boolean [Messages.StartUpdate]: () => void From c9453e9703ba1a67a8b8c606532f2b213345ebf7 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 2 Jun 2020 13:21:16 +0200 Subject: [PATCH 02/55] Implement protocol handler ipc for electron --- electron/src/protocol-handler.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/electron/src/protocol-handler.ts b/electron/src/protocol-handler.ts index 3f8c5b308..0057999dc 100644 --- a/electron/src/protocol-handler.ts +++ b/electron/src/protocol-handler.ts @@ -1,6 +1,8 @@ import { app } from "electron" import events from "events" import { createMainWindow, getOpenWindows, trackWindow } from "./window" +import { expose } from "./ipc/_ipc" +import { Messages } from "./shared/ipc" const urlEventEmitter = new events.EventEmitter() const urlEventChannel = "deeplink:url" @@ -8,6 +10,19 @@ const urlEventChannel = "deeplink:url" let urlEventQueue: string[] = [] let isWindowReady = false +expose(Messages.IsDefaultProtocolClient, () => { + return app.isDefaultProtocolClient("web+stellar") +}) + +expose(Messages.IsDifferentHandlerInstalled, () => { + const name = app.getApplicationNameForProtocol("web+stellar://") // '://' is needed here + return Boolean(name) +}) + +expose(Messages.SetAsDefaultProtocolClient, () => { + return app.setAsDefaultProtocolClient("web+stellar") +}) + export function subscribe(subscribeCallback: (...args: any[]) => void) { urlEventEmitter.on(urlEventChannel, subscribeCallback) const unsubscribe = () => urlEventEmitter.removeListener(urlEventChannel, subscribeCallback) From fa630c9aed801e4bdcab1a496cbdaec1b88a4a96 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 2 Jun 2020 13:15:23 +0200 Subject: [PATCH 03/55] Add helper functions for protocol handler state --- src/Platform/protocol-handler.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Platform/protocol-handler.ts b/src/Platform/protocol-handler.ts index 6d005f550..0de3d098c 100644 --- a/src/Platform/protocol-handler.ts +++ b/src/Platform/protocol-handler.ts @@ -1,6 +1,18 @@ -import { subscribeToMessages } from "./ipc" +import { call, subscribeToMessages } from "./ipc" import { Messages } from "../../shared/ipc" export function subscribeToDeepLinkURLs(callback: (url: string) => void) { return subscribeToMessages(Messages.DeepLinkURL, callback) } + +export function isDefaultProtocolClient() { + return call(Messages.IsDefaultProtocolClient) +} + +export function isDifferentHandlerInstalled() { + return call(Messages.IsDifferentHandlerInstalled) +} + +export function setAsDefaultProtocolClient() { + return call(Messages.SetAsDefaultProtocolClient) +} From b50c6c0a6749ac3391e9ddb3785a9d756cac81aa Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 2 Jun 2020 13:14:03 +0200 Subject: [PATCH 04/55] Add ProtocolHandlerPermission component --- .../components/ProtocolHandlerPermission.tsx | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/Toasts/components/ProtocolHandlerPermission.tsx diff --git a/src/Toasts/components/ProtocolHandlerPermission.tsx b/src/Toasts/components/ProtocolHandlerPermission.tsx new file mode 100644 index 000000000..1f55f674c --- /dev/null +++ b/src/Toasts/components/ProtocolHandlerPermission.tsx @@ -0,0 +1,114 @@ +import React from "react" +import IconButton from "@material-ui/core/IconButton" +import Grow from "@material-ui/core/Grow" +import SnackbarContent from "@material-ui/core/SnackbarContent" +import Tooltip from "@material-ui/core/Tooltip" +import { useTheme } from "@material-ui/core/styles" +import CloseIcon from "@material-ui/icons/Close" +import CheckIcon from "@material-ui/icons/Check" +import { NotificationsContext } from "~App/contexts/notifications" +import { HorizontalLayout } from "~Layout/components/Box" +import { + isDefaultProtocolClient, + isDifferentHandlerInstalled, + setAsDefaultProtocolClient +} from "~platform/protocol-handler" + +function isNotificationDismissed() { + return localStorage.getItem("protocol-handler-notification-dismissed") !== null +} + +function setNotificationDismissed() { + localStorage.setItem("protocol-handler-notification-dismissed", "true") +} + +interface PermissionNotificationProps { + onHide: () => void + open: boolean +} + +const PermissionNotification = React.memo(function PermissionNotification(props: PermissionNotificationProps) { + const { onHide } = props + const { showNotification } = React.useContext(NotificationsContext) + const theme = useTheme() + + const requestPermission = React.useCallback(() => { + setAsDefaultProtocolClient().then(success => { + if (success) { + showNotification("success", "Successfully registered Solar as default handler.") + } else { + showNotification("error", "Could not register Solar as default handler.") + } + onHide() + }) + }, [showNotification, onHide]) + + const dismiss = React.useCallback(() => { + setNotificationDismissed() + onHide() + }, [onHide]) + + return ( + + + + Do you want Solar to handle interactive Stellar links on this computer (recommended)? + + + + + + + + + + + + + } + style={{ + display: "flex", + alignItems: "center", + background: "white", + color: theme.palette.text.primary, + cursor: "pointer", + flexGrow: 0, + justifyContent: "center" + }} + /> + + ) +}) + +function ProtocolHandlerPermission() { + const [showPermissionNotification, setShowPermissionNotification] = React.useState(false) + + React.useEffect(() => { + if (isNotificationDismissed()) { + return + } + + isDefaultProtocolClient().then(isDefault => { + if (isDefault) { + setShowPermissionNotification(false) + } else { + isDifferentHandlerInstalled().then(isDifferentInstalled => { + if (!isDifferentInstalled) { + setAsDefaultProtocolClient() + setShowPermissionNotification(false) + } else { + setShowPermissionNotification(true) + } + }) + } + }) + }, []) + + const hidePermissionNotification = React.useCallback(() => setShowPermissionNotification(false), []) + + return +} + +export default React.memo(ProtocolHandlerPermission) From 6f75fba4a50cea619fad16e36c99e612f5442173 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 2 Jun 2020 13:14:41 +0200 Subject: [PATCH 05/55] Show ProtocolHandlerPermission on desktop --- src/App/components/AccountListView.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/App/components/AccountListView.tsx b/src/App/components/AccountListView.tsx index 9210c5099..1c4b87d38 100644 --- a/src/App/components/AccountListView.tsx +++ b/src/App/components/AccountListView.tsx @@ -14,6 +14,7 @@ import MainTitle from "~Generic/components/MainTitle" import { useIsMobile, useRouter } from "~Generic/hooks/userinterface" import getUpdater from "~Platform/updater" import AppNotificationPermission from "~Toasts/components/AppNotificationPermission" +import ProtocolHandlerPermission from "~Toasts/components/ProtocolHandlerPermission" import { AccountsContext } from "../contexts/accounts" import { NotificationsContext, trackError } from "../contexts/notifications" import { SettingsContext } from "../contexts/settings" @@ -143,6 +144,9 @@ function AllAccountsPage() { onCreateTestnetAccount={() => router.history.push(routes.newAccount(true))} /> + {process.env.PLATFORM === "linux" || process.env.PLATFORM === "darwin" || process.env.PLATFORM === "win32" ? ( + + ) : null} Date: Tue, 2 Jun 2020 13:16:19 +0200 Subject: [PATCH 06/55] Add app settings item for protocol handler --- src/AppSettings/components/AppSettings.tsx | 13 +++++++++++- src/AppSettings/components/Settings.tsx | 24 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/AppSettings/components/AppSettings.tsx b/src/AppSettings/components/AppSettings.tsx index 0cb5e25b0..4a50eb018 100644 --- a/src/AppSettings/components/AppSettings.tsx +++ b/src/AppSettings/components/AppSettings.tsx @@ -8,6 +8,7 @@ import * as routes from "~App/routes" import { useIsMobile, useRouter } from "~Generic/hooks/userinterface" import { matchesRoute } from "~Generic/lib/routes" import Carousel from "~Layout/components/Carousel" +import { isDefaultProtocolClient, setAsDefaultProtocolClient } from "~platform/protocol-handler" import ManageTrustedServicesDialog from "./ManageTrustedServicesDialog" import { BiometricLockSetting, @@ -15,7 +16,8 @@ import { LanguageSetting, MultiSigSetting, TestnetSetting, - TrustedServicesSetting + TrustedServicesSetting, + ProtocolHandlerSetting } from "./Settings" const SettingsDialogs = React.memo(function SettingsDialogs() { @@ -30,6 +32,8 @@ function AppSettings() { const router = useRouter() const { i18n } = useTranslation() + const [isDefaultHandler, setIsDefaultHandler] = React.useState(false) + const showSettingsOverview = matchesRoute(router.location.pathname, routes.settings(), true) const { accounts } = React.useContext(AccountsContext) @@ -53,6 +57,12 @@ function AppSettings() { [i18n, settings] ) + isDefaultProtocolClient().then(setIsDefaultHandler) + + const setDefaultClient = React.useCallback(() => { + setAsDefaultProtocolClient().then(success => setIsDefaultHandler(success)) + }, [setIsDefaultHandler]) + return ( @@ -73,6 +83,7 @@ function AppSettings() { /> + {trustedServicesEnabled ? : undefined} diff --git a/src/AppSettings/components/Settings.tsx b/src/AppSettings/components/Settings.tsx index b95371eaf..ea4253b7f 100644 --- a/src/AppSettings/components/Settings.tsx +++ b/src/AppSettings/components/Settings.tsx @@ -203,3 +203,27 @@ export const TrustedServicesSetting = React.memo(function TrustedServicesSetting /> ) }) + +interface ProtocolHandlerSettingProps { + onClick: () => void + isDefaultHandler: boolean +} + +export const ProtocolHandlerSetting = React.memo(function ProtocolHandlerSetting(props: ProtocolHandlerSettingProps) { + const classes = useSettingsStyles(props) + const { t } = useTranslation() + + return ( + } + onClick={props.onClick} + primaryText={ + props.isDefaultHandler ? "Solar is handling all Stellar links" : "Make Solar handle all Stellar URIs" + } + secondaryText={ + props.isDefaultHandler ? "" : "Click to make Solar the default handler for Stellar requests on this computer" + } + /> + ) +}) From 7c19144fbf20a3f776588b12543c8a5511634ec7 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 2 Jun 2020 13:16:51 +0200 Subject: [PATCH 07/55] Implement protocol handler ipc for web --- src/Platform/ipc/web.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Platform/ipc/web.ts b/src/Platform/ipc/web.ts index fef6cc8b8..7e7a7ca4c 100644 --- a/src/Platform/ipc/web.ts +++ b/src/Platform/ipc/web.ts @@ -63,6 +63,16 @@ callHandlers[Messages.ShowNotification] = (localNotification: LocalNotification) }) } +let isDefault = false +let differentHandler = true +callHandlers[Messages.IsDifferentHandlerInstalled] = () => differentHandler +callHandlers[Messages.IsDefaultProtocolClient] = () => isDefault +callHandlers[Messages.SetAsDefaultProtocolClient] = () => { + isDefault = true + differentHandler = false + return true +} + const defaultTestingKeys: KeysData = { "1": { metadata: { From 1fbceda9bd6ddd604826053b46b40082b880e532 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 2 Jun 2020 16:13:51 +0200 Subject: [PATCH 08/55] Replace hardcoded strings --- i18n/locales/en/app-settings.json | 9 +++++++++ i18n/locales/en/app.json | 9 +++++++++ src/AppSettings/components/Settings.tsx | 11 ++++++----- .../components/ProtocolHandlerPermission.tsx | 14 ++++++++------ 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/i18n/locales/en/app-settings.json b/i18n/locales/en/app-settings.json index 92e832966..8fff6935f 100644 --- a/i18n/locales/en/app-settings.json +++ b/i18n/locales/en/app-settings.json @@ -44,6 +44,15 @@ } } }, + "protocol-handler": { + "text": { + "primary": "Handle Stellar protocol requests", + "secondary": { + "default": "Solar is handling all Stellar links", + "non-default": "Make Solar handle all Stellar URIs" + } + } + }, "testnet": { "text": { "primary": "Show Testnet Accounts", diff --git a/i18n/locales/en/app.json b/i18n/locales/en/app.json index 686e44532..3fac74f30 100644 --- a/i18n/locales/en/app.json +++ b/i18n/locales/en/app.json @@ -63,6 +63,15 @@ "text": "Solar will now show notifications" }, "message": "Enable app notifications" + }, + "protocol-handler": { + "error": "Could not register Solar as default handler.", + "message": "Do you want Solar to handle interactive Stellar links on this computer (recommended)?", + "success": "Successfully registered Solar as default handler.", + "tooltip": { + "dismiss": "Dismiss", + "install": "Install" + } } } }, diff --git a/src/AppSettings/components/Settings.tsx b/src/AppSettings/components/Settings.tsx index ea4253b7f..8d89f7230 100644 --- a/src/AppSettings/components/Settings.tsx +++ b/src/AppSettings/components/Settings.tsx @@ -8,6 +8,7 @@ import { makeStyles } from "@material-ui/core/styles" import ArrowRightIcon from "@material-ui/icons/KeyboardArrowRight" import FingerprintIcon from "@material-ui/icons/Fingerprint" import GroupIcon from "@material-ui/icons/Group" +import ProtocolHandlerIcon from "@material-ui/icons/AddCircleOutline" import LanguageIcon from "@material-ui/icons/Language" import MessageIcon from "@material-ui/icons/Message" import TestnetIcon from "@material-ui/icons/MoneyOff" @@ -216,13 +217,13 @@ export const ProtocolHandlerSetting = React.memo(function ProtocolHandlerSetting return ( } + icon={} onClick={props.onClick} - primaryText={ - props.isDefaultHandler ? "Solar is handling all Stellar links" : "Make Solar handle all Stellar URIs" - } + primaryText={t("app-settings.settings.protocol-handler.text.primary")} secondaryText={ - props.isDefaultHandler ? "" : "Click to make Solar the default handler for Stellar requests on this computer" + props.isDefaultHandler + ? t("app-settings.settings.protocol-handler.text.secondary.default") + : t("app-settings.settings.protocol-handler.text.secondary.non-default") } /> ) diff --git a/src/Toasts/components/ProtocolHandlerPermission.tsx b/src/Toasts/components/ProtocolHandlerPermission.tsx index 1f55f674c..f69a420e8 100644 --- a/src/Toasts/components/ProtocolHandlerPermission.tsx +++ b/src/Toasts/components/ProtocolHandlerPermission.tsx @@ -1,4 +1,5 @@ import React from "react" +import { useTranslation } from "react-i18next" import IconButton from "@material-ui/core/IconButton" import Grow from "@material-ui/core/Grow" import SnackbarContent from "@material-ui/core/SnackbarContent" @@ -31,17 +32,18 @@ const PermissionNotification = React.memo(function PermissionNotification(props: const { onHide } = props const { showNotification } = React.useContext(NotificationsContext) const theme = useTheme() + const { t } = useTranslation() const requestPermission = React.useCallback(() => { setAsDefaultProtocolClient().then(success => { if (success) { - showNotification("success", "Successfully registered Solar as default handler.") + showNotification("success", t("app.notification.permission.protocol-handler.success")) } else { - showNotification("error", "Could not register Solar as default handler.") + showNotification("error", t("app.notification.permission.protocol-handler.error")) } onHide() }) - }, [showNotification, onHide]) + }, [onHide, showNotification, t]) const dismiss = React.useCallback(() => { setNotificationDismissed() @@ -54,14 +56,14 @@ const PermissionNotification = React.memo(function PermissionNotification(props: message={ - Do you want Solar to handle interactive Stellar links on this computer (recommended)? + {t("app.notification.permission.protocol-handler.message")} - + - + From 0d9082d34f5974c54793a14a34b78e65c4df4879 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 8 Jun 2020 11:01:11 +0200 Subject: [PATCH 09/55] Disable installation as protocol client --- electron/src/bootstrap.ts | 3 --- electron/src/protocol-handler.ts | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/electron/src/bootstrap.ts b/electron/src/bootstrap.ts index 792e8a2e3..de6ba0e46 100644 --- a/electron/src/bootstrap.ts +++ b/electron/src/bootstrap.ts @@ -5,6 +5,3 @@ app.name = "Solar Wallet" // Needs to match the value in electron-build.yml app.setAppUserModelId("io.solarwallet.app") - -// Disabled until we actually ship SEP-7 support -// app.setAsDefaultProtocolClient("web+stellar") diff --git a/electron/src/protocol-handler.ts b/electron/src/protocol-handler.ts index 0057999dc..2d07517f0 100644 --- a/electron/src/protocol-handler.ts +++ b/electron/src/protocol-handler.ts @@ -20,7 +20,9 @@ expose(Messages.IsDifferentHandlerInstalled, () => { }) expose(Messages.SetAsDefaultProtocolClient, () => { - return app.setAsDefaultProtocolClient("web+stellar") + // Disabled until we actually ship SEP-7 support + // return app.setAsDefaultProtocolClient("web+stellar") + return false }) export function subscribe(subscribeCallback: (...args: any[]) => void) { From f7cae4e64ceea511cb69c135d60f691fd29cc448 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 8 Jun 2020 11:01:52 +0200 Subject: [PATCH 10/55] Hide protocol handler setting with env var flag --- src/AppSettings/components/AppSettings.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/AppSettings/components/AppSettings.tsx b/src/AppSettings/components/AppSettings.tsx index 4a50eb018..7e826e9f0 100644 --- a/src/AppSettings/components/AppSettings.tsx +++ b/src/AppSettings/components/AppSettings.tsx @@ -39,6 +39,7 @@ function AppSettings() { const { accounts } = React.useContext(AccountsContext) const settings = React.useContext(SettingsContext) const trustedServicesEnabled = process.env.TRUSTED_SERVICES && process.env.TRUSTED_SERVICES === "enabled" + const protocolHandlerEnabled = process.env.REQUEST_HANDLER && process.env.REQUEST_HANDLER === "enabled" const getEffectiveLanguage = (lang: L, fallback: F) => { return availableLanguages.indexOf(lang as any) > -1 ? lang : fallback @@ -83,7 +84,11 @@ function AppSettings() { /> - + {protocolHandlerEnabled ? ( + + ) : ( + undefined + )} {trustedServicesEnabled ? : undefined} From 3ce61a10051e7ff89137c36158ed0b2862d60b0f Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 23 Nov 2020 12:56:04 +0100 Subject: [PATCH 11/55] 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 12/55] 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 13/55] 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 14/55] 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 15/55] 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 16/55] 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 17/55] 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 18/55] 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 19/55] 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 20/55] 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 21/55] 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 22/55] 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 23/55] 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 24/55] 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 25/55] 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 26/55] 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 27/55] 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 28/55] 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 29/55] 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 30/55] 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 31/55] 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 32/55] 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 33/55] 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")}
)}
From fe1abcab41502e6337bfc8d33cfc8ba899400a69 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 18 Jan 2021 16:24:00 +0100 Subject: [PATCH 34/55] Create TransactionRequestReviewDialog --- .../TransactionRequestReviewDialog.tsx | 312 ++++++++++++++++++ .../components/TransactionReviewDialog.tsx | 4 +- 2 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 src/Transaction/components/TransactionRequestReviewDialog.tsx diff --git a/src/Transaction/components/TransactionRequestReviewDialog.tsx b/src/Transaction/components/TransactionRequestReviewDialog.tsx new file mode 100644 index 000000000..4f6f9e71e --- /dev/null +++ b/src/Transaction/components/TransactionRequestReviewDialog.tsx @@ -0,0 +1,312 @@ +import BigNumber from "big.js" +import React from "react" +import { Trans, useTranslation } from "react-i18next" +import { Operation, Server, Transaction } from "stellar-sdk" +import TransactionSender, { SendTransaction } from "./TransactionSender" +import Box from "@material-ui/core/Box" +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 { TransactionStellarUri } from "@stellarguard/stellar-uri" +import AccountSelectionList from "~Account/components/AccountSelectionList" +import { Account, AccountsContext } from "~App/contexts/accounts" +import { breakpoints, warningColor } from "~App/theme" +import { ActionButton, DialogActionsBox } from "~Generic/components/DialogActions" +import MainTitle from "~Generic/components/MainTitle" +import TestnetBadge from "~Generic/components/TestnetBadge" +import { useLiveAccountDataSet } from "~Generic/hooks/stellar-subscriptions" +import { AccountData } from "~Generic/lib/account" +import DialogBody from "~Layout/components/DialogBody" +import { useTransactionTitle } from "~TransactionReview/components/TransactionReviewDialog" +import TransactionSummary from "~TransactionReview/components/TransactionSummary" + +function getSelectableAccounts(transaction: Transaction, accounts: Account[], accountsData: AccountData[]) { + return accounts.filter(acc => { + const accountData = accountsData.find(data => data.account_id === acc.publicKey) + if (!accountData) return false + + const paymentOperations = transaction.operations.filter( + operations => operations.type === "payment" + ) as Operation.Payment[] + if (paymentOperations.length > 0) { + const allOperationsViable = paymentOperations.every(operation => { + // check if account holds trustline for every asset used in payment operations + const asset = operation.asset + if (asset.isNative()) { + return true + } else { + return accountData.balances.some( + (balance: any) => balance.asset_code === asset.code && balance.asset_issuer === asset.issuer + ) + } + }) + if (!allOperationsViable) return false + } + + const changeTrustOperations = transaction.operations.filter( + operation => operation.type === "changeTrust" + ) as Operation.ChangeTrust[] + if (changeTrustOperations.length > 0) { + const allOperationsViable = changeTrustOperations.every(operation => { + const asset = operation.line + if (BigNumber(operation.limit).eq(0)) { + // remove-trust operation + if ( + !accountData.balances.some( + (balance: any) => balance.asset_code === asset.code && balance.asset_issuer === asset.issuer + ) + ) { + return false + } + } else { + // add-trust operation + if ( + accountData.balances.some( + (balance: any) => balance.asset_code === asset.code && balance.asset_issuer === asset.issuer + ) + ) { + return false + } + } + return true + }) + + if (!allOperationsViable) return false + } + + return true + }) +} + +const useStyles = makeStyles(() => ({ + root: { + display: "flex", + flexDirection: "column", + padding: "12px 0 0" + }, + 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 TransactionRequestReviewDialogProps { + accounts: Account[] + accountsData: AccountData[] + horizon: Server + onClose: () => void + onAccountChange: (account: Account) => void + selectedAccount: Account | null + sendTransaction: SendTransaction + txStellarUri: TransactionStellarUri +} + +function TransactionRequestReviewDialog(props: TransactionRequestReviewDialogProps) { + const { accounts, accountsData, horizon, onClose, onAccountChange, selectedAccount, sendTransaction } = props + const { msg, originDomain, pubkey, signature, isTestNetwork: testnet } = props.txStellarUri + const transaction = React.useMemo(() => props.txStellarUri.getTransaction(), [props.txStellarUri]) + const replacements = React.useMemo(() => props.txStellarUri.getReplacements(), [props.txStellarUri]) + const sourceAccountReplacement = React.useMemo( + () => replacements.find(replacement => replacement.path === "sourceAccount"), + [replacements] + ) + + const classes = useStyles() + const { t } = useTranslation() + const getTitle = useTransactionTitle() + + const selectableAccounts = React.useMemo(() => { + if (pubkey) { + // pubkey parameter specifies public key of the account that should sign + const requiredAccount = accounts.find(acc => acc.publicKey === pubkey) + return requiredAccount ? [requiredAccount] : [] + } else { + return getSelectableAccounts(transaction, accounts, accountsData) + } + }, [accounts, accountsData, transaction, pubkey]) + + const getNewSeqNumber = React.useCallback( + async account => { + const fetchedSeqNum = await horizon.loadAccount(account).then(acc => acc.sequence) + const newSeqNum = BigNumber(fetchedSeqNum) + .add(1) + .toString() + return newSeqNum + }, + [horizon] + ) + + const onSelect = React.useCallback(async () => { + const filledReplacements: { [key: string]: any } = {} + const seqNumReplacement = replacements.find(replacement => replacement.id === "seqNum") + if (seqNumReplacement) { + const sourceAccount = sourceAccountReplacement && selectedAccount ? selectedAccount.publicKey : transaction.source + const newSeqNum = await getNewSeqNumber(sourceAccount) + filledReplacements[seqNumReplacement.id] = newSeqNum + } + if (sourceAccountReplacement && selectedAccount) { + const sourceAccount = sourceAccountReplacement && selectedAccount ? selectedAccount.publicKey : transaction.source + filledReplacements[sourceAccountReplacement.id] = selectedAccount.publicKey + + if (!seqNumReplacement) { + // artificially add seqNum replacement to facilitate replacing seq number for new source account + const artificialSeqNumReplacement = { id: "SEQ", path: "seqNum", hint: "sequence number" } + props.txStellarUri.addReplacement(artificialSeqNumReplacement) + const newSeqNum = await getNewSeqNumber(sourceAccount) + filledReplacements[artificialSeqNumReplacement.id] = newSeqNum + } + } + + const newTx = props.txStellarUri.replace(filledReplacements).getTransaction() + sendTransaction(newTx) + }, [ + getNewSeqNumber, + replacements, + props.txStellarUri, + sourceAccountReplacement, + sendTransaction, + selectedAccount, + transaction.source + ]) + + return ( + + {t("transaction-request.payment-account-selection.title")} + {testnet ? : null} + + } + /> + } + 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")} + + + )} + {msg && ( + + Message: + {msg} + + )} + + {getTitle(transaction)} + + + {sourceAccountReplacement ? ( + selectableAccounts.length > 0 ? ( + <> + + Select the source account
+
+ {sourceAccountReplacement.hint && ( + + Hint: {sourceAccountReplacement.hint} + + )} + + + ) : pubkey ? ( + + The transaction request specified '{pubkey}' as the target signer but this account is not imported. + + ) : ( + + No eligible account found. + + ) + ) : ( + <> + + Select the account that will sign the transaction
+
+ + + )} +
+
+ ) +} + +interface ConnectedTransactionRequestReviewDialogProps { + onClose: () => void + txStellarUri: TransactionStellarUri +} + +function ConnectedTransferRequestReviewDialog(props: ConnectedTransactionRequestReviewDialogProps) { + const { accounts } = React.useContext(AccountsContext) + const testnet = props.txStellarUri.isTestNetwork + const selectableAccounts = React.useMemo(() => accounts.filter(acc => acc.testnet === testnet), [accounts, testnet]) + const accountsData = useLiveAccountDataSet( + selectableAccounts.map(acc => acc.publicKey), + testnet + ) + const [selectedAccount, setSelectedAccount] = React.useState(null) + + return ( + // FIXME fix default account of TransactionSender + + {({ horizon, sendTransaction }) => ( + + )} + + ) +} + +export default React.memo(ConnectedTransferRequestReviewDialog) diff --git a/src/TransactionReview/components/TransactionReviewDialog.tsx b/src/TransactionReview/components/TransactionReviewDialog.tsx index 120ba71e5..9f90bfc38 100644 --- a/src/TransactionReview/components/TransactionReviewDialog.tsx +++ b/src/TransactionReview/components/TransactionReviewDialog.tsx @@ -27,7 +27,7 @@ function isOfferDeletionOperation(operation: Operation) { ) } -function useTitle() { +export function useTransactionTitle() { const getOperationTitle = useOperationTitle() const { t } = useTranslation() @@ -65,7 +65,7 @@ interface TransactionReviewDialogBodyProps { export function TransactionReviewDialogBody(props: TransactionReviewDialogBodyProps) { const dialogActionsRef = useDialogActions() const isSmallScreen = useIsMobile() - const getTitle = useTitle() + const getTitle = useTransactionTitle() const titleContent = React.useMemo( () => ( From d6ad31d0b0df919aa84fb2831d3eb1f40073013f Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 18 Jan 2021 16:24:15 +0100 Subject: [PATCH 35/55] Add `fullWidth` prop to TransactionSummary --- src/TransactionReview/components/TransactionSummary.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/TransactionReview/components/TransactionSummary.tsx b/src/TransactionReview/components/TransactionSummary.tsx index 436445871..6f3afa62b 100644 --- a/src/TransactionReview/components/TransactionSummary.tsx +++ b/src/TransactionReview/components/TransactionSummary.tsx @@ -54,6 +54,7 @@ interface DefaultTransactionSummaryProps { accountData: AccountData canSubmit: boolean isDangerousSignatureRequest?: boolean + fullWidth?: boolean showHash?: boolean showSigners?: boolean showSource?: boolean @@ -90,7 +91,9 @@ function DefaultTransactionSummary(props: DefaultTransactionSummaryProps) { const isAccountCreation = props.transaction.operations.some(op => op.type === "createAccount") const isWideScreen = useMediaQuery("(min-width:900px)") - const widthStyling = isWideScreen ? { maxWidth: 700, minWidth: 400 } : { minWidth: "66vw" } + const widthStyling = isWideScreen + ? { maxWidth: props.fullWidth ? "unset" : 700, minWidth: 400 } + : { minWidth: "66vw" } const transaction = props.transaction as TransactionWithUndocumentedProps const transactionHash = React.useMemo(() => { @@ -213,6 +216,7 @@ function WebAuthTransactionSummary(props: WebAuthTransactionSummaryProps) { interface TransactionSummaryProps { account: Account | null canSubmit: boolean + fullWidth?: boolean showHash?: boolean showSource?: boolean signatureRequest?: SignatureRequest From af9d4bd899b9b13c23c3f4046bc9e69368bdb975 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 18 Jan 2021 16:24:41 +0100 Subject: [PATCH 36/55] Rename PaymentAccountSelectionDialog --- .../components/PaymentRequestReviewDialog.tsx | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 src/Transaction/components/PaymentRequestReviewDialog.tsx diff --git a/src/Transaction/components/PaymentRequestReviewDialog.tsx b/src/Transaction/components/PaymentRequestReviewDialog.tsx new file mode 100644 index 000000000..5de27fb2f --- /dev/null +++ b/src/Transaction/components/PaymentRequestReviewDialog.tsx @@ -0,0 +1,261 @@ +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" +import TestnetBadge from "~Generic/components/TestnetBadge" + +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 PaymentRequestReviewDialogProps { + payStellarUri: PayStellarUri + onClose: () => void +} + +function PaymentRequestReviewDialog(props: PaymentRequestReviewDialogProps) { + 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 ( + <> + + {t("transaction-request.payment-account-selection.title")} + {testnet ? : null} + + } + /> + } + 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 PaymentRequestReviewDialog From a2b9807a9626e0e9ad19cfb3f7069cfb0fecb274 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 18 Jan 2021 16:25:31 +0100 Subject: [PATCH 37/55] Rename TransactionRequestHandler --- src/App/bootstrap/app-stage2.tsx | 4 +- .../components/PaymentRequestReviewDialog.tsx | 261 ------------------ ...log.tsx => PaymentRequestReviewDialog.tsx} | 12 +- ...questHandler.tsx => StellarUriHandler.tsx} | 24 +- .../TransactionRequestReviewDialog.tsx | 2 +- 5 files changed, 26 insertions(+), 277 deletions(-) delete mode 100644 src/Transaction/components/PaymentRequestReviewDialog.tsx rename src/TransactionRequest/components/{PaymentAccountSelectionDialog.tsx => PaymentRequestReviewDialog.tsx} (95%) rename src/TransactionRequest/components/{TransactionRequestHandler.tsx => StellarUriHandler.tsx} (74%) rename src/{Transaction => TransactionRequest}/components/TransactionRequestReviewDialog.tsx (99%) diff --git a/src/App/bootstrap/app-stage2.tsx b/src/App/bootstrap/app-stage2.tsx index 83a55574f..190f44e7d 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 "~TransactionRequest/components/TransactionRequestHandler" +import StellarUriHandler from "~TransactionRequest/components/StellarUriHandler" import AllAccountsPage from "../components/AccountListView" import AndroidBackButton from "../components/AndroidBackButton" import DesktopNotifications from "../components/DesktopNotifications" @@ -73,7 +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/PaymentRequestReviewDialog.tsx b/src/Transaction/components/PaymentRequestReviewDialog.tsx deleted file mode 100644 index 5de27fb2f..000000000 --- a/src/Transaction/components/PaymentRequestReviewDialog.tsx +++ /dev/null @@ -1,261 +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" -import TestnetBadge from "~Generic/components/TestnetBadge" - -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 PaymentRequestReviewDialogProps { - payStellarUri: PayStellarUri - onClose: () => void -} - -function PaymentRequestReviewDialog(props: PaymentRequestReviewDialogProps) { - 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 ( - <> - - {t("transaction-request.payment-account-selection.title")} - {testnet ? : null} - - } - /> - } - 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 PaymentRequestReviewDialog diff --git a/src/TransactionRequest/components/PaymentAccountSelectionDialog.tsx b/src/TransactionRequest/components/PaymentRequestReviewDialog.tsx similarity index 95% rename from src/TransactionRequest/components/PaymentAccountSelectionDialog.tsx rename to src/TransactionRequest/components/PaymentRequestReviewDialog.tsx index a853346d3..a0c693bd9 100644 --- a/src/TransactionRequest/components/PaymentAccountSelectionDialog.tsx +++ b/src/TransactionRequest/components/PaymentRequestReviewDialog.tsx @@ -118,7 +118,7 @@ const useStyles = makeStyles(() => ({ } })) -interface PaymentAccountSelectionDialogProps { +interface PaymentRequestReviewDialogProps { accounts: Account[] horizon: Server onAccountChange: (account: Account) => void @@ -128,7 +128,7 @@ interface PaymentAccountSelectionDialogProps { sendTransaction: SendTransaction } -function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps) { +function PaymentRequestReviewDialog(props: PaymentRequestReviewDialogProps) { const { onClose, onAccountChange } = props const { amount, @@ -228,8 +228,8 @@ function PaymentAccountSelectionDialog(props: PaymentAccountSelectionDialogProps ) } -function ConnectedPaymentAccountSelectionDialog( - props: Pick +function ConnectedPaymentRequestReviewDialog( + props: Pick ) { const { accounts } = React.useContext(AccountsContext) const testnet = props.payStellarUri.isTestNetwork @@ -241,7 +241,7 @@ function ConnectedPaymentAccountSelectionDialog( return accountsForNetwork.length > 0 ? ( {({ horizon, sendTransaction }) => ( - ) -function TransactionRequestHandler() { +function StellarUriHandler() { const { uri, clearURI } = React.useContext(TransactionRequestContext) const { trustedServices, setSetting } = React.useContext(SettingsContext) const [closedStellarURI, setClosedStellarURI] = React.useState(null) @@ -59,12 +60,21 @@ function TransactionRequestHandler() { return ( - + ) - } + } else { + const txStellarUri = renderedURI as TransactionStellarUri + const onClose = () => { + closeDialog() + } - return null + return ( + + + + ) + } } -export default React.memo(TransactionRequestHandler) +export default React.memo(StellarUriHandler) diff --git a/src/Transaction/components/TransactionRequestReviewDialog.tsx b/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx similarity index 99% rename from src/Transaction/components/TransactionRequestReviewDialog.tsx rename to src/TransactionRequest/components/TransactionRequestReviewDialog.tsx index 4f6f9e71e..d94f55d78 100644 --- a/src/Transaction/components/TransactionRequestReviewDialog.tsx +++ b/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx @@ -2,7 +2,7 @@ import BigNumber from "big.js" import React from "react" import { Trans, useTranslation } from "react-i18next" import { Operation, Server, Transaction } from "stellar-sdk" -import TransactionSender, { SendTransaction } from "./TransactionSender" +import TransactionSender, { SendTransaction } from "../../Transaction/components/TransactionSender" import Box from "@material-ui/core/Box" import makeStyles from "@material-ui/core/styles/makeStyles" import Typography from "@material-ui/core/Typography" From 8659c3b5a2e066243f7671621ccf233bb0f46a93 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 18 Jan 2021 16:25:52 +0100 Subject: [PATCH 38/55] Check transaction during uri verification --- src/App/contexts/transactionRequest.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/App/contexts/transactionRequest.tsx b/src/App/contexts/transactionRequest.tsx index 08d4ef08d..3d7087ed2 100644 --- a/src/App/contexts/transactionRequest.tsx +++ b/src/App/contexts/transactionRequest.tsx @@ -1,5 +1,5 @@ import React from "react" -import { StellarUri, StellarUriType } from "@stellarguard/stellar-uri" +import { StellarUri, StellarUriType, TransactionStellarUri } from "@stellarguard/stellar-uri" import { CustomError } from "~Generic/lib/errors" import { subscribeToDeepLinkURLs } from "~Platform/protocol-handler" import { verifyTransactionRequest } from "~Transaction/lib/stellar-uri" @@ -31,6 +31,13 @@ export function TransactionRequestProvider(props: Props) { const verifyStellarURI = React.useCallback(async (incomingURI: string) => { try { const parsedURI = await verifyTransactionRequest(incomingURI, { allowUnsafeTestnetURIs }) + + if (parsedURI.operation === StellarUriType.Transaction) { + // check if contained transaction is valid + const txURI = parsedURI as TransactionStellarUri + txURI.getTransaction() + } + setURI(parsedURI) } catch (error) { trackError(error) From fe3dfad8331461c5b9fa84c03c667c46e78cfb12 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 18 Jan 2021 16:26:33 +0100 Subject: [PATCH 39/55] Add padding around AccountSelectionList Setting padding-right and -left to zero removes the shadows and looks weird --- src/Account/components/AccountSelectionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Account/components/AccountSelectionList.tsx b/src/Account/components/AccountSelectionList.tsx index 80008692f..953457839 100644 --- a/src/Account/components/AccountSelectionList.tsx +++ b/src/Account/components/AccountSelectionList.tsx @@ -31,7 +31,7 @@ function AccountSelectionList(props: AccountSelectionListProps) { } return ( - + {props.accounts.map((account, index) => ( Date: Mon, 18 Jan 2021 16:51:00 +0100 Subject: [PATCH 40/55] Refactor selectable accounts --- .../TransactionRequestReviewDialog.tsx | 57 +++++++------------ 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx b/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx index d94f55d78..c444fb874 100644 --- a/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx +++ b/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx @@ -21,6 +21,7 @@ import { AccountData } from "~Generic/lib/account" import DialogBody from "~Layout/components/DialogBody" import { useTransactionTitle } from "~TransactionReview/components/TransactionReviewDialog" import TransactionSummary from "~TransactionReview/components/TransactionSummary" +import { findMatchingBalanceLine } from "~Generic/lib/stellar" function getSelectableAccounts(transaction: Transaction, accounts: Account[], accountsData: AccountData[]) { return accounts.filter(acc => { @@ -30,50 +31,30 @@ function getSelectableAccounts(transaction: Transaction, accounts: Account[], ac const paymentOperations = transaction.operations.filter( operations => operations.type === "payment" ) as Operation.Payment[] - if (paymentOperations.length > 0) { - const allOperationsViable = paymentOperations.every(operation => { - // check if account holds trustline for every asset used in payment operations - const asset = operation.asset - if (asset.isNative()) { - return true - } else { - return accountData.balances.some( - (balance: any) => balance.asset_code === asset.code && balance.asset_issuer === asset.issuer - ) - } - }) - if (!allOperationsViable) return false + for (const operation of paymentOperations) { + const asset = operation.asset + // check if account holds trustline for every asset used in payment operations + if (!asset.isNative() && !findMatchingBalanceLine(accountData.balances, asset)) { + return false + } } const changeTrustOperations = transaction.operations.filter( operation => operation.type === "changeTrust" ) as Operation.ChangeTrust[] - if (changeTrustOperations.length > 0) { - const allOperationsViable = changeTrustOperations.every(operation => { - const asset = operation.line - if (BigNumber(operation.limit).eq(0)) { - // remove-trust operation - if ( - !accountData.balances.some( - (balance: any) => balance.asset_code === asset.code && balance.asset_issuer === asset.issuer - ) - ) { - return false - } - } else { - // add-trust operation - if ( - accountData.balances.some( - (balance: any) => balance.asset_code === asset.code && balance.asset_issuer === asset.issuer - ) - ) { - return false - } + for (const operation of changeTrustOperations) { + const asset = operation.line + if (BigNumber(operation.limit).eq(0)) { + // check if account has the specified trustline to remove + if (!findMatchingBalanceLine(accountData.balances, asset)) { + return false } - return true - }) - - if (!allOperationsViable) return false + } else { + // check if account does not already have a trustline for this asset + if (findMatchingBalanceLine(accountData.balances, asset)) { + return false + } + } } return true From 34f47e1f974260c0e5d9f90c7af20c67076ca68a Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 20 Jan 2021 10:36:49 +0100 Subject: [PATCH 41/55] Remove selectable accounts check --- .../TransactionRequestReviewDialog.tsx | 63 +++---------------- 1 file changed, 8 insertions(+), 55 deletions(-) diff --git a/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx b/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx index c444fb874..8d243677b 100644 --- a/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx +++ b/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx @@ -1,8 +1,3 @@ -import BigNumber from "big.js" -import React from "react" -import { Trans, useTranslation } from "react-i18next" -import { Operation, Server, Transaction } from "stellar-sdk" -import TransactionSender, { SendTransaction } from "../../Transaction/components/TransactionSender" import Box from "@material-ui/core/Box" import makeStyles from "@material-ui/core/styles/makeStyles" import Typography from "@material-ui/core/Typography" @@ -10,56 +5,20 @@ import CancelIcon from "@material-ui/icons/Cancel" import SelectIcon from "@material-ui/icons/Check" import WarningIcon from "@material-ui/icons/Warning" import { TransactionStellarUri } from "@stellarguard/stellar-uri" +import BigNumber from "big.js" +import React from "react" +import { Trans, useTranslation } from "react-i18next" +import { Server } from "stellar-sdk" import AccountSelectionList from "~Account/components/AccountSelectionList" import { Account, AccountsContext } from "~App/contexts/accounts" import { breakpoints, warningColor } from "~App/theme" import { ActionButton, DialogActionsBox } from "~Generic/components/DialogActions" import MainTitle from "~Generic/components/MainTitle" import TestnetBadge from "~Generic/components/TestnetBadge" -import { useLiveAccountDataSet } from "~Generic/hooks/stellar-subscriptions" -import { AccountData } from "~Generic/lib/account" import DialogBody from "~Layout/components/DialogBody" import { useTransactionTitle } from "~TransactionReview/components/TransactionReviewDialog" import TransactionSummary from "~TransactionReview/components/TransactionSummary" -import { findMatchingBalanceLine } from "~Generic/lib/stellar" - -function getSelectableAccounts(transaction: Transaction, accounts: Account[], accountsData: AccountData[]) { - return accounts.filter(acc => { - const accountData = accountsData.find(data => data.account_id === acc.publicKey) - if (!accountData) return false - - const paymentOperations = transaction.operations.filter( - operations => operations.type === "payment" - ) as Operation.Payment[] - for (const operation of paymentOperations) { - const asset = operation.asset - // check if account holds trustline for every asset used in payment operations - if (!asset.isNative() && !findMatchingBalanceLine(accountData.balances, asset)) { - return false - } - } - - const changeTrustOperations = transaction.operations.filter( - operation => operation.type === "changeTrust" - ) as Operation.ChangeTrust[] - for (const operation of changeTrustOperations) { - const asset = operation.line - if (BigNumber(operation.limit).eq(0)) { - // check if account has the specified trustline to remove - if (!findMatchingBalanceLine(accountData.balances, asset)) { - return false - } - } else { - // check if account does not already have a trustline for this asset - if (findMatchingBalanceLine(accountData.balances, asset)) { - return false - } - } - } - - return true - }) -} +import TransactionSender, { SendTransaction } from "../../Transaction/components/TransactionSender" const useStyles = makeStyles(() => ({ root: { @@ -88,7 +47,6 @@ const useStyles = makeStyles(() => ({ interface TransactionRequestReviewDialogProps { accounts: Account[] - accountsData: AccountData[] horizon: Server onClose: () => void onAccountChange: (account: Account) => void @@ -98,7 +56,7 @@ interface TransactionRequestReviewDialogProps { } function TransactionRequestReviewDialog(props: TransactionRequestReviewDialogProps) { - const { accounts, accountsData, horizon, onClose, onAccountChange, selectedAccount, sendTransaction } = props + const { accounts, horizon, onClose, onAccountChange, selectedAccount, sendTransaction } = props const { msg, originDomain, pubkey, signature, isTestNetwork: testnet } = props.txStellarUri const transaction = React.useMemo(() => props.txStellarUri.getTransaction(), [props.txStellarUri]) const replacements = React.useMemo(() => props.txStellarUri.getReplacements(), [props.txStellarUri]) @@ -117,9 +75,9 @@ function TransactionRequestReviewDialog(props: TransactionRequestReviewDialogPro const requiredAccount = accounts.find(acc => acc.publicKey === pubkey) return requiredAccount ? [requiredAccount] : [] } else { - return getSelectableAccounts(transaction, accounts, accountsData) + return accounts } - }, [accounts, accountsData, transaction, pubkey]) + }, [accounts, pubkey]) const getNewSeqNumber = React.useCallback( async account => { @@ -266,10 +224,6 @@ function ConnectedTransferRequestReviewDialog(props: ConnectedTransactionRequest const { accounts } = React.useContext(AccountsContext) const testnet = props.txStellarUri.isTestNetwork const selectableAccounts = React.useMemo(() => accounts.filter(acc => acc.testnet === testnet), [accounts, testnet]) - const accountsData = useLiveAccountDataSet( - selectableAccounts.map(acc => acc.publicKey), - testnet - ) const [selectedAccount, setSelectedAccount] = React.useState(null) return ( @@ -279,7 +233,6 @@ function ConnectedTransferRequestReviewDialog(props: ConnectedTransactionRequest Date: Wed, 20 Jan 2021 11:03:41 +0100 Subject: [PATCH 42/55] Show info dialog if no accounts are imported --- .../TransactionRequestReviewDialog.tsx | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx b/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx index 8d243677b..048eff5d7 100644 --- a/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx +++ b/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx @@ -215,6 +215,39 @@ function TransactionRequestReviewDialog(props: TransactionRequestReviewDialogPro ) } +function NoAccountsDialog(props: { onClose: () => void; testnet: boolean }) { + const { t } = useTranslation() + return ( + + {t("transaction-request.payment-account-selection.title")} + {props.testnet ? : null} + + } + /> + } + actions={ + + } onClick={props.onClose} type="secondary"> + {t("transaction-request.payment-account-selection.action.dismiss")} + + + } + > + + No accounts found for network. + You must import an account before you can sign transaction requests. + + + ) +} + interface ConnectedTransactionRequestReviewDialogProps { onClose: () => void txStellarUri: TransactionStellarUri @@ -226,9 +259,8 @@ function ConnectedTransferRequestReviewDialog(props: ConnectedTransactionRequest const selectableAccounts = React.useMemo(() => accounts.filter(acc => acc.testnet === testnet), [accounts, testnet]) const [selectedAccount, setSelectedAccount] = React.useState(null) - return ( - // FIXME fix default account of TransactionSender - + return selectableAccounts.length > 0 ? ( + {({ horizon, sendTransaction }) => ( )} + ) : ( + ) } From 05eb8c25fcac32ef62f298e2ba1d967092f8fe70 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 25 Jan 2021 10:11:01 +0100 Subject: [PATCH 43/55] Replace hard-coded strings --- i18n/locales/en/transaction-request.json | 24 +++++++++++++++++++ .../TransactionRequestReviewDialog.tsx | 22 ++++++++--------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/i18n/locales/en/transaction-request.json b/i18n/locales/en/transaction-request.json index 5f3fa41e9..90b67206b 100644 --- a/i18n/locales/en/transaction-request.json +++ b/i18n/locales/en/transaction-request.json @@ -29,6 +29,30 @@ "title": "Transaction proposed", "warning": "The origin of this request cannot be verified! Decline when in doubt." }, + "transaction": { + "account-selector": { + "source-account": "Select the source account", + "signing-account": "Select the account that will sign the transaction" + }, + "hint": "Hint", + "action": { + "dismiss": "Dismiss", + "select": "Select" + }, + "error": { + "signer-not-imported": "The transaction request specified '{{signer}}' as the target signer but this account is not imported.", + "no-eligible-accounts": "No eligible account found." + }, + "header": { + "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." + }, + "uri-content": { + "message": "Message" + }, + "title": "Transaction proposed", + "warning": "The origin of this request cannot be verified! Decline when in doubt." + }, "verify-trusted-service": { "action": { "trust": "Trust", diff --git a/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx b/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx index 048eff5d7..2773c4ada 100644 --- a/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx +++ b/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx @@ -132,7 +132,7 @@ function TransactionRequestReviewDialog(props: TransactionRequestReviewDialogPro onBack={onClose} title={ - {t("transaction-request.payment-account-selection.title")} + {t("transaction-request.transaction.title")} {testnet ? : null} } @@ -141,10 +141,10 @@ function TransactionRequestReviewDialog(props: TransactionRequestReviewDialogPro actions={ } onClick={onClose} type="secondary"> - {t("transaction-request.payment-account-selection.action.dismiss")} + {t("transaction-request.transaction.action.dismiss")} } onClick={onSelect} type="primary"> - {t("transaction-request.payment-account-selection.action.select")} + {t("transaction-request.transaction.action.select")} } @@ -152,14 +152,14 @@ function TransactionRequestReviewDialog(props: TransactionRequestReviewDialogPro {signature ? ( - + The following transaction has been proposed by {{ originDomain }}. ) : ( - {t("transaction-request.payment-account-selection.warning")} + {t("transaction-request.payment.warning")} )} @@ -184,28 +184,28 @@ function TransactionRequestReviewDialog(props: TransactionRequestReviewDialogPro selectableAccounts.length > 0 ? ( <> - Select the source account
+ {t("transaction-request.transaction.account-selector.source-account")}
{sourceAccountReplacement.hint && ( - Hint: {sourceAccountReplacement.hint} + {t("transaction-request.transaction.hint")}: {sourceAccountReplacement.hint} )} ) : pubkey ? ( - The transaction request specified '{pubkey}' as the target signer but this account is not imported. + {t("transaction-request.transaction.error.signer-not-imported", { signer: pubkey })} ) : ( - No eligible account found. + {t("transaction-request.transaction.error.no-eligible-account")} ) ) : ( <> - Select the account that will sign the transaction
+ {t("transaction-request.transaction.account-selector.signing-account")}
@@ -264,7 +264,7 @@ function ConnectedTransferRequestReviewDialog(props: ConnectedTransactionRequest {({ horizon, sendTransaction }) => ( Date: Mon, 25 Jan 2021 10:13:02 +0100 Subject: [PATCH 44/55] Remove whitespace of NoAccountsDialog --- ...ccountsDialog.tsx => NoAccountsDialog.tsx} | 0 .../components/PaymentRequestReviewDialog.tsx | 2 +- .../TransactionRequestReviewDialog.tsx | 34 +------------------ 3 files changed, 2 insertions(+), 34 deletions(-) rename src/TransactionRequest/components/{ NoAccountsDialog.tsx => NoAccountsDialog.tsx} (100%) diff --git a/src/TransactionRequest/components/ NoAccountsDialog.tsx b/src/TransactionRequest/components/NoAccountsDialog.tsx similarity index 100% rename from src/TransactionRequest/components/ NoAccountsDialog.tsx rename to src/TransactionRequest/components/NoAccountsDialog.tsx diff --git a/src/TransactionRequest/components/PaymentRequestReviewDialog.tsx b/src/TransactionRequest/components/PaymentRequestReviewDialog.tsx index a0c693bd9..10476c4c7 100644 --- a/src/TransactionRequest/components/PaymentRequestReviewDialog.tsx +++ b/src/TransactionRequest/components/PaymentRequestReviewDialog.tsx @@ -20,7 +20,7 @@ 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" +import NoAccountsDialog from "./NoAccountsDialog" interface ConnectedPaymentFormProps { accountData: AccountData diff --git a/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx b/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx index 2773c4ada..49e80883b 100644 --- a/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx +++ b/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx @@ -19,6 +19,7 @@ import DialogBody from "~Layout/components/DialogBody" import { useTransactionTitle } from "~TransactionReview/components/TransactionReviewDialog" import TransactionSummary from "~TransactionReview/components/TransactionSummary" import TransactionSender, { SendTransaction } from "../../Transaction/components/TransactionSender" +import NoAccountsDialog from "./NoAccountsDialog" const useStyles = makeStyles(() => ({ root: { @@ -215,39 +216,6 @@ function TransactionRequestReviewDialog(props: TransactionRequestReviewDialogPro ) } -function NoAccountsDialog(props: { onClose: () => void; testnet: boolean }) { - const { t } = useTranslation() - return ( - - {t("transaction-request.payment-account-selection.title")} - {props.testnet ? : null} - - } - /> - } - actions={ - - } onClick={props.onClose} type="secondary"> - {t("transaction-request.payment-account-selection.action.dismiss")} - - - } - > - - No accounts found for network. - You must import an account before you can sign transaction requests. - - - ) -} - interface ConnectedTransactionRequestReviewDialogProps { onClose: () => void txStellarUri: TransactionStellarUri From dd381894cda56ca67aa38c21e595f344e427585d Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 25 Jan 2021 11:04:09 +0100 Subject: [PATCH 45/55] Create unified StellarRequestReviewDialog which combines handling payment and transaction requests --- .../components/PaymentRequestContent.tsx | 157 +++++++++++ .../components/PaymentRequestReviewDialog.tsx | 259 ------------------ .../components/StellarRequestReviewDialog.tsx | 123 +++++++++ .../components/StellarUriHandler.tsx | 39 +-- .../components/TransactionRequestContent.tsx | 195 +++++++++++++ .../TransactionRequestReviewDialog.tsx | 248 ----------------- 6 files changed, 485 insertions(+), 536 deletions(-) create mode 100644 src/TransactionRequest/components/PaymentRequestContent.tsx delete mode 100644 src/TransactionRequest/components/PaymentRequestReviewDialog.tsx create mode 100644 src/TransactionRequest/components/StellarRequestReviewDialog.tsx create mode 100644 src/TransactionRequest/components/TransactionRequestContent.tsx delete mode 100644 src/TransactionRequest/components/TransactionRequestReviewDialog.tsx diff --git a/src/TransactionRequest/components/PaymentRequestContent.tsx b/src/TransactionRequest/components/PaymentRequestContent.tsx new file mode 100644 index 000000000..866cf8200 --- /dev/null +++ b/src/TransactionRequest/components/PaymentRequestContent.tsx @@ -0,0 +1,157 @@ +import Box from "@material-ui/core/Box" +import Typography from "@material-ui/core/Typography" +import { PayStellarUri } from "@stellarguard/stellar-uri" +import React from "react" +import { useTranslation } from "react-i18next" +import { Asset, Server, Transaction } from "stellar-sdk" +import AccountSelectionList from "~Account/components/AccountSelectionList" +import { Account } from "~App/contexts/accounts" +import { trackError } from "~App/contexts/notifications" +import ViewLoading from "~Generic/components/ViewLoading" +import { useLiveAccountDataSet, useLiveAccountOffers } from "~Generic/hooks/stellar-subscriptions" +import { RefStateObject } from "~Generic/hooks/userinterface" +import { AccountData } from "~Generic/lib/account" +import { getAssetsFromBalances } from "~Generic/lib/stellar" +import { PaymentParams } from "~Payment/components/PaymentDialog" +import PaymentForm from "~Payment/components/PaymentForm" +import { SendTransaction } from "../../Transaction/components/TransactionSender" + +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 ( + + ) +} + +interface PaymentRequestContentProps { + accounts: Account[] + actionsRef: RefStateObject + horizon: Server + onAccountChange: (account: Account) => void + onClose: () => void + payStellarUri: PayStellarUri + selectedAccount: Account | null + sendTransaction: SendTransaction +} + +function PaymentRequestContent(props: PaymentRequestContentProps) { + const { + amount, + assetCode, + assetIssuer, + destination, + memo, + memoType, + msg, + isTestNetwork: testnet + } = props.payStellarUri + + 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 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 ( + + }> + {props.selectedAccount && accountData && ( + <> + {msg && ( + + {t("transaction-request.payment.uri-content.message")}: {msg} + + )} + + + )} + + + {t("transaction-request.payment.account-selector")} + + {props.accounts.length > 0 ? ( + + ) : ( + + {asset.code === "XLM" + ? t("transaction-request.payment.error.no-activated-accounts") + : t("transaction-request.payment.error.no-accounts-with-trustline")} + + )} + + ) +} + +export default PaymentRequestContent diff --git a/src/TransactionRequest/components/PaymentRequestReviewDialog.tsx b/src/TransactionRequest/components/PaymentRequestReviewDialog.tsx deleted file mode 100644 index 10476c4c7..000000000 --- a/src/TransactionRequest/components/PaymentRequestReviewDialog.tsx +++ /dev/null @@ -1,259 +0,0 @@ -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 PaymentRequestReviewDialogProps { - accounts: Account[] - horizon: Server - onAccountChange: (account: Account) => void - onClose: () => void - payStellarUri: PayStellarUri - selectedAccount: Account | null - sendTransaction: SendTransaction -} - -function PaymentRequestReviewDialog(props: PaymentRequestReviewDialogProps) { - 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.warning")} - - - )} - }> - {props.selectedAccount && accountData && ( - <> - {msg && ( - - {t("transaction-request.payment.uri-content.message")}: {msg} - - )} - - - )} - - - {t("transaction-request.payment.account-selector")} - - {props.accounts.length > 0 ? ( - - ) : ( - - {asset.code === "XLM" - ? t("transaction-request.payment.error.no-activated-accounts") - : t("transaction-request.payment.error.no-accounts-with-trustline")} - - )} - - - ) -} - -function ConnectedPaymentRequestReviewDialog( - 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 ConnectedPaymentRequestReviewDialog diff --git a/src/TransactionRequest/components/StellarRequestReviewDialog.tsx b/src/TransactionRequest/components/StellarRequestReviewDialog.tsx new file mode 100644 index 000000000..3860a3c49 --- /dev/null +++ b/src/TransactionRequest/components/StellarRequestReviewDialog.tsx @@ -0,0 +1,123 @@ +import Box from "@material-ui/core/Box" +import { makeStyles } from "@material-ui/core/styles" +import Typography from "@material-ui/core/Typography" +import WarningIcon from "@material-ui/icons/Warning" +import { PayStellarUri, StellarUri, StellarUriType, TransactionStellarUri } from "@stellarguard/stellar-uri" +import React from "react" +import { Trans, useTranslation } from "react-i18next" +import { Account, AccountsContext } from "~App/contexts/accounts" +import { breakpoints, warningColor } from "~App/theme" +import MainTitle from "~Generic/components/MainTitle" +import { RefStateObject, useDialogActions } from "~Generic/hooks/userinterface" +import DialogBody from "~Layout/components/DialogBody" +import TransactionSender from "../../Transaction/components/TransactionSender" +import NoAccountsDialog from "./NoAccountsDialog" +import PaymentRequestContent from "./PaymentRequestContent" +import TransactionRequestContent from "./TransactionRequestContent" + +const useStyles = makeStyles(() => ({ + root: { + display: "flex", + flexDirection: "column", + padding: "12px 0 0" + }, + warningContainer: { + alignItems: "center", + alignSelf: "center", + background: warningColor, + display: "flex", + justifyContent: "center", + padding: "6px 16px", + width: "fit-content", + + [breakpoints.up(600)]: { + width: "100%" + } + } +})) + +interface StellarRequestReviewDialogProps { + children: React.ReactNode + actionsRef: RefStateObject + stellarUri: StellarUri + onClose: () => void +} + +function StellarRequestReviewDialog(props: StellarRequestReviewDialogProps) { + const { onClose } = props + + const classes = useStyles() + const { t } = useTranslation() + + return ( + } + actions={props.actionsRef} + > + + {props.stellarUri.signature ? ( + + + The following transaction has been proposed by {{ originDomain: props.stellarUri.originDomain }}. + + + ) : ( + + + {t("transaction-request.payment.warning")} + + + )} + {props.children} + + + ) +} + +function ConnectedStellarRequestReviewDialog(props: Pick) { + const { accounts } = React.useContext(AccountsContext) + const testnet = props.stellarUri.isTestNetwork + const accountsForNetwork = React.useMemo(() => accounts.filter(acc => acc.testnet === testnet), [accounts, testnet]) + const [selectedAccount, setSelectedAccount] = React.useState( + accountsForNetwork.length > 0 ? accountsForNetwork[0] : null + ) + + const dialogActionsRef = useDialogActions() + + return accountsForNetwork.length > 0 ? ( + + {({ horizon, sendTransaction }) => ( + + {props.stellarUri.operation === StellarUriType.Pay ? ( + + ) : ( + + )} + + )} + + ) : ( + + ) +} + +export default ConnectedStellarRequestReviewDialog diff --git a/src/TransactionRequest/components/StellarUriHandler.tsx b/src/TransactionRequest/components/StellarUriHandler.tsx index 5d523e2f8..ffb5773be 100644 --- a/src/TransactionRequest/components/StellarUriHandler.tsx +++ b/src/TransactionRequest/components/StellarUriHandler.tsx @@ -1,13 +1,12 @@ -import React from "react" +import { Dialog } from "@material-ui/core" 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, TransactionStellarUri } from "@stellarguard/stellar-uri" -import { TransactionRequestContext } from "~App/contexts/transactionRequest" +import { StellarUri } from "@stellarguard/stellar-uri" +import React from "react" import { SettingsContext } from "~App/contexts/settings" +import { TransactionRequestContext } from "~App/contexts/transactionRequest" +import StellarRequestReviewDialog from "./StellarRequestReviewDialog" import VerifyTrustedServiceDialog from "./VerifyTrustedServiceDialog" -import PaymentRequestReviewDialog from "./PaymentRequestReviewDialog" -import TransactionRequestReviewDialog from "./TransactionRequestReviewDialog" const Transition = React.forwardRef((props: TransitionProps, ref) => ) @@ -52,29 +51,11 @@ function StellarUriHandler() { ) } - if (renderedURI.operation === StellarUriType.Pay) { - const payStellarUri = renderedURI as PayStellarUri - const onClose = () => { - closeDialog() - } - - return ( - - - - ) - } else { - const txStellarUri = renderedURI as TransactionStellarUri - const onClose = () => { - closeDialog() - } - - return ( - - - - ) - } + return ( + + + + ) } export default React.memo(StellarUriHandler) diff --git a/src/TransactionRequest/components/TransactionRequestContent.tsx b/src/TransactionRequest/components/TransactionRequestContent.tsx new file mode 100644 index 000000000..fff9d479c --- /dev/null +++ b/src/TransactionRequest/components/TransactionRequestContent.tsx @@ -0,0 +1,195 @@ +import Box from "@material-ui/core/Box" +import makeStyles from "@material-ui/core/styles/makeStyles" +import Typography from "@material-ui/core/Typography" +import CloseIcon from "@material-ui/icons/Close" +import SendIcon from "@material-ui/icons/Send" +import { TransactionStellarUri } from "@stellarguard/stellar-uri" +import BigNumber from "big.js" +import React from "react" +import { useTranslation } from "react-i18next" +import { Server } from "stellar-sdk" +import AccountSelectionList from "~Account/components/AccountSelectionList" +import { Account } from "~App/contexts/accounts" +import { ActionButton, DialogActionsBox } from "~Generic/components/DialogActions" +import Portal from "~Generic/components/Portal" +import { RefStateObject } from "~Generic/hooks/userinterface" +import { useTransactionTitle } from "~TransactionReview/components/TransactionReviewDialog" +import TransactionSummary from "~TransactionReview/components/TransactionSummary" +import { SendTransaction } from "../../Transaction/components/TransactionSender" + +const useStyles = makeStyles(() => ({ + root: { + display: "flex", + flexDirection: "column", + padding: "12px 0 0" + }, + uriContainer: { + paddingTop: 32, + paddingBottom: 32 + } +})) + +interface TransactionRequestContentProps { + accounts: Account[] + actionsRef: RefStateObject + horizon: Server + onClose: () => void + onAccountChange: (account: Account) => void + selectedAccount: Account | null + sendTransaction: SendTransaction + txStellarUri: TransactionStellarUri +} + +function TransactionRequestContent(props: TransactionRequestContentProps) { + const { onAccountChange, onClose, sendTransaction } = props + const { msg, pubkey, isTestNetwork: testnet } = props.txStellarUri + const transaction = React.useMemo(() => props.txStellarUri.getTransaction(), [props.txStellarUri]) + const replacements = React.useMemo(() => props.txStellarUri.getReplacements(), [props.txStellarUri]) + const sourceAccountReplacement = React.useMemo( + () => replacements.find(replacement => replacement.path === "sourceAccount"), + [replacements] + ) + const [txCreationPending, setTxCreationPending] = React.useState(false) + + const classes = useStyles() + const { t } = useTranslation() + const getTitle = useTransactionTitle() + + const selectableAccounts = React.useMemo(() => { + if (pubkey) { + // pubkey parameter specifies public key of the account that should sign + const requiredAccount = props.accounts.find(acc => acc.publicKey === pubkey) + return requiredAccount ? [requiredAccount] : [] + } else { + return props.accounts + } + }, [props.accounts, pubkey]) + + const getNewSeqNumber = React.useCallback( + async account => { + const fetchedSeqNum = await props.horizon.loadAccount(account).then(acc => acc.sequence) + const newSeqNum = BigNumber(fetchedSeqNum) + .add(1) + .toString() + return newSeqNum + }, + [props.horizon] + ) + + const onSelect = React.useCallback(async () => { + setTxCreationPending(true) + const filledReplacements: { [key: string]: any } = {} + const seqNumReplacement = replacements.find(replacement => replacement.id === "seqNum") + if (seqNumReplacement) { + const sourceAccount = + sourceAccountReplacement && props.selectedAccount ? props.selectedAccount.publicKey : transaction.source + const newSeqNum = await getNewSeqNumber(sourceAccount) + filledReplacements[seqNumReplacement.id] = newSeqNum + } + if (sourceAccountReplacement && props.selectedAccount) { + const sourceAccount = + sourceAccountReplacement && props.selectedAccount ? props.selectedAccount.publicKey : transaction.source + filledReplacements[sourceAccountReplacement.id] = props.selectedAccount.publicKey + + if (!seqNumReplacement) { + // artificially add seqNum replacement to facilitate replacing seq number for new source account + const artificialSeqNumReplacement = { id: "SEQ", path: "seqNum", hint: "sequence number" } + props.txStellarUri.addReplacement(artificialSeqNumReplacement) + const newSeqNum = await getNewSeqNumber(sourceAccount) + filledReplacements[artificialSeqNumReplacement.id] = newSeqNum + } + } + + const newTx = props.txStellarUri.replace(filledReplacements).getTransaction() + sendTransaction(newTx) + setTxCreationPending(false) + }, [ + getNewSeqNumber, + props.txStellarUri, + props.selectedAccount, + replacements, + sourceAccountReplacement, + sendTransaction, + transaction.source + ]) + + const dialogActions = React.useMemo( + () => ( + + } onClick={onClose}> + {t("payment.actions.dismiss")} + + } + disabled={!props.selectedAccount} + loading={txCreationPending} + onClick={onSelect} + type="primary" + > + {t("payment.actions.submit")} + + + ), + [onSelect, onClose, props.selectedAccount, t, txCreationPending] + ) + + return ( + + {msg && ( + + Message: + {msg} + + )} + + {getTitle(transaction)} + + + {sourceAccountReplacement ? ( + selectableAccounts.length > 0 ? ( + <> + + {t("transaction-request.transaction.account-selector.source-account")}
+
+ {sourceAccountReplacement.hint && ( + + {t("transaction-request.transaction.hint")}: {sourceAccountReplacement.hint} + + )} + + + ) : pubkey ? ( + + {t("transaction-request.transaction.error.signer-not-imported", { signer: pubkey })} + + ) : ( + + {t("transaction-request.transaction.error.no-eligible-account")} + + ) + ) : ( + <> + + {t("transaction-request.transaction.account-selector.signing-account")}
+
+ + + )} + {dialogActions} +
+ ) +} + +export default TransactionRequestContent diff --git a/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx b/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx deleted file mode 100644 index 49e80883b..000000000 --- a/src/TransactionRequest/components/TransactionRequestReviewDialog.tsx +++ /dev/null @@ -1,248 +0,0 @@ -import Box from "@material-ui/core/Box" -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 { TransactionStellarUri } from "@stellarguard/stellar-uri" -import BigNumber from "big.js" -import React from "react" -import { Trans, useTranslation } from "react-i18next" -import { Server } from "stellar-sdk" -import AccountSelectionList from "~Account/components/AccountSelectionList" -import { Account, AccountsContext } from "~App/contexts/accounts" -import { breakpoints, warningColor } from "~App/theme" -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" -import { useTransactionTitle } from "~TransactionReview/components/TransactionReviewDialog" -import TransactionSummary from "~TransactionReview/components/TransactionSummary" -import TransactionSender, { SendTransaction } from "../../Transaction/components/TransactionSender" -import NoAccountsDialog from "./NoAccountsDialog" - -const useStyles = makeStyles(() => ({ - root: { - display: "flex", - flexDirection: "column", - padding: "12px 0 0" - }, - 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 TransactionRequestReviewDialogProps { - accounts: Account[] - horizon: Server - onClose: () => void - onAccountChange: (account: Account) => void - selectedAccount: Account | null - sendTransaction: SendTransaction - txStellarUri: TransactionStellarUri -} - -function TransactionRequestReviewDialog(props: TransactionRequestReviewDialogProps) { - const { accounts, horizon, onClose, onAccountChange, selectedAccount, sendTransaction } = props - const { msg, originDomain, pubkey, signature, isTestNetwork: testnet } = props.txStellarUri - const transaction = React.useMemo(() => props.txStellarUri.getTransaction(), [props.txStellarUri]) - const replacements = React.useMemo(() => props.txStellarUri.getReplacements(), [props.txStellarUri]) - const sourceAccountReplacement = React.useMemo( - () => replacements.find(replacement => replacement.path === "sourceAccount"), - [replacements] - ) - - const classes = useStyles() - const { t } = useTranslation() - const getTitle = useTransactionTitle() - - const selectableAccounts = React.useMemo(() => { - if (pubkey) { - // pubkey parameter specifies public key of the account that should sign - const requiredAccount = accounts.find(acc => acc.publicKey === pubkey) - return requiredAccount ? [requiredAccount] : [] - } else { - return accounts - } - }, [accounts, pubkey]) - - const getNewSeqNumber = React.useCallback( - async account => { - const fetchedSeqNum = await horizon.loadAccount(account).then(acc => acc.sequence) - const newSeqNum = BigNumber(fetchedSeqNum) - .add(1) - .toString() - return newSeqNum - }, - [horizon] - ) - - const onSelect = React.useCallback(async () => { - const filledReplacements: { [key: string]: any } = {} - const seqNumReplacement = replacements.find(replacement => replacement.id === "seqNum") - if (seqNumReplacement) { - const sourceAccount = sourceAccountReplacement && selectedAccount ? selectedAccount.publicKey : transaction.source - const newSeqNum = await getNewSeqNumber(sourceAccount) - filledReplacements[seqNumReplacement.id] = newSeqNum - } - if (sourceAccountReplacement && selectedAccount) { - const sourceAccount = sourceAccountReplacement && selectedAccount ? selectedAccount.publicKey : transaction.source - filledReplacements[sourceAccountReplacement.id] = selectedAccount.publicKey - - if (!seqNumReplacement) { - // artificially add seqNum replacement to facilitate replacing seq number for new source account - const artificialSeqNumReplacement = { id: "SEQ", path: "seqNum", hint: "sequence number" } - props.txStellarUri.addReplacement(artificialSeqNumReplacement) - const newSeqNum = await getNewSeqNumber(sourceAccount) - filledReplacements[artificialSeqNumReplacement.id] = newSeqNum - } - } - - const newTx = props.txStellarUri.replace(filledReplacements).getTransaction() - sendTransaction(newTx) - }, [ - getNewSeqNumber, - replacements, - props.txStellarUri, - sourceAccountReplacement, - sendTransaction, - selectedAccount, - transaction.source - ]) - - return ( - - {t("transaction-request.transaction.title")} - {testnet ? : null} - - } - /> - } - actions={ - - } onClick={onClose} type="secondary"> - {t("transaction-request.transaction.action.dismiss")} - - } onClick={onSelect} type="primary"> - {t("transaction-request.transaction.action.select")} - - - } - > - - {signature ? ( - - - The following transaction has been proposed by {{ originDomain }}. - - - ) : ( - - - {t("transaction-request.payment.warning")} - - - )} - {msg && ( - - Message: - {msg} - - )} - - {getTitle(transaction)} - - - {sourceAccountReplacement ? ( - selectableAccounts.length > 0 ? ( - <> - - {t("transaction-request.transaction.account-selector.source-account")}
-
- {sourceAccountReplacement.hint && ( - - {t("transaction-request.transaction.hint")}: {sourceAccountReplacement.hint} - - )} - - - ) : pubkey ? ( - - {t("transaction-request.transaction.error.signer-not-imported", { signer: pubkey })} - - ) : ( - - {t("transaction-request.transaction.error.no-eligible-account")} - - ) - ) : ( - <> - - {t("transaction-request.transaction.account-selector.signing-account")}
-
- - - )} -
-
- ) -} - -interface ConnectedTransactionRequestReviewDialogProps { - onClose: () => void - txStellarUri: TransactionStellarUri -} - -function ConnectedTransferRequestReviewDialog(props: ConnectedTransactionRequestReviewDialogProps) { - const { accounts } = React.useContext(AccountsContext) - const testnet = props.txStellarUri.isTestNetwork - const selectableAccounts = React.useMemo(() => accounts.filter(acc => acc.testnet === testnet), [accounts, testnet]) - const [selectedAccount, setSelectedAccount] = React.useState(null) - - return selectableAccounts.length > 0 ? ( - - {({ horizon, sendTransaction }) => ( - - )} - - ) : ( - - ) -} - -export default React.memo(ConnectedTransferRequestReviewDialog) From d362dbd41eca41049213cd1be58af72305eee502 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 25 Jan 2021 11:09:18 +0100 Subject: [PATCH 46/55] Rename i18n keys --- i18n/locales/en/transaction-request.json | 26 ++++++++----------- .../components/StellarRequestReviewDialog.tsx | 6 ++--- .../components/TransactionRequestContent.tsx | 6 ++--- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/i18n/locales/en/transaction-request.json b/i18n/locales/en/transaction-request.json index 90b67206b..e492c596b 100644 --- a/i18n/locales/en/transaction-request.json +++ b/i18n/locales/en/transaction-request.json @@ -19,22 +19,15 @@ "no-activated-accounts": "No activated accounts found.", "no-accounts-with-trustline": "No accounts with matching trustline found." }, - "header": { - "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." - }, "uri-content": { "message": "Message" - }, - "title": "Transaction proposed", - "warning": "The origin of this request cannot be verified! Decline when in doubt." + } }, "transaction": { "account-selector": { "source-account": "Select the source account", "signing-account": "Select the account that will sign the transaction" }, - "hint": "Hint", "action": { "dismiss": "Dismiss", "select": "Select" @@ -43,26 +36,29 @@ "signer-not-imported": "The transaction request specified '{{signer}}' as the target signer but this account is not imported.", "no-eligible-accounts": "No eligible account found." }, - "header": { - "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." - }, + "hint": "Hint", "uri-content": { "message": "Message" }, - "title": "Transaction proposed", "warning": "The origin of this request cannot be verified! Decline when in doubt." }, + "stellar-uri": { + "header": { + "origin-domain": "The following transaction has been proposed by <1>{{originDomain}}<3>", + "warning": "The origin of this request cannot be verified! Decline when in doubt." + }, + "title": "Transaction proposed" + }, "verify-trusted-service": { "action": { "trust": "Trust", "cancel": "Cancel" }, - "title": "Verify Trusted Service", "info": { "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." - } + }, + "title": "Verify Trusted Service" } } diff --git a/src/TransactionRequest/components/StellarRequestReviewDialog.tsx b/src/TransactionRequest/components/StellarRequestReviewDialog.tsx index 3860a3c49..6f3abbfc7 100644 --- a/src/TransactionRequest/components/StellarRequestReviewDialog.tsx +++ b/src/TransactionRequest/components/StellarRequestReviewDialog.tsx @@ -52,20 +52,20 @@ function StellarRequestReviewDialog(props: StellarRequestReviewDialogProps) { return ( } + top={} actions={props.actionsRef} > {props.stellarUri.signature ? ( - + The following transaction has been proposed by {{ originDomain: props.stellarUri.originDomain }}. ) : ( - {t("transaction-request.payment.warning")} + {t("transaction-request.stellar-uri.header.warning")} )} diff --git a/src/TransactionRequest/components/TransactionRequestContent.tsx b/src/TransactionRequest/components/TransactionRequestContent.tsx index fff9d479c..762e22631 100644 --- a/src/TransactionRequest/components/TransactionRequestContent.tsx +++ b/src/TransactionRequest/components/TransactionRequestContent.tsx @@ -117,7 +117,7 @@ function TransactionRequestContent(props: TransactionRequestContentProps) { () => ( } onClick={onClose}> - {t("payment.actions.dismiss")} + {t("transaction-request.transaction.action.dismiss")} } @@ -126,7 +126,7 @@ function TransactionRequestContent(props: TransactionRequestContentProps) { onClick={onSelect} type="primary" > - {t("payment.actions.submit")} + {t("transaction-request.transaction.action.select")} ), @@ -137,7 +137,7 @@ function TransactionRequestContent(props: TransactionRequestContentProps) { {msg && ( - Message: + {t("transaciton-request.transaction.uri-content.message")}: {msg} )} From 1360e886a5317341e478f681df12e7829050d778 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 25 Jan 2021 11:42:50 +0100 Subject: [PATCH 47/55] Remove paymentParams prop from PaymentDialog --- src/Payment/components/PaymentDialog.tsx | 16 ++-------------- src/Payment/components/PaymentForm.tsx | 9 ++++++++- .../components/PaymentRequestContent.tsx | 2 +- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/Payment/components/PaymentDialog.tsx b/src/Payment/components/PaymentDialog.tsx index 23ad4e6b8..241e46ee4 100644 --- a/src/Payment/components/PaymentDialog.tsx +++ b/src/Payment/components/PaymentDialog.tsx @@ -1,6 +1,6 @@ import React from "react" import { useTranslation } from "react-i18next" -import { Asset, MemoType, Server, Transaction } from "stellar-sdk" +import { Asset, 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" @@ -15,21 +15,12 @@ import MainTitle from "~Generic/components/MainTitle" import TransactionSender from "~Transaction/components/TransactionSender" import PaymentForm from "./PaymentForm" -export interface PaymentParams { - amount?: string - asset?: Asset - destination?: string - memo?: string - memoType?: MemoType -} - interface Props { account: Account accountData: AccountData horizon: Server onClose: () => void openOrdersCount: number - paymentParams?: PaymentParams sendTransaction: (transaction: Transaction) => Promise } @@ -83,7 +74,6 @@ function PaymentDialog(props: Props) { actionsRef={dialogActionsRef} onSubmit={handleSubmit} openOrdersCount={props.openOrdersCount} - preselectedParams={props.paymentParams} testnet={props.account.testnet} trustedAssets={trustedAssets} txCreationPending={txCreationPending} @@ -92,9 +82,7 @@ function PaymentDialog(props: Props) { ) } -function ConnectedPaymentDialog( - props: Pick & { onSubmissionCompleted?: () => void } -) { +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) diff --git a/src/Payment/components/PaymentForm.tsx b/src/Payment/components/PaymentForm.tsx index 0f3e61795..85db26ee0 100644 --- a/src/Payment/components/PaymentForm.tsx +++ b/src/Payment/components/PaymentForm.tsx @@ -23,7 +23,14 @@ 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 { PaymentParams } from "./PaymentDialog" + +export interface PaymentParams { + amount?: string + asset?: Asset + destination?: string + memo?: string + memoType?: MemoType +} export interface PaymentFormValues { amount: string diff --git a/src/TransactionRequest/components/PaymentRequestContent.tsx b/src/TransactionRequest/components/PaymentRequestContent.tsx index 866cf8200..0ea0aed59 100644 --- a/src/TransactionRequest/components/PaymentRequestContent.tsx +++ b/src/TransactionRequest/components/PaymentRequestContent.tsx @@ -12,7 +12,7 @@ import { useLiveAccountDataSet, useLiveAccountOffers } from "~Generic/hooks/stel import { RefStateObject } from "~Generic/hooks/userinterface" import { AccountData } from "~Generic/lib/account" import { getAssetsFromBalances } from "~Generic/lib/stellar" -import { PaymentParams } from "~Payment/components/PaymentDialog" +import { PaymentParams } from "~Payment/components/PaymentForm" import PaymentForm from "~Payment/components/PaymentForm" import { SendTransaction } from "../../Transaction/components/TransactionSender" From 16a9d2b5358504c90ae882f9492bcec6edcb0721 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 25 Jan 2021 12:21:37 +0100 Subject: [PATCH 48/55] Show message on submission error --- .../components/TransactionRequestContent.tsx | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/TransactionRequest/components/TransactionRequestContent.tsx b/src/TransactionRequest/components/TransactionRequestContent.tsx index 762e22631..55ce6727b 100644 --- a/src/TransactionRequest/components/TransactionRequestContent.tsx +++ b/src/TransactionRequest/components/TransactionRequestContent.tsx @@ -10,6 +10,7 @@ import { useTranslation } from "react-i18next" import { Server } from "stellar-sdk" import AccountSelectionList from "~Account/components/AccountSelectionList" import { Account } from "~App/contexts/accounts" +import { trackError } from "~App/contexts/notifications" import { ActionButton, DialogActionsBox } from "~Generic/components/DialogActions" import Portal from "~Generic/components/Portal" import { RefStateObject } from "~Generic/hooks/userinterface" @@ -77,32 +78,37 @@ function TransactionRequestContent(props: TransactionRequestContentProps) { ) const onSelect = React.useCallback(async () => { - setTxCreationPending(true) - const filledReplacements: { [key: string]: any } = {} - const seqNumReplacement = replacements.find(replacement => replacement.id === "seqNum") - if (seqNumReplacement) { - const sourceAccount = - sourceAccountReplacement && props.selectedAccount ? props.selectedAccount.publicKey : transaction.source - const newSeqNum = await getNewSeqNumber(sourceAccount) - filledReplacements[seqNumReplacement.id] = newSeqNum - } - if (sourceAccountReplacement && props.selectedAccount) { - const sourceAccount = - sourceAccountReplacement && props.selectedAccount ? props.selectedAccount.publicKey : transaction.source - filledReplacements[sourceAccountReplacement.id] = props.selectedAccount.publicKey - - if (!seqNumReplacement) { - // artificially add seqNum replacement to facilitate replacing seq number for new source account - const artificialSeqNumReplacement = { id: "SEQ", path: "seqNum", hint: "sequence number" } - props.txStellarUri.addReplacement(artificialSeqNumReplacement) + try { + setTxCreationPending(true) + const filledReplacements: { [key: string]: any } = {} + const seqNumReplacement = replacements.find(replacement => replacement.id === "seqNum") + if (seqNumReplacement) { + const sourceAccount = + sourceAccountReplacement && props.selectedAccount ? props.selectedAccount.publicKey : transaction.source const newSeqNum = await getNewSeqNumber(sourceAccount) - filledReplacements[artificialSeqNumReplacement.id] = newSeqNum + filledReplacements[seqNumReplacement.id] = newSeqNum } - } + if (sourceAccountReplacement && props.selectedAccount) { + const sourceAccount = + sourceAccountReplacement && props.selectedAccount ? props.selectedAccount.publicKey : transaction.source + filledReplacements[sourceAccountReplacement.id] = props.selectedAccount.publicKey - const newTx = props.txStellarUri.replace(filledReplacements).getTransaction() - sendTransaction(newTx) - setTxCreationPending(false) + if (!seqNumReplacement) { + // artificially add seqNum replacement to facilitate replacing seq number for new source account + const artificialSeqNumReplacement = { id: "SEQ", path: "seqNum", hint: "sequence number" } + props.txStellarUri.addReplacement(artificialSeqNumReplacement) + const newSeqNum = await getNewSeqNumber(sourceAccount) + filledReplacements[artificialSeqNumReplacement.id] = newSeqNum + } + } + + const newTx = props.txStellarUri.replace(filledReplacements).getTransaction() + sendTransaction(newTx) + } catch (error) { + trackError(error) + } finally { + setTxCreationPending(false) + } }, [ getNewSeqNumber, props.txStellarUri, From f93d217bc45eaa92c9658d4ca3e5172d90111aa6 Mon Sep 17 00:00:00 2001 From: Kosta Korenkov Date: Thu, 24 Oct 2024 10:33:28 +0200 Subject: [PATCH 49/55] =?UTF-8?q?brand:=20Solar=20=E2=86=92=20Sunce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/TransactionRequest/components/StellarUriHandler.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TransactionRequest/components/StellarUriHandler.tsx b/src/TransactionRequest/components/StellarUriHandler.tsx index ffb5773be..d185e9db9 100644 --- a/src/TransactionRequest/components/StellarUriHandler.tsx +++ b/src/TransactionRequest/components/StellarUriHandler.tsx @@ -23,7 +23,7 @@ function StellarUriHandler() { clearURI() // Clear location href, since it might contain secret search params - window.history.pushState({}, "Solar Wallet", window.location.href.replace(window.location.search, "")) + window.history.pushState({}, "Sunce Wallet", window.location.href.replace(window.location.search, "")) }, [clearURI, uri]) if (!renderedURI) { From 452afb871dca2334d26a05b1fec63c49ef75d09e Mon Sep 17 00:00:00 2001 From: Kosta Korenkov Date: Thu, 24 Oct 2024 13:44:07 +0200 Subject: [PATCH 50/55] enable protocol handler for electron --- electron/src/bootstrap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/src/bootstrap.ts b/electron/src/bootstrap.ts index 3e794d709..861f8102a 100644 --- a/electron/src/bootstrap.ts +++ b/electron/src/bootstrap.ts @@ -7,4 +7,4 @@ app.name = "Sunce Wallet" app.setAppUserModelId("org.montelibero.sunce") // Disabled until we actually ship SEP-7 support -// app.setAsDefaultProtocolClient("web+stellar") +app.setAsDefaultProtocolClient("web+stellar") From ad8dfa17bf0230cab8e53f51c149a8cd47f362e0 Mon Sep 17 00:00:00 2001 From: Kosta Korenkov Date: Thu, 24 Oct 2024 14:19:00 +0200 Subject: [PATCH 51/55] add some missing translations. transaction-request string are translated only for RU locale for now. For Spanish and Italian locales english version is used (TODO: translate) --- i18n/es.ts | 2 + i18n/it.ts | 2 + i18n/locales/es/generic.json | 4 +- i18n/locales/es/payment.json | 1 + i18n/locales/es/transaction-request.json | 64 ++++++++++++++++++++++++ i18n/locales/it/generic.json | 2 + i18n/locales/it/payment.json | 1 + i18n/locales/it/transaction-request.json | 64 ++++++++++++++++++++++++ i18n/locales/ru/generic.json | 2 + i18n/locales/ru/payment.json | 1 + i18n/locales/ru/transaction-request.json | 64 ++++++++++++++++++++++++ i18n/ru.ts | 2 + src/App/contexts/transactionRequest.tsx | 8 ++- src/Transaction/lib/stellar-uri.ts | 3 +- 14 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 i18n/locales/es/transaction-request.json create mode 100644 i18n/locales/it/transaction-request.json create mode 100644 i18n/locales/ru/transaction-request.json diff --git a/i18n/es.ts b/i18n/es.ts index 40c0d7632..3f0db30a7 100644 --- a/i18n/es.ts +++ b/i18n/es.ts @@ -7,6 +7,7 @@ import Generic from "./locales/es/generic.json" import Operations from "./locales/es/operations.json" import Payment from "./locales/es/payment.json" import Trading from "./locales/es/trading.json" +import TransactionRequest from "./locales/es/transaction-request.json" import TransferService from "./locales/es/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 diff --git a/i18n/it.ts b/i18n/it.ts index 9aea1f08b..65b60cd5c 100644 --- a/i18n/it.ts +++ b/i18n/it.ts @@ -7,6 +7,7 @@ import Generic from "./locales/it/generic.json" import Operations from "./locales/it/operations.json" import Payment from "./locales/it/payment.json" import Trading from "./locales/it/trading.json" +import TransactionRequest from "./locales/it/transaction-request.json" import TransferService from "./locales/it/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 diff --git a/i18n/locales/es/generic.json b/i18n/locales/es/generic.json index d824cc44e..843dd4430 100644 --- a/i18n/locales/es/generic.json +++ b/i18n/locales/es/generic.json @@ -35,12 +35,14 @@ "request-failed-error": "Solicitud a {{target}} ha fallado con estado {{status}}: {{message}}", "stellar-address-not-found-error": "Dirección Stellar no encontrada: {{address}}", "stellar-address-request-failed-error": "Falló la resolución de la dirección Stellar {{address}}", + "stellar-uri-verification-error": "No se ha podido verificar la firma de Stellar URI", "submission-failed-error": "Envío de transacción a {{endpoint}} falló con estado {{status}}: {{message}}", "testnet-endpoint-not-available-error": "{{service}} no provee un punto de conexión Testnet.", "timeout-error": "Solicitud ha caducado", "unexpected-action-error": "Acción imprevista: {{action}}", "unexpected-state-error": "Se encontró un estado inesperado: {{state}}", "unexpected-response-type-error": "Tipo de respuesta imprevista: {{type}} / ${dataType}", + "unexpected-stellar-uri-type-error": "La uri entrante {{incomingURI}} no coincide con ningún tipo esperado.", "unknown-error": "Ocurrió un error desconocido.", "update-already-running-error": "¡La actualización ya se está ejecutando!", "wrong-password-error": "Contraseña equivocada.", @@ -96,4 +98,4 @@ "user-interface": { "copied-to-clipboard": "Copiado a Portapapeles." } -} \ No newline at end of file +} diff --git a/i18n/locales/es/payment.json b/i18n/locales/es/payment.json index 93842e100..b8da66d15 100644 --- a/i18n/locales/es/payment.json +++ b/i18n/locales/es/payment.json @@ -1,5 +1,6 @@ { "actions": { + "dismiss": "Desechar", "submit": "Enviar ahora" }, "memo-metadata": { diff --git a/i18n/locales/es/transaction-request.json b/i18n/locales/es/transaction-request.json new file mode 100644 index 000000000..e492c596b --- /dev/null +++ b/i18n/locales/es/transaction-request.json @@ -0,0 +1,64 @@ +{ + "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", + "select": "Select" + }, + "error": { + "no-activated-accounts": "No activated accounts found.", + "no-accounts-with-trustline": "No accounts with matching trustline found." + }, + "uri-content": { + "message": "Message" + } + }, + "transaction": { + "account-selector": { + "source-account": "Select the source account", + "signing-account": "Select the account that will sign the transaction" + }, + "action": { + "dismiss": "Dismiss", + "select": "Select" + }, + "error": { + "signer-not-imported": "The transaction request specified '{{signer}}' as the target signer but this account is not imported.", + "no-eligible-accounts": "No eligible account found." + }, + "hint": "Hint", + "uri-content": { + "message": "Message" + }, + "warning": "The origin of this request cannot be verified! Decline when in doubt." + }, + "stellar-uri": { + "header": { + "origin-domain": "The following transaction has been proposed by <1>{{originDomain}}<3>", + "warning": "The origin of this request cannot be verified! Decline when in doubt." + }, + "title": "Transaction proposed" + }, + "verify-trusted-service": { + "action": { + "trust": "Trust", + "cancel": "Cancel" + }, + "info": { + "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." + }, + "title": "Verify Trusted Service" + } +} diff --git a/i18n/locales/it/generic.json b/i18n/locales/it/generic.json index 4259ce46f..6e8f0f916 100755 --- a/i18n/locales/it/generic.json +++ b/i18n/locales/it/generic.json @@ -31,12 +31,14 @@ "request-failed-error": "Richiesta a {{target}} fallita {{status}}: {{message}}", "stellar-address-not-found-error": "Indirizzo stellare non trovato: {{address}}", "stellar-address-request-failed-error": "Risoluzione stellare dell'indirizzo di {{address}} non riuscita.", + "stellar-uri-verification-error": "Non è stato possibile verificare la firma di Stellar URI.", "submission-failed-error": "Invio della transazione a {{endpoint}} fallita {{status}}: {{message}}", "testnet-endpoint-not-available-error": "{{service}} non fornisce un endpoint testnet.", "timeout-error": "Richiesta scaduta", "unexpected-action-error": "Azione imprevista: {{action}}", "unexpected-state-error": "Stato imprevisto riscontrato: {{state}}", "unexpected-response-type-error": "Tipo di risposta imprevista: {{type}} / ${dataType}", + "unexpected-stellar-uri-type-error": "L'uri in arrivo {{incomingURI}} non corrisponde a nessun tipo previsto.", "unknown-error": "Si è verificato un errore sconosciuto.", "update-already-running-error": "L'aggiornamento è già in esecuzione!", "wrong-password-error": "Password errata.", diff --git a/i18n/locales/it/payment.json b/i18n/locales/it/payment.json index 2e1f34323..f9d41fb83 100755 --- a/i18n/locales/it/payment.json +++ b/i18n/locales/it/payment.json @@ -1,5 +1,6 @@ { "actions": { + "dismiss": "Respingi", "submit": "Spedisci ora" }, "memo-metadata": { diff --git a/i18n/locales/it/transaction-request.json b/i18n/locales/it/transaction-request.json new file mode 100644 index 000000000..e492c596b --- /dev/null +++ b/i18n/locales/it/transaction-request.json @@ -0,0 +1,64 @@ +{ + "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", + "select": "Select" + }, + "error": { + "no-activated-accounts": "No activated accounts found.", + "no-accounts-with-trustline": "No accounts with matching trustline found." + }, + "uri-content": { + "message": "Message" + } + }, + "transaction": { + "account-selector": { + "source-account": "Select the source account", + "signing-account": "Select the account that will sign the transaction" + }, + "action": { + "dismiss": "Dismiss", + "select": "Select" + }, + "error": { + "signer-not-imported": "The transaction request specified '{{signer}}' as the target signer but this account is not imported.", + "no-eligible-accounts": "No eligible account found." + }, + "hint": "Hint", + "uri-content": { + "message": "Message" + }, + "warning": "The origin of this request cannot be verified! Decline when in doubt." + }, + "stellar-uri": { + "header": { + "origin-domain": "The following transaction has been proposed by <1>{{originDomain}}<3>", + "warning": "The origin of this request cannot be verified! Decline when in doubt." + }, + "title": "Transaction proposed" + }, + "verify-trusted-service": { + "action": { + "trust": "Trust", + "cancel": "Cancel" + }, + "info": { + "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." + }, + "title": "Verify Trusted Service" + } +} diff --git a/i18n/locales/ru/generic.json b/i18n/locales/ru/generic.json index fa8012ffe..fffb42aeb 100644 --- a/i18n/locales/ru/generic.json +++ b/i18n/locales/ru/generic.json @@ -40,12 +40,14 @@ "request-failed-error": "Запрос к {{target}} завершился с ошибкой статуса {{status}}: {{message}}", "stellar-address-not-found-error": "Адрес Stellar не найден: {{address}}", "stellar-address-request-failed-error": "Не удалось найти адрес Stellar {{address}}.", + "stellar-uri-verification-error": "Не удалось верифицировать Stellar URI.", "submission-failed-error": "Не удалось отправить транзакцию на {{endpoint}} с ошибкой статуса {{status}}: {{message}}", "testnet-endpoint-not-available-error": "{{service}} не предоставляет конечную точку тестовой сети.", "timeout-error": "Время запроса истекло", "unexpected-action-error": "Неожиданное действие: {{action}}", "unexpected-state-error": "Обнаружено неожиданное состояние: {{state}}", "unexpected-response-type-error": "Неожиданный тип ответа: {{type}} / ${dataType}", + "unexpected-stellar-uri-type-error": "Запрос {{incomingURI}} не поддерживается", "unknown-error": "Произошла неизвестная ошибка.", "update-already-running-error": "Обновление уже запущено!", "wrong-password-error": "Неверный пароль.", diff --git a/i18n/locales/ru/payment.json b/i18n/locales/ru/payment.json index 063548555..49b5ae860 100644 --- a/i18n/locales/ru/payment.json +++ b/i18n/locales/ru/payment.json @@ -1,5 +1,6 @@ { "actions": { + "dismiss": "Отменить", "submit": "Отправить сейчас" }, "memo-metadata": { diff --git a/i18n/locales/ru/transaction-request.json b/i18n/locales/ru/transaction-request.json new file mode 100644 index 000000000..f990076dc --- /dev/null +++ b/i18n/locales/ru/transaction-request.json @@ -0,0 +1,64 @@ +{ + "no-accounts": { + "action": { + "dismiss": "Закрыть" + }, + "info": { + "1": "Нет аккаунтов для указанной сети.", + "2": "Вы должны добавить аккаунт, чтобы подписывать платёжные запросы" + }, + "title": "Предложена транзакция" + }, + "payment": { + "account-selector": "Выберите аккаунт", + "action": { + "dismiss": "Отменить", + "select": "Выбрать" + }, + "error": { + "no-activated-accounts": "Не найдено активных аккаунтов.", + "no-accounts-with-trustline": "Нет аккаунтов с подходящей линией доверия." + }, + "uri-content": { + "message": "Сообщение" + } + }, + "transaction": { + "account-selector": { + "source-account": "Выберите аккаунт источник", + "signing-account": "Выберите аккаунт, который будет подписывать" + }, + "action": { + "dismiss": "Отмена", + "select": "Выбрать" + }, + "error": { + "signer-not-imported": "Аккаунт '{{signer}}' выбран для подписи, но он не импортирован.", + "no-eligible-accounts": "Нет подходящих аккаунтов." + }, + "hint": "Подсказка", + "uri-content": { + "message": "Сообщение" + }, + "warning": "Источник этого запроса не может быть верифицирован. Отклоните запрос, если сомневаетесь." + }, + "stellar-uri": { + "header": { + "origin-domain": "Данная транзакция была предложена <1>{{originDomain}}<3>", + "warning": "Источник этого запроса не может быть верифицирован. Отклоните запрос, если сомневаетесь." + }, + "title": "Предложена транзакция" + }, + "verify-trusted-service": { + "action": { + "trust": "Доверять", + "cancel": "Отменить" + }, + "info": { + "1": "Вы открыли Stellar URI от неизвестного источника", + "2": "Если вы доверяете этому домену, то вы можете добавить этот сервис в список доверенных.", + "3": "Вы можете смотреть и редактировать список доверенных сервисов в настройках приложения." + }, + "title": "Верифицировать доверенный сервис" + } +} diff --git a/i18n/ru.ts b/i18n/ru.ts index aac7f223b..7a4e05092 100644 --- a/i18n/ru.ts +++ b/i18n/ru.ts @@ -7,6 +7,7 @@ import Generic from "./locales/ru/generic.json" import Operations from "./locales/ru/operations.json" import Payment from "./locales/ru/payment.json" import Trading from "./locales/ru/trading.json" +import TransactionRequest from "./locales/ru/transaction-request.json" import TransferService from "./locales/ru/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 diff --git a/src/App/contexts/transactionRequest.tsx b/src/App/contexts/transactionRequest.tsx index 3d7087ed2..4c56da301 100644 --- a/src/App/contexts/transactionRequest.tsx +++ b/src/App/contexts/transactionRequest.tsx @@ -4,6 +4,7 @@ import { CustomError } from "~Generic/lib/errors" import { subscribeToDeepLinkURLs } from "~Platform/protocol-handler" import { verifyTransactionRequest } from "~Transaction/lib/stellar-uri" import { trackError } from "./notifications" +import { useTranslation } from "react-i18next" const allowUnsafeTestnetURIs = Boolean(process.env.ALLOW_UNSAFE_TESTNET_URIS) @@ -26,6 +27,8 @@ const TransactionRequestContext = React.createContext(initialValues export function TransactionRequestProvider(props: Props) { const [uri, setURI] = React.useState(null) + const { t } = useTranslation() + const clearURI = React.useCallback(() => setURI(null), []) const verifyStellarURI = React.useCallback(async (incomingURI: string) => { @@ -56,8 +59,9 @@ export function TransactionRequestProvider(props: Props) { trackError( CustomError( "UnexpectedStellarUriTypeError", - `Incoming uri ${incomingURI} does not match any expected type.`, - { incomingURI } + t("unexpected-stellar-uri-type-error", `Incoming uri ${incomingURI} does not match any expected type.`, { + incomingURI + }) ) ) break diff --git a/src/Transaction/lib/stellar-uri.ts b/src/Transaction/lib/stellar-uri.ts index 355109e7f..7b1c3afe6 100644 --- a/src/Transaction/lib/stellar-uri.ts +++ b/src/Transaction/lib/stellar-uri.ts @@ -1,3 +1,4 @@ +import i18next from "../../App/i18n" import { parseStellarUri } from "@stellarguard/stellar-uri" import { CustomError } from "~Generic/lib/errors" @@ -13,7 +14,7 @@ export async function verifyTransactionRequest(request: string, options: Verific if (parsedURI.isTestNetwork && options.allowUnsafeTestnetURIs) { // ignore } else { - throw CustomError("StellarUriVerificationError", "Stellar URI's signature could not be verified.") + throw CustomError("StellarUriVerificationError", i18next.t("stellar-uri-verification-error")) } } From 48b3a5a2a6028996b6357c034f75bf90fab7e0bd Mon Sep 17 00:00:00 2001 From: Kosta Korenkov Date: Thu, 24 Oct 2024 15:12:42 +0200 Subject: [PATCH 52/55] enable SEP-7 registration for electron app --- electron/src/protocol-handler.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/electron/src/protocol-handler.ts b/electron/src/protocol-handler.ts index f73e9a783..165cc4829 100644 --- a/electron/src/protocol-handler.ts +++ b/electron/src/protocol-handler.ts @@ -20,9 +20,7 @@ expose(Messages.IsDifferentHandlerInstalled, () => { }) expose(Messages.SetAsDefaultProtocolClient, () => { - // Disabled until we actually ship SEP-7 support - // return app.setAsDefaultProtocolClient("web+stellar") - return false + return app.setAsDefaultProtocolClient("web+stellar") }) export function subscribe(subscribeCallback: (...args: any[]) => void) { From 5c1d8800563adfce86dfeeaa0233200f121a2948 Mon Sep 17 00:00:00 2001 From: Kosta Korenkov Date: Thu, 24 Oct 2024 15:13:13 +0200 Subject: [PATCH 53/55] register web+stellar handler on menu item click for web app --- src/Platform/ipc/web.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Platform/ipc/web.ts b/src/Platform/ipc/web.ts index cd9f381ec..669a1ffbf 100644 --- a/src/Platform/ipc/web.ts +++ b/src/Platform/ipc/web.ts @@ -64,6 +64,11 @@ let differentHandler = true callHandlers[Messages.IsDifferentHandlerInstalled] = () => differentHandler callHandlers[Messages.IsDefaultProtocolClient] = () => isDefault callHandlers[Messages.SetAsDefaultProtocolClient] = () => { + window.navigator.registerProtocolHandler( + "web+stellar", + `${window.location.origin}/?uri=%s`, + "Stellar request handler" + ) isDefault = true differentHandler = false return true @@ -198,12 +203,6 @@ function initSettings() { } function subscribeToDeepLinkURLs(callback: (url: string) => void) { - window.navigator.registerProtocolHandler( - "web+stellar", - `${window.location.origin}/?uri=%s`, - "Stellar request handler" - ) - // check if a stellar uri has been passed already const uri = new URLSearchParams(window.location.search).get("uri") if (uri) { From db5efb1bc579be5437d3b8db660c9980c8730841 Mon Sep 17 00:00:00 2001 From: Kosta Korenkov Date: Thu, 24 Oct 2024 17:53:18 +0200 Subject: [PATCH 54/55] show uri handler permission for all the platforms except mobiles --- src/App/components/AccountListView.tsx | 4 +--- src/App/cordova/protocol-handler.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/App/components/AccountListView.tsx b/src/App/components/AccountListView.tsx index 9da6c803f..ea32e75ea 100644 --- a/src/App/components/AccountListView.tsx +++ b/src/App/components/AccountListView.tsx @@ -139,9 +139,7 @@ function AllAccountsPage() { onCreateTestnetAccount={() => router.history.push(routes.newAccount(true))} /> - {process.env.PLATFORM === "linux" || process.env.PLATFORM === "darwin" || process.env.PLATFORM === "win32" ? ( - - ) : null} + ) { window.handleOpenURL = handleOpenURL(contentWindow, iframeReady) + + // there is no way we can check for default handler in cordova + expose(Messages.IsDefaultProtocolClient, () => { + return true + }) + + expose(Messages.IsDifferentHandlerInstalled, () => { + return false + }) + + expose(Messages.SetAsDefaultProtocolClient, () => { + return true + }) } const handleOpenURL = (contentWindow: Window, iframeReady: Promise) => (url: string) => { From 152b63d8149810ac775570b83d398a140c3a5cbc Mon Sep 17 00:00:00 2001 From: Soz Nov Date: Sun, 10 Nov 2024 19:35:01 +0200 Subject: [PATCH 55/55] bump version 1.1.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 131e1f778..21752eae4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "org.montelibero.sunce", - "version": "1.0.1", + "version": "1.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "org.montelibero.sunce", - "version": "1.0.1", + "version": "1.1.0", "license": "MIT", "dependencies": { "@stellarguard/stellar-uri": "^2.0.0", diff --git a/package.json b/package.json index 3d9ac56ba..813a0265b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "org.montelibero.sunce", "displayName": "Sunce Wallet", - "version": "1.0.1", + "version": "1.1.0", "description": "Wallet for the Stellar payment network by Montelibero.", "license": "MIT", "private": true,