diff --git a/apps/desktop/public/electron.js b/apps/desktop/public/electron.js
index 2f3aa9f383..1457209a00 100644
--- a/apps/desktop/public/electron.js
+++ b/apps/desktop/public/electron.js
@@ -97,6 +97,13 @@ function createWindow() {
}
});
+ mainWindow.webContents.session.webRequest.onBeforeRequest((details, callback) => {
+ if (details.url.startsWith("http://")) {
+ return callback({ cancel: true });
+ }
+ callback({});
+ });
+
protocol.handle(APP_PROTOCOL, async req => {
try {
const uri = new URL(decodeURI(req.url));
diff --git a/apps/desktop/src/views/settings/network/UpsertNetworkModal.test.tsx b/apps/desktop/src/views/settings/network/UpsertNetworkModal.test.tsx
index fd1524f60c..42f1ded0b7 100644
--- a/apps/desktop/src/views/settings/network/UpsertNetworkModal.test.tsx
+++ b/apps/desktop/src/views/settings/network/UpsertNetworkModal.test.tsx
@@ -29,9 +29,9 @@ describe("", () => {
const updatedNetwork = {
...customNetwork,
- rpcUrl: "https://rpc",
- tzktApiUrl: "https://tzkt",
- tzktExplorerUrl: "https://explorer",
+ rpcUrl: "https://rpc.com",
+ tzktApiUrl: "https://tzkt.com",
+ tzktExplorerUrl: "https://explorer.com",
buyTezUrl: "",
};
@@ -57,9 +57,9 @@ describe("", () => {
const updatedNetwork = {
...customNetwork,
- rpcUrl: "https://rpc",
- tzktApiUrl: "https://tzkt",
- tzktExplorerUrl: "https://explorer",
+ rpcUrl: "https://rpc.com",
+ tzktApiUrl: "https://tzkt.com",
+ tzktExplorerUrl: "https://explorer.com",
buyTezUrl: "",
};
diff --git a/apps/desktop/src/views/settings/network/UpsertNetworkModal.tsx b/apps/desktop/src/views/settings/network/UpsertNetworkModal.tsx
index 419df9af0f..0bfbff3182 100644
--- a/apps/desktop/src/views/settings/network/UpsertNetworkModal.tsx
+++ b/apps/desktop/src/views/settings/network/UpsertNetworkModal.tsx
@@ -9,7 +9,7 @@ import {
ModalFooter,
ModalHeader,
} from "@chakra-ui/react";
-import { useDynamicModalContext } from "@umami/components";
+import { useDynamicModalContext, validateUrl } from "@umami/components";
import { networksActions, useAvailableNetworks } from "@umami/state";
import { type Network } from "@umami/tezos";
import { useForm } from "react-hook-form";
@@ -69,6 +69,7 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
{...register("rpcUrl", {
required: "RPC URL is required",
setValueAs: removeTrailingSlashes,
+ validate: validateUrl,
})}
/>
{errors.rpcUrl && {errors.rpcUrl.message}}
@@ -80,6 +81,7 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
{...register("tzktApiUrl", {
required: "Tzkt API URL is required",
setValueAs: removeTrailingSlashes,
+ validate: validateUrl,
})}
/>
{errors.tzktApiUrl && {errors.tzktApiUrl.message}}
@@ -91,6 +93,7 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
{...register("tzktExplorerUrl", {
required: "Tzkt Explorer URL is required",
setValueAs: removeTrailingSlashes,
+ validate: validateUrl,
})}
/>
{errors.tzktExplorerUrl && (
@@ -100,10 +103,15 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
Buy Tez URL
-
+ validateUrl(url, { allowEmpty: true }),
+ })}
+ />
-
diff --git a/apps/web/src/components/Menu/NetworkMenu/EditNetworkMenu.test.tsx b/apps/web/src/components/Menu/NetworkMenu/EditNetworkMenu.test.tsx
index 7a6c5fca7b..64176f7db4 100644
--- a/apps/web/src/components/Menu/NetworkMenu/EditNetworkMenu.test.tsx
+++ b/apps/web/src/components/Menu/NetworkMenu/EditNetworkMenu.test.tsx
@@ -13,9 +13,7 @@ beforeEach(() => {
describe("", () => {
describe("edit mode", () => {
- beforeEach(() => {
- store.dispatch(networksActions.upsertNetwork(customNetwork));
- });
+ beforeEach(() => store.dispatch(networksActions.upsertNetwork(customNetwork)));
it("doesn't render name field", async () => {
await renderInDrawer(, store);
@@ -29,9 +27,9 @@ describe("", () => {
const updatedNetwork = {
...customNetwork,
- rpcUrl: "https://rpc",
- tzktApiUrl: "https://tzkt",
- tzktExplorerUrl: "https://explorer",
+ rpcUrl: "https://rpc.com",
+ tzktApiUrl: "https://tzkt.com",
+ tzktExplorerUrl: "https://explorer.com",
buyTezUrl: "",
};
@@ -57,9 +55,9 @@ describe("", () => {
const updatedNetwork = {
...customNetwork,
- rpcUrl: "https://rpc",
- tzktApiUrl: "https://tzkt",
- tzktExplorerUrl: "https://explorer",
+ rpcUrl: "https://rpc.com",
+ tzktApiUrl: "https://tzkt.com",
+ tzktExplorerUrl: "https://explorer.com",
buyTezUrl: "",
};
diff --git a/apps/web/src/components/Menu/NetworkMenu/EditNetworkMenu.tsx b/apps/web/src/components/Menu/NetworkMenu/EditNetworkMenu.tsx
index 35d4453fba..1f0f4acde3 100644
--- a/apps/web/src/components/Menu/NetworkMenu/EditNetworkMenu.tsx
+++ b/apps/web/src/components/Menu/NetworkMenu/EditNetworkMenu.tsx
@@ -1,5 +1,5 @@
import { Button, FormControl, FormErrorMessage, FormLabel, Input, VStack } from "@chakra-ui/react";
-import { useDynamicDrawerContext } from "@umami/components";
+import { useDynamicDrawerContext, validateUrl } from "@umami/components";
import { networksActions, useAppDispatch, useAvailableNetworks } from "@umami/state";
import { type Network } from "@umami/tezos";
import { useForm } from "react-hook-form";
@@ -56,6 +56,7 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
{...register("rpcUrl", {
required: "RPC URL is required",
setValueAs: removeTrailingSlashes,
+ validate: validateUrl,
})}
/>
{errors.rpcUrl && {errors.rpcUrl.message}}
@@ -67,6 +68,7 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
{...register("tzktApiUrl", {
required: "Tzkt API URL is required",
setValueAs: removeTrailingSlashes,
+ validate: validateUrl,
})}
/>
{errors.tzktApiUrl && {errors.tzktApiUrl.message}}
@@ -78,6 +80,7 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
{...register("tzktExplorerUrl", {
required: "Tzkt Explorer URL is required",
setValueAs: removeTrailingSlashes,
+ validate: validateUrl,
})}
/>
{errors.tzktExplorerUrl && (
@@ -87,7 +90,12 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
Buy Tez URL
-
+ validateUrl(url, { allowEmpty: true }),
+ })}
+ />
diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts
index ab8d00f841..3ae632992e 100644
--- a/packages/components/src/index.ts
+++ b/packages/components/src/index.ts
@@ -3,3 +3,4 @@ export * from "./DynamicDisclosure";
export * from "./hooks";
export * from "./ReactIdenticon";
export * from "./MnemonicAutocomplete";
+export * from "./validateUrl";
diff --git a/packages/components/src/validateUrl/index.ts b/packages/components/src/validateUrl/index.ts
new file mode 100644
index 0000000000..ac6832fef2
--- /dev/null
+++ b/packages/components/src/validateUrl/index.ts
@@ -0,0 +1 @@
+export * from "./validateUrl";
diff --git a/packages/components/src/validateUrl/validateUrl.test.ts b/packages/components/src/validateUrl/validateUrl.test.ts
new file mode 100644
index 0000000000..3021e0bd9f
--- /dev/null
+++ b/packages/components/src/validateUrl/validateUrl.test.ts
@@ -0,0 +1,38 @@
+import { validateUrl } from "./validateUrl";
+
+describe("validateUrl", () => {
+ it.each(["mailto", "http", "umami", "file"])(
+ "returns 'Invalid URL' for %s protocol",
+ protocol => {
+ expect(validateUrl(`${protocol}://example.com`)).toBe("Invalid URL");
+ }
+ );
+
+ describe("empty URL", () => {
+ it("returns 'Invalid URL'", () => {
+ expect(validateUrl("")).toBe("Invalid URL");
+ expect(validateUrl(undefined)).toBe("Invalid URL");
+ });
+
+ it("returns true if allowEmpty option is set", () => {
+ expect(validateUrl("", { allowEmpty: true })).toBe(true);
+ expect(validateUrl(undefined, { allowEmpty: true })).toBe(true);
+ });
+ });
+
+ it('returns "Invalid URL" for invalid URL', () => {
+ expect(validateUrl("invalid url")).toBe("Invalid URL");
+ expect(validateUrl("http://invalid url")).toBe("Invalid URL");
+ expect(validateUrl("https://invalid url")).toBe("Invalid URL");
+ expect(validateUrl("https://")).toBe("Invalid URL");
+ expect(validateUrl("https:/example.com")).toBe("Invalid URL");
+ expect(validateUrl("https:/example")).toBe("Invalid URL");
+ });
+
+ it("returns true for a valid url", () => {
+ expect(validateUrl("https://example.com")).toBe(true);
+ expect(validateUrl("https://example.com:8080")).toBe(true);
+ expect(validateUrl("https://example.com/path")).toBe(true);
+ expect(validateUrl("https://example.com/path?query=1")).toBe(true);
+ });
+});
diff --git a/packages/components/src/validateUrl/validateUrl.ts b/packages/components/src/validateUrl/validateUrl.ts
new file mode 100644
index 0000000000..430e17684a
--- /dev/null
+++ b/packages/components/src/validateUrl/validateUrl.ts
@@ -0,0 +1,23 @@
+const URL_REGEX = new RegExp(
+ "^(https:\\/\\/)?" + // protocol
+ "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
+ "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR IP (v4) address
+ "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
+ "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
+ "(\\#[-a-z\\d_]*)?$", // fragment locator
+ "i"
+);
+
+const ERROR_MESSAGE = "Invalid URL";
+
+export const validateUrl = (url: string | undefined, { allowEmpty } = { allowEmpty: false }) => {
+ try {
+ if (!url) {
+ return allowEmpty || ERROR_MESSAGE;
+ }
+ new URL(url);
+ return URL_REGEX.test(url) || ERROR_MESSAGE;
+ } catch {
+ return ERROR_MESSAGE;
+ }
+};