+ {/* Generate payment URL for Address action */}
+
+
+
{/* 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