diff --git a/app/Routes.js b/app/Routes.js index 295dbf7033..68317327c1 100644 --- a/app/Routes.js +++ b/app/Routes.js @@ -29,6 +29,7 @@ const WalletSendPage = resolver('containers/wallet/WalletSendPage'); const WalletReceivePage = resolver('containers/wallet/WalletReceivePage'); const DaedalusTransferPage = resolver('containers/transfer/DaedalusTransferPage'); const AdaRedemptionPage = resolver('containers/wallet/AdaRedemptionPage'); +const URILandingPage = resolver('containers/uri/URILandingPage'); /* eslint-disable max-len */ export const Routes = ( @@ -83,6 +84,11 @@ export const Routes = ( path={ROUTES.DAEDALUS_TRANFER.ROOT} component={(props) => } /> + } + /> diff --git a/app/assets/images/generate-uri.inline.svg b/app/assets/images/generate-uri.inline.svg new file mode 100644 index 0000000000..2e1114d7fb --- /dev/null +++ b/app/assets/images/generate-uri.inline.svg @@ -0,0 +1,12 @@ + + + + icon/generate-url.inline + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/app/assets/images/uri/invalid-uri.inline.svg b/app/assets/images/uri/invalid-uri.inline.svg new file mode 100644 index 0000000000..1bad1829ee --- /dev/null +++ b/app/assets/images/uri/invalid-uri.inline.svg @@ -0,0 +1,35 @@ + + + + invalid-url.inline + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/uri/perform-tx-uri.inline.svg b/app/assets/images/uri/perform-tx-uri.inline.svg new file mode 100644 index 0000000000..767a6a3616 --- /dev/null +++ b/app/assets/images/uri/perform-tx-uri.inline.svg @@ -0,0 +1,64 @@ + + + + perform-tx-url.inline + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/components/profile/terms-of-use/TermsOfUseForm.js b/app/components/profile/terms-of-use/TermsOfUseForm.js index 78118dc56c..397f3def11 100644 --- a/app/components/profile/terms-of-use/TermsOfUseForm.js +++ b/app/components/profile/terms-of-use/TermsOfUseForm.js @@ -10,16 +10,13 @@ import { CheckboxSkin } from 'react-polymorph/lib/skins/simple/CheckboxSkin'; import LocalizableError from '../../../i18n/LocalizableError'; import TermsOfUseText from './TermsOfUseText'; import styles from './TermsOfUseForm.scss'; +import globalMessages from '../../../i18n/global-messages'; const messages = defineMessages({ checkboxLabel: { id: 'profile.termsOfUse.checkboxLabel', defaultMessage: '!!!I agree with the terms of use', }, - submitLabel: { - id: 'profile.termsOfUse.submitLabel', - defaultMessage: '!!!Continue', - }, }); type Props = {| @@ -82,7 +79,7 @@ export default class TermsOfUseForm extends Component { + {/* Verify Address action */}
svg { height: 20px; width: 20px; } - } + } + + .btnGenerateURI { + cursor: pointer; + } } - .verifyActionBlock { - margin-left: unset; - + .verifyActionBlock, .generateURLActionBlock { button { cursor: pointer; } } + + .generateURLActionBlock { + margin-left: unset; + } } } } @@ -196,7 +202,7 @@ font-family: var(--font-medium); font-weight: 600; margin-bottom: 10px; - } + } } } @@ -217,14 +223,14 @@ font-size: 14px; line-height: 1.57; } - + .hashLabel { font-size: 16px; margin-bottom: 30px; line-height: 1.31; font-family: var(--font-medium); } - + .instructionsText { font-size: 14px; letter-spacing: 0; @@ -235,22 +241,22 @@ .generatedAddresses { font-size: 14px; - + h2 { font-size: 16px; line-height: 1.31; letter-spacing: 0; margin-bottom: 20px; - + button { font-family: var(--font-medium); text-transform: uppercase; } } - + .walletAddress { padding: 12px 0; - + & + .walletAddress { border-top: 1px solid var(--theme-separation-border-color); } @@ -258,6 +264,7 @@ .addressMargin { margin-right: 32.5px; } + } - } -} \ No newline at end of file + } +} diff --git a/app/components/wallet/ada-redemption/AdaRedemptionDisclaimer.js b/app/components/wallet/ada-redemption/AdaRedemptionDisclaimer.js index 0a67b9c8c8..457a85e8c1 100644 --- a/app/components/wallet/ada-redemption/AdaRedemptionDisclaimer.js +++ b/app/components/wallet/ada-redemption/AdaRedemptionDisclaimer.js @@ -9,6 +9,7 @@ import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; import { CheckboxSkin } from 'react-polymorph/lib/skins/simple/CheckboxSkin'; import attentionIcon from '../../../assets/images/attention-big-light.inline.svg'; import styles from './AdaRedemptionDisclaimer.scss'; +import globalMessages from '../../../i18n/global-messages'; const messages = defineMessages({ disclaimerTitle: { @@ -23,10 +24,6 @@ const messages = defineMessages({ id: 'wallet.redeem.disclaimerOverlay.checkboxLabel', defaultMessage: '!!!I’ve understood the information above', }, - submitLabel: { - id: 'wallet.redeem.disclaimerOverlay.submitLabel', - defaultMessage: '!!!Continue', - }, }); type Props = {| @@ -78,7 +75,7 @@ export default class AdaRedemptionDisclaimer extends Component {
diff --git a/app/i18n/global-messages.js b/app/i18n/global-messages.js index 9cdbe53de6..2ed7dd26f4 100644 --- a/app/i18n/global-messages.js +++ b/app/i18n/global-messages.js @@ -48,6 +48,10 @@ const globalMessages = defineMessages({ id: 'global.labels.confirm', defaultMessage: '!!!Confirm', }, + continue: { + id: 'global.labels.continue', + defaultMessage: '!!!Continue', + }, finish: { id: 'global.labels.finish', defaultMessage: '!!!Finish', diff --git a/app/i18n/locales/en-US.json b/app/i18n/locales/en-US.json index 8174083b5b..b830c1a11c 100644 --- a/app/i18n/locales/en-US.json +++ b/app/i18n/locales/en-US.json @@ -207,6 +207,26 @@ "transfer.summary.recoveredBalance.label": "Recovered balance", "transfer.summary.transactionFee.label": "Transaction fees", "transfer.summary.transferButton.label": "Transfer Funds", + "uri.display.dialog.title": "Generated URL", + "uri.display.dialog.copy.notification": "URL successfully copied", + "uri.generate.dialog.title": "Generate URL", + "uri.generate.dialog.confirm.label": "Generate", + "uri.generate.dialog.address.label": "Receiver address", + "uri.generate.dialog.amount.label": "Amount (ADA)", + "uri.generate.dialog.invalid.amount": "Please enter a valid amount", + "uri.invalid.dialog.title": "Invalid URL", + "uri.invalid.dialog.warning.text1": "The link you clicked is invalid.", + "uri.invalid.dialog.warning.text2": "Please ask the receiver to double-check the format.", + "uri.landing.dialog.title": "Perform a transaction from a Cardano URL", + "uri.landing.dialog.warning.line1" : "Make sure:", + "uri.landing.dialog.warning.line2" : "You are on Yoroi's official extension.", + "uri.landing.dialog.warning.line3" : "You are not being victim of a phishing or man-in-the-middle attack.", + "uri.landing.dialog.confirm.label": "I understand", + "uri.verify.dialog.title": "Transaction details", + "uri.verify.dialog.address.label": "Receiver address", + "uri.verify.dialog.amount.label": "Amount (ADA)", + "uri.verify.dialog.text": "Before continuing, make sure the transaction details are correct.", + "wallet.add.dialog.create.description": "Create wallet", "wallet.add.dialog.createLedgerWalletNotificationMessage": "Ledger Connect is currently in progress. Until it completes, it is not possible to restore or import new wallets.", "wallet.add.dialog.createTrezorWalletNotificationMessage": "Trezor Connect is currently in progress. Until it completes, it is not possible to restore or import new wallets.", "wallet.add.dialog.restoreNotificationMessage": "Wallet restoration is currently in progress. Until it completes, it is not possible to restore or import new wallets.", @@ -300,6 +320,7 @@ "wallet.receive.confirmationDialog.verifyAddressButtonLabel": "Verify on hardware wallet", "wallet.receive.page.addressCopyNotificationMessage": "You have successfully copied your wallet address", "wallet.receive.page.copyAddressLabel": "Copy address", + "wallet.receive.page.generatePaymentURLLabel": "Generate payment URL", "wallet.receive.page.generateNewAddressButtonLabel": "Generate new address", "wallet.receive.page.generatedAddressesSectionTitle": "Generated addresses", "wallet.receive.page.hideUsedLabel": "hide used", @@ -454,4 +475,4 @@ "wallet.transaction.type.intrawallet": "{currency} intrawallet transaction", "wallet.transaction.type.multiparty": "{currency} multiparty transaction", "widgets.explorer.tooltip": "Go to {websiteName}" -} \ No newline at end of file +} diff --git a/app/routes-config.js b/app/routes-config.js index 4547c819c3..12234dcef1 100644 --- a/app/routes-config.js +++ b/app/routes-config.js @@ -25,5 +25,8 @@ export const ROUTES = { }, DAEDALUS_TRANFER: { ROOT: '/daedalus-transfer', - } + }, + SEND_FROM_URI: { + ROOT: '/send-from-uri', + }, }; diff --git a/app/stores/toplevel/LoadingStore.js b/app/stores/toplevel/LoadingStore.js index c26e20fa12..8580e73ecd 100644 --- a/app/stores/toplevel/LoadingStore.js +++ b/app/stores/toplevel/LoadingStore.js @@ -1,10 +1,13 @@ // @flow -import { observable, computed, when, runInAction } from 'mobx'; +import { action, observable, computed, when, runInAction } from 'mobx'; import { defineMessages } from 'react-intl'; import Store from '../base/Store'; import Wallet from '../../domain/Wallet'; import environment from '../../environment'; import { ROUTES } from '../../routes-config'; +import { matchRoute } from '../../utils/routing'; +import { getURIParameters } from '../../utils/URIHandling'; +import type { UriParams } from '../../utils/URIHandling'; import LocalizableError, { localizedError } from '../../i18n/LocalizableError'; @@ -29,6 +32,16 @@ export default class LoadingStore extends Store { @observable error: ?LocalizableError = null; @observable _loading: boolean = true; + /** + * null if app not opened from URI Scheme OR URI scheme was invalid + */ + @observable _uriParams: ?UriParams = null; + + _originRoute: { + route: string, // internal route + location: string, // full URL + } = { route: '', location: '' }; + @observable loadRustRequest: Request Promise> = new Request Promise>(RustModule.load.bind(RustModule)); @@ -52,6 +65,7 @@ export default class LoadingStore extends Store { api: this.api, currVersion: environment.version }).promise; + await this.validateUriPath(); await this._openPageAfterLoad(); runInAction(() => { this.error = null; @@ -71,11 +85,47 @@ export default class LoadingStore extends Store { return !!this._loading; } + @computed get fromUriScheme(): boolean { + return matchRoute(ROUTES.SEND_FROM_URI.ROOT, this._originRoute.route); + } + + @computed get uriParams(): ?UriParams { + return this._uriParams; + } + + @action + validateUriPath = async (): Promise => { + if (this.fromUriScheme) { + this._uriParams = await getURIParameters( + decodeURIComponent(this._originRoute.location), + this.stores.substores.ada.wallets.isValidAddress + ); + } + } + + /** + * Need to clear any data inijected by the URI after we've applied it + */ + @action + resetUriParams = (): void => { + this._uriParams = null; + this._originRoute = { route: '', location: '' }; + } + _isRefresh = (): boolean => this.isLoading; - _redirectToLoading = (): void => ( - this.actions.router.goToRoute.trigger({ route: ROUTES.ROOT }) - ); + _redirectToLoading = (): void => { + // before redirecting, save origin route in case we need to come back to + // it later (this is the case when user comes from a URI link) + runInAction(() => { + this._originRoute = { + route: this.stores.app.currentRoute, + location: window.location.href, + }; + // note: we don't validate the path since we need to wait for the WASM bindings to load first + }); + this.actions.router.goToRoute.trigger({ route: ROUTES.ROOT }); + } /** Select which page to open after app is done loading */ _openPageAfterLoad = async (): Promise => { @@ -89,10 +139,14 @@ export default class LoadingStore extends Store { // Dynamic Initialization of Topbar Categories this.stores.topbar.updateCategories(); - this.actions.router.goToRoute.trigger({ - route: ROUTES.WALLETS.TRANSACTIONS, - params: { id: firstWallet.id } - }); + if (this.fromUriScheme) { + this.actions.router.goToRoute.trigger({ route: ROUTES.SEND_FROM_URI.ROOT }); + } else { + this.actions.router.goToRoute.trigger({ + route: ROUTES.WALLETS.TRANSACTIONS, + params: { id: firstWallet.id } + }); + } } else { this.actions.router.goToRoute.trigger({ route: ROUTES.WALLETS.ADD }); } diff --git a/app/stores/toplevel/ProfileStore.js b/app/stores/toplevel/ProfileStore.js index ba88feedfe..e7cbc6a02f 100644 --- a/app/stores/toplevel/ProfileStore.js +++ b/app/stores/toplevel/ProfileStore.js @@ -9,6 +9,7 @@ import { THEMES } from '../../themes'; import type { Theme } from '../../themes'; import { ROUTES } from '../../routes-config'; import globalMessages from '../../i18n/global-messages'; +import registerProtocols from '../../uri-protocols'; import type { ExplorerType } from '../../domain/Explorer'; import type { GetSelectedExplorerFunc, SaveSelectedExplorerFunc, @@ -95,6 +96,7 @@ export default class ProfileStore extends Store { this._redirectToLanguageSelectionIfNoLocaleSet, this._redirectToTermsOfUseScreenIfTermsNotAccepted, this._redirectToMainUiAfterTermsAreAccepted, + this._attemptURIProtocolRegistrationIfNoLocaleSet, ]); this._getTermsOfUseAcceptance(); // eagerly cache } @@ -355,4 +357,14 @@ export default class ProfileStore extends Store { this._redirectToRoot(); } }; + + // ========== URI protocol registration ========== // + + _attemptURIProtocolRegistrationIfNoLocaleSet = () => { + const { isLoading } = this.stores.loading; + if (!isLoading && !this.areTermsOfUseAccepted && !this.isCurrentLocaleSet) { + // this is likely the first time the user launches the app + registerProtocols(); + } + }; } diff --git a/app/uri-protocols.js b/app/uri-protocols.js new file mode 100644 index 0000000000..2c886711af --- /dev/null +++ b/app/uri-protocols.js @@ -0,0 +1,33 @@ +// @flow + +import { ROUTES } from './routes-config'; +import { Logger, stringifyError } from './utils/logging'; + + +const cardanoURI = { + PROTOCOL: 'web+cardano', + URL: 'main_window.html#' + ROUTES.SEND_FROM_URI.ROOT + '?q=%s', + TITLE: 'Yoroi', +}; + +const registerProtocols = () => { + + // $FlowFixMe InstallTrigger is a global from the browser + const isFirefox = typeof InstallTrigger !== 'undefined'; + const isChrome = !!window.chrome && + (!!window.chrome.webstore || !!window.chrome.runtime) && + !isFirefox; + if (isChrome) { + try { + navigator.registerProtocolHandler( + cardanoURI.PROTOCOL, + cardanoURI.URL, + cardanoURI.TITLE + ); + } catch (err) { + Logger.error(`uri-protocols:registerProtocols ${stringifyError(err)}`); + } + } +}; + +export default registerProtocols; diff --git a/app/utils/URIHandling.js b/app/utils/URIHandling.js new file mode 100644 index 0000000000..b035946189 --- /dev/null +++ b/app/utils/URIHandling.js @@ -0,0 +1,62 @@ +// @flow + +// TODO: +// - change file name to something more generic like URIHandling.js + +import BigNumber from 'bignumber.js'; +import { DECIMAL_PLACES_IN_ADA } from '../config/numbersConfig'; +import { isValidAmountInLovelaces } from './validations'; + +export type UriParams = { + address: string, + amount: BigNumber, +} +/** + * retrieves URI parameters following the web+cardano protocol + */ +export const getURIParameters = async ( + uri: string, + addressValidator: (string => Promise) +): Promise => { + const params = {}; + if (!uri) uri = decodeURIComponent(window.location.href); + const addressRegex = new RegExp('cardano:([A-HJ-NP-Za-km-z1-9]+)'); + const addressMatch = addressRegex.exec(uri); + if (addressMatch && addressMatch[1]) { + if (!await addressValidator(addressMatch[1])) { + return null; + } + params.address = addressMatch[1]; + } else { + return null; + } + // consider use of URLSearchParams + const amountRegex = new RegExp('amount=([0-9]+\\.?[0-9]*)'); + const amountMatch = amountRegex.exec(uri); + if (amountMatch && amountMatch[1]) { + try { + const asNum = new BigNumber(amountMatch[1]); + const asString = asNum.shiftedBy(DECIMAL_PLACES_IN_ADA).toString(); + if (!isValidAmountInLovelaces(asString)) { + return null; + } + params.amount = asNum; + } catch (err) { + return null; + } + } else { + return null; + } + return params; +}; + +/** + * builds URI string according to web+cardano protocol + */ +export const buildURI = ( + address: string, + amount: number +): string => { + if (amount) return 'web+cardano:' + address + '?amount=' + amount; + return 'web+cardano:' + address; +}; diff --git a/app/utils/formatters.js b/app/utils/formatters.js index 84a484c426..dd96bb4667 100644 --- a/app/utils/formatters.js +++ b/app/utils/formatters.js @@ -13,6 +13,8 @@ export const formattedAmountToBigNumber = (amount: string) => { }; /** + * Returns number in lovelaces + * * removes leading zeros * ensures `DECIMAL_PLACES_IN_ADA` decimal positions * shifts decimal places over to turn into a whole number diff --git a/chrome/manifest.development.json b/chrome/manifest.development.json index 580b4dba16..a71c89231b 100644 --- a/chrome/manifest.development.json +++ b/chrome/manifest.development.json @@ -35,5 +35,12 @@ } ], "content_security_policy": "default-src 'self' http://localhost:3000 https://localhost:3000 http://localhost:8097; frame-src https://connect.trezor.io/ https://emurgo.github.io/yoroi-extension-ledger-bridge; script-src 'self' 'unsafe-eval' http://localhost:3000 https://localhost:3000 http://localhost:8097 blob:; object-src 'self'; connect-src https://iohk-mainnet.yoroiwallet.com wss://iohk-mainnet.yoroiwallet.com:443 http://localhost:3000 https://localhost:3000 http://localhost:8080 https://localhost:8080 http://localhost:8097 ws://localhost:8080 ws://localhost:8097 wss://localhost:8080 wss://stg-yoroi-backend.yoroiwallet.com:443 https://stg-yoroi-backend.yoroiwallet.com; style-src * 'unsafe-inline' 'self' blob:; img-src 'self' http://localhost:3000 data:;", - "key": "pojejnpjgcacmnpkdiklhlnlbkjechfh" + "key": "pojejnpjgcacmnpkdiklhlnlbkjechfh", + "protocol_handlers": [ + { + "protocol": "web+cardano", + "name": "Yoroi", + "uriTemplate": "main_window.html#/send-from-uri?q=%s" + } + ] } diff --git a/chrome/manifest.mainnet.json b/chrome/manifest.mainnet.json index 9cadaa384c..eee6d919ff 100644 --- a/chrome/manifest.mainnet.json +++ b/chrome/manifest.mainnet.json @@ -34,5 +34,12 @@ "js": ["js/trezor-content-script.js"] } ], - "content_security_policy": "default-src 'self'; frame-src https://connect.trezor.io/ https://emurgo.github.io/yoroi-extension-ledger-bridge; script-src 'self' 'unsafe-eval' blob:; connect-src https://iohk-mainnet.yoroiwallet.com wss://iohk-mainnet.yoroiwallet.com:443; style-src * 'unsafe-inline' 'self' blob:; img-src 'self' data:;" + "content_security_policy": "default-src 'self'; frame-src https://connect.trezor.io/ https://emurgo.github.io/yoroi-extension-ledger-bridge; script-src 'self' 'unsafe-eval' blob:; connect-src https://iohk-mainnet.yoroiwallet.com wss://iohk-mainnet.yoroiwallet.com:443; style-src * 'unsafe-inline' 'self' blob:; img-src 'self' data:;", + "protocol_handlers": [ + { + "protocol": "web+cardano", + "name": "Yoroi", + "uriTemplate": "main_window.html#/send-from-uri?q=%s" + } + ] } diff --git a/chrome/manifest.staging.json b/chrome/manifest.staging.json index 9d8d393a31..98a1520bdf 100644 --- a/chrome/manifest.staging.json +++ b/chrome/manifest.staging.json @@ -35,5 +35,12 @@ "js": ["js/trezor-content-script.js"] } ], - "content_security_policy": "default-src 'self'; frame-src https://connect.trezor.io/ https://emurgo.github.io/yoroi-extension-ledger-bridge; script-src 'self' 'unsafe-eval' blob:; connect-src wss://stg-yoroi-backend.yoroiwallet.com:443 https://stg-yoroi-backend.yoroiwallet.com; style-src * 'unsafe-inline' 'self' blob:; img-src 'self' data:;" + "content_security_policy": "default-src 'self'; frame-src https://connect.trezor.io/ https://emurgo.github.io/yoroi-extension-ledger-bridge; script-src 'self' 'unsafe-eval' blob:; connect-src wss://stg-yoroi-backend.yoroiwallet.com:443 https://stg-yoroi-backend.yoroiwallet.com; style-src * 'unsafe-inline' 'self' blob:; img-src 'self' data:;", + "protocol_handlers": [ + { + "protocol": "web+cardano", + "name": "Yoroi", + "uriTemplate": "main_window.html#/send-from-uri?q=%s" + } + ] } diff --git a/chrome/manifest.test.json b/chrome/manifest.test.json index 2dd81f5b00..89c380f419 100644 --- a/chrome/manifest.test.json +++ b/chrome/manifest.test.json @@ -34,5 +34,12 @@ "js": ["js/trezor-content-script.js"] } ], - "content_security_policy": "default-src 'self'; frame-src https://connect.trezor.io/ https://emurgo.github.io/yoroi-extension-ledger-bridge; script-src 'self' 'unsafe-eval' blob:; connect-src http://localhost:8080 https://localhost:8080 ws://localhost:8080 wss://localhost:8080; style-src * 'unsafe-inline' 'self' blob:; img-src 'self' data:;" + "content_security_policy": "default-src 'self'; frame-src https://connect.trezor.io/ https://emurgo.github.io/yoroi-extension-ledger-bridge; script-src 'self' 'unsafe-eval' blob:; connect-src http://localhost:8080 https://localhost:8080 ws://localhost:8080 wss://localhost:8080; style-src * 'unsafe-inline' 'self' blob:; img-src 'self' data:;", + "protocol_handlers": [ + { + "protocol": "web+cardano", + "name": "Yoroi", + "uriTemplate": "main_window.html#/send-from-uri?q=%s" + } + ] } diff --git a/chrome/manifest.testnet.json b/chrome/manifest.testnet.json index 3238912e57..a40389f00d 100644 --- a/chrome/manifest.testnet.json +++ b/chrome/manifest.testnet.json @@ -35,5 +35,12 @@ "js": ["js/trezor-content-script.js"] } ], - "content_security_policy": "default-src 'self'; frame-src https://connect.trezor.io/ https://emurgo.github.io/yoroi-extension-ledger-bridge; script-src 'self' 'unsafe-eval'; connect-src wss://testnet-yoroi-backend.yoroiwallet.com:443 https://testnet-yoroi-backend.yoroiwallet.com; style-src * 'unsafe-inline' 'self' blob:; img-src 'self' data:;" + "content_security_policy": "default-src 'self'; frame-src https://connect.trezor.io/ https://emurgo.github.io/yoroi-extension-ledger-bridge; script-src 'self' 'unsafe-eval'; connect-src wss://testnet-yoroi-backend.yoroiwallet.com:443 https://testnet-yoroi-backend.yoroiwallet.com; style-src * 'unsafe-inline' 'self' blob:; img-src 'self' data:;", + "protocol_handlers": [ + { + "protocol": "web+cardano", + "name": "Yoroi", + "uriTemplate": "main_window.html#/send-from-uri?q=%s" + } + ] } diff --git a/features/main-ui.feature b/features/main-ui.feature index 7571cc3ff2..1fdacf6c50 100644 --- a/features/main-ui.feature +++ b/features/main-ui.feature @@ -43,4 +43,4 @@ Feature: Main UI Scenario: User can't restore Daedalus wallet in Yoroi if Yoroi wallet is not created (IT-30) When There is no wallet stored And I am on the Daedalus Transfer instructions screen - Then I see transactions buttons are disabled \ No newline at end of file + Then I see transactions buttons are disabled diff --git a/features/step_definitions/uri-steps.js b/features/step_definitions/uri-steps.js new file mode 100644 index 0000000000..5be271cba6 --- /dev/null +++ b/features/step_definitions/uri-steps.js @@ -0,0 +1,80 @@ +// @flow + +import { When, Then } from 'cucumber'; +import { By } from 'selenium-webdriver'; +import { expect } from 'chai'; +import { getExtensionId } from '../support/helpers/route-helpers'; + +When(/^I click on "generate payment URL" button$/, async function () { + await this.click('.WalletReceive_btnGenerateURI'); + await this.waitForElement('.URIGenerateDialog'); +}); + +Then(/^I generate a URI for ([0-9]+) ADA$/, async function (amount) { + await this.input("input[name='amount']", amount); + await this.click('.URIGenerateDialog .primary'); +}); + +Then(/^I should see the URI displayed in a new dialog$/, async function () { + await this.waitForElement('.URIDisplayDialog'); +}); + +Then(/^I click on the copy to clipboard icon$/, async function () { + await this.click('.URIDisplayDialog_copyIcon'); +}); + +Then(/^I should see "URL successfully copied" notification:$/, async function (data) { + const notification = data.hashes()[0]; + const notificationMessage = await this.intl(notification.message); + await this.waitForElement(`//div[@class='NotificationMessage_message'][contains(text(), '${notificationMessage}')]`, By.xpath); +}); + +When(/^I open a cardano URI for address (([^"]*)) and ([0-9]+) ADA$/, async function (address, amount) { + // retreive extension ID in order to build an absolute URL with the cardano URI embeded + const extId = await getExtensionId.call(this); + expect(extId).to.not.be.null; + // In practice, clicking a cardano URI will cause the browser to open a URL of this form + const uri = 'chrome-extension://' + extId + + '/main_window.html#/send-from-uri?q=web+cardano:' + address + '?amount=' + amount; + await this.driver.get('about:blank'); // dummy step, but needed + await this.driver.get(uri); +}); + +Then(/^I should see and accept a warning dialog$/, async function () { + await this.waitForElement('.URILandingDialog'); + await this.click('.URILandingDialog .primary'); +}); + +Then(/^I should see a dialog with the transaction details$/, async function (table) { + const fields = table.hashes()[0]; + await this.waitForElement('.URIVerifyDialog'); + await this.waitUntilContainsText('.URIVerifyDialog_address', fields.address); + await this.waitUntilContainsText('.URIVerifyDialog_amount', fields.amount); +}); + +When(/^I confirm the URI transaction details$/, async function () { + await this.click('.URIVerifyDialog .primary'); +}); + +Then(/^I should land on send wallet screen with prefilled parameters$/, async function (table) { + const fields = table.hashes()[0]; + const rxInput = await this.driver.findElement(By.xpath("//input[@name='receiver']")).getAttribute('value'); + expect(rxInput).to.be.equal(fields.address); + const amountInput = await this.driver.findElement(By.xpath("//input[@name='amount']")).getAttribute('value'); + expect(amountInput).to.be.equal(fields.amount); +}); + +When(/^I open an invalid cardano URI$/, async function () { + const extId = await getExtensionId.call(this); + expect(extId).to.not.be.null; + const invalidAddress = 'Ae2tdPwUPEZKmw0y3AU3cXb5Chnasj6mvVNxV1H11997q3VW5IhbSfQwGpm'; + const amount = '1'; + const uri = 'chrome-extension://' + extId + + '/main_window.html#/send-from-uri?q=web+cardano:' + invalidAddress + '?amount=' + amount; + await this.driver.get('about:blank'); // dummy step, but needed + await this.driver.get(uri); +}); + +Then(/^I should see an "invalid URI" dialog$/, async function () { + await this.waitForElement('.URIInvalidDialog'); +}); diff --git a/features/support/helpers/route-helpers.js b/features/support/helpers/route-helpers.js index 0baf020c70..6137723b81 100644 --- a/features/support/helpers/route-helpers.js +++ b/features/support/helpers/route-helpers.js @@ -18,3 +18,14 @@ export const navigateTo = function (requestedRoute: string) { window.yoroi.actions.router.goToRoute.trigger({ route }); }, requestedRoute); }; + +export const getExtensionId = function (): Promise { + const context = this; + return context.driver.wait(async () => { + const pageUrl = await context.driver.getCurrentUrl(); + const extIdRegex = new RegExp('extension://([-a-z0-9]+)/main_window'); + const matchExtId = extIdRegex.exec(pageUrl); + if (matchExtId && matchExtId[1]) return matchExtId[1]; + return null; + }); +}; diff --git a/features/uri-scheme.feature b/features/uri-scheme.feature new file mode 100644 index 0000000000..8ee545acdc --- /dev/null +++ b/features/uri-scheme.feature @@ -0,0 +1,35 @@ +Feature: URI scheme + + Background: + Given I have opened the extension + And I have completed the basic setup + And There is a wallet stored named empty-wallet + + @it-107 + Scenario: Ensure user can generate a wallet URI and copy it to clipboard (IT-107) + When I go to the receive screen + Then I should see the Receive screen + When I click on "generate payment URL" button + And I generate a URI for 10 ADA + Then I should see the URI displayed in a new dialog + When I click on the copy to clipboard icon + Then I should see "URL successfully copied" notification: + | message | + | uri.display.dialog.copy.notification | + + @it-108 + Scenario: Ensure user can send a tx from a URI link (IT-108) + When I open a cardano URI for address Ae2tdPwUPEZKmwoy3AU3cXb5Chnasj6mvVNxV1H11997q3VW5ihbSfQwGpm and 10 ADA + Then I should see and accept a warning dialog + Then I should see a dialog with the transaction details + | address | amount | + | Ae2tdPwUPEZKmwoy3AU3cXb5Chnasj6mvVNxV1H11997q3VW5ihbSfQwGpm | 10 | + When I confirm the URI transaction details + Then I should land on send wallet screen with prefilled parameters + | address | amount | + | Ae2tdPwUPEZKmwoy3AU3cXb5Chnasj6mvVNxV1H11997q3VW5ihbSfQwGpm | 10.000000 | + + @it-109 + Scenario: Invalid URI leads user to warning dialog (IT-109) + When I open an invalid cardano URI + Then I should see an "invalid URI" dialog