Skip to content
This repository has been archived by the owner on Mar 23, 2023. It is now read-only.

Commit

Permalink
feat: AIP26 handling (#2929)
Browse files Browse the repository at this point in the history
  • Loading branch information
clucasalcantara authored Oct 14, 2020
1 parent 021b1c4 commit d092c43
Show file tree
Hide file tree
Showing 13 changed files with 2,057 additions and 176 deletions.
6 changes: 6 additions & 0 deletions src/app/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { translations as errorTranslations } from "domains/error/i18n";
import { translations as profileTranslations } from "domains/profile/i18n";
import { ipcRenderer } from "electron";
import electron from "electron";
import nock from "nock";
import React from "react";
Expand All @@ -18,6 +19,7 @@ import {
import { App } from "./App";

jest.mock("electron", () => ({
ipcRenderer: { on: jest.fn(), send: jest.fn(), removeListener: jest.fn() },
remote: {
nativeTheme: {
shouldUseDarkColors: true,
Expand All @@ -42,6 +44,10 @@ describe("App", () => {
.persist();
});

beforeEach(() => {
ipcRenderer.on.mockImplementationOnce((event, callback) => callback(event, null));
});

it("should render splash screen", async () => {
process.env.REACT_APP_BUILD_MODE = "demo";

Expand Down
5 changes: 3 additions & 2 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,21 @@ import { Theme } from "types";

import { middlewares, RouterView, routes } from "../router";
import { EnvironmentProvider, ThemeProvider, useEnvironmentContext, useThemeContext } from "./contexts";
import { useDeeplink, useEnvSynchronizer, useNetworkStatus } from "./hooks";
import { useNetworkStatus } from "./hooks";
import { useEnvSynchronizer } from "./hooks/use-synchronizer";
import { i18n } from "./i18n";
import { httpClient } from "./services";

const __DEV__ = process.env.NODE_ENV !== "production";

const Main = () => {
const [showSplash, setShowSplash] = useState(true);

const { pathname } = useLocation();
const { theme, setTheme } = useThemeContext();
const { env, persist } = useEnvironmentContext();
const isOnline = useNetworkStatus();
const { start, runAll } = useEnvSynchronizer();
useDeeplink();

const location = useLocation();
const pathname = (location as any).location?.pathname || location.pathname;
Expand Down
2 changes: 2 additions & 0 deletions src/app/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export * from "./env";
export * from "./network-status";
export * from "./use-clipboard";
export * from "./use-query-params";
export * from "./use-deeplink";
export * from "./use-synchronizer";
export * from "./use-reload-path";
148 changes: 148 additions & 0 deletions src/app/hooks/use-deeplink.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { translations } from "app/i18n/common/i18n";
import { toasts } from "app/services";
import { ipcRenderer } from "electron";
import { createMemoryHistory } from "history";
import React from "react";
import { Route } from "react-router-dom";
import { getDefaultProfileId, getDefaultWalletId, renderWithRouter } from "testing-library";

import { useDeeplink } from "./use-deeplink";

const history = createMemoryHistory();
const walletURL = `/profiles/${getDefaultProfileId()}/wallets/${getDefaultWalletId()}`;

jest.mock("electron", () => ({
ipcRenderer: { on: jest.fn(), send: jest.fn(), removeListener: jest.fn() },
}));

describe("useDeeplink hook", () => {
const toastSpy = jest.spyOn(toasts, "warning").mockImplementationOnce((subject) => jest.fn(subject));

const TestComponent: React.FC = () => {
useDeeplink();

return <h1>Deeplink tester</h1>;
};

it("should subscribe to deeplink listener", () => {
ipcRenderer.on.mockImplementationOnce((event, callback) =>
callback(
event,
"ark:transfer?coin=ark&network=mainnet&recipient=DNjuJEDQkhrJ7cA9FZ2iVXt5anYiM8Jtc9&amount=1.2&memo=ARK",
),
);

const { getByText, history } = renderWithRouter(
<Route pathname="/">
<TestComponent />
</Route>,
);

expect(getByText("Deeplink tester")).toBeTruthy();
expect(ipcRenderer.on).toBeCalledWith("process-url", expect.any(Function));
});

it("should subscribe to deeplink listener and toast a warning to select a profile", () => {
ipcRenderer.on.mockImplementationOnce((event, callback) =>
callback(
event,
"ark:transfer?coin=ark&network=mainnet&recipient=DNjuJEDQkhrJ7cA9FZ2iVXt5anYiM8Jtc9&amount=1.2&memo=ARK",
),
);

const { getByText, history } = renderWithRouter(
<Route pathname="/">
<TestComponent />
</Route>,
{
routes: ["/"],
},
);

expect(getByText("Deeplink tester")).toBeTruthy();
expect(toastSpy).toHaveBeenCalledWith(translations.SELECT_A_PROFILE);
expect(ipcRenderer.on).toBeCalledWith("process-url", expect.any(Function));
});

it("should subscribe to deeplink listener and toast a warning to select a wallet", () => {
ipcRenderer.on.mockImplementationOnce((event, callback) =>
callback(
event,
"ark:transfer?coin=ark&network=mainnet&recipient=DNjuJEDQkhrJ7cA9FZ2iVXt5anYiM8Jtc9&amount=1.2&memo=ARK",
),
);

window.history.pushState({}, "Deeplink Test", `/profiles/${getDefaultProfileId()}/dashboard`);

const { getByText, history } = renderWithRouter(
<Route pathname="/profiles/:profileId">
<TestComponent />
</Route>,
{
routes: [`/profiles/${getDefaultProfileId()}/dashboard`],
},
);

expect(getByText("Deeplink tester")).toBeTruthy();
expect(toastSpy).toHaveBeenCalledWith(translations.SELECT_A_WALLET);
expect(ipcRenderer.on).toBeCalledWith("process-url", expect.any(Function));
});

it("should subscribe to deeplink listener and navigate", () => {
ipcRenderer.on.mockImplementationOnce((event, callback) =>
callback(
event,
"ark:transfer?coin=ark&network=mainnet&recipient=DNjuJEDQkhrJ7cA9FZ2iVXt5anYiM8Jtc9&amount=1.2&memo=ARK",
),
);

window.history.pushState(
{},
"Deeplink Test",
`/profiles/${getDefaultProfileId()}/wallets/${getDefaultWalletId()}`,
);

const { getByText, history } = renderWithRouter(
<Route pathname="/profiles/:profileId/wallets/:walletId">
<TestComponent />
</Route>,
{
routes: [walletURL],
},
);

expect(getByText("Deeplink tester")).toBeTruthy();
expect(history.location.pathname).toEqual(
`/profiles/${getDefaultProfileId()}/wallets/${getDefaultWalletId()}/send-transfer`,
);
expect(ipcRenderer.on).toBeCalledWith("process-url", expect.any(Function));
});

it("should subscribe to deeplink listener and navigate when no method found", () => {
ipcRenderer.on.mockImplementationOnce((event, callback) =>
callback(
event,
"ark:vote?coin=ark&network=mainnet&recipient=DNjuJEDQkhrJ7cA9FZ2iVXt5anYiM8Jtc9&amount=1.2&memo=ARK",
),
);

window.history.pushState(
{},
"Deeplink Test",
`/profiles/${getDefaultProfileId()}/wallets/${getDefaultWalletId()}`,
);

const { getByText, history } = renderWithRouter(
<Route pathname="/profiles/:profileId/wallets/:walletId">
<TestComponent />
</Route>,
{
routes: [walletURL],
},
);

expect(getByText("Deeplink tester")).toBeTruthy();
expect(history.location.pathname).toEqual("/");
expect(ipcRenderer.on).toBeCalledWith("process-url", expect.any(Function));
});
});
63 changes: 63 additions & 0 deletions src/app/hooks/use-deeplink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { URI } from "@arkecosystem/platform-sdk-support/dist/uri";
import { toasts } from "app/services";
import { ipcRenderer } from "electron";
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";

const useDeepLinkHandler = () => {
const history = useHistory();
const { t } = useTranslation();
const uriService = new URI();

const navigate = useCallback((url: string, deeplinkSchema?: any) => history.push(url, deeplinkSchema), [history]);

const handler = useCallback(
(event: any, deeplink: string) => {
if (deeplink) {
if (window.location.pathname === "/") return toasts.warning(t("COMMON.SELECT_A_PROFILE"));

if (window.location.pathname.includes("/dashboard")) return toasts.warning(t("COMMON.SELECT_A_WALLET"));

const deeplinkSchema = uriService.deserialize(deeplink);
const urlParts = window.location.pathname.split("/");
const activeSession = {
profileId: urlParts[2],
walletId: urlParts[4],
};

switch (deeplinkSchema.method) {
case "transfer":
return navigate(
`/profiles/${activeSession.profileId}/wallets/${activeSession.walletId}/send-transfer`,
deeplinkSchema,
);

default:
return navigate("/");
}
}
},
[t, uriService, navigate],
);

useEffect((): any => {
ipcRenderer.on("process-url", handler);

return () => ipcRenderer.removeListener("process-url", handler);
}, [handler]);

return {
handler,
};
};

export const useDeeplink = () => {
const { handler } = useDeepLinkHandler();

useEffect(() => {
handler(null, "");
}, [handler]);
};

export default useDeeplink;
2 changes: 2 additions & 0 deletions src/app/i18n/common/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ export const translations: { [key: string]: any } = {
SELECTED: "Selected",
SELECT_ALL: "Select All",
SELECT_OPTION: "Select {{option}}",
SELECT_A_PROFILE: "You should select a profile to access this URL",
SELECT_A_WALLET: "You should select a wallet to deeplink transactions",
SEND: "Send",
SETTINGS: "Settings",
SHOW: "Show",
Expand Down
11 changes: 10 additions & 1 deletion src/domains/transaction/components/AddRecipient/AddRecipient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const AddRecipient = ({
const [addedRecipients, setAddressRecipients] = useState<RecipientListItem[]>(recipients!);
const [isSingle, setIsSingle] = useState(isSingleRecipient);
const [displayAmount, setDisplayAmount] = useState<string | undefined>();
const [recipientsAmount, setRecipientsAmount] = useState<any>();

const { t } = useTranslation();

Expand All @@ -84,6 +85,14 @@ export const AddRecipient = ({
register("amount");
}, [register]);

useEffect(() => {
setRecipientsAmount(
recipients
?.reduce((accumulator, currentValue) => Number(accumulator) + Number(currentValue.amount), 0)
.toString(),
);
}, [recipients, displayAmount]);

const availableAmount = useMemo(
() => addedRecipients.reduce((sum, item) => sum.minus(item.amount), maxAvailableAmount),
[maxAvailableAmount, addedRecipients],
Expand Down Expand Up @@ -174,7 +183,7 @@ export const AddRecipient = ({
name="amount"
placeholder={t("COMMON.AMOUNT")}
className="pr-20"
value={displayAmount}
value={displayAmount || recipientsAmount}
onChange={(currency) => {
setDisplayAmount(currency.display);
setValue("amount", currency.value, { shouldValidate: true, shouldDirty: true });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ exports[`AddRecipient should render 1`] = `
name="amount"
placeholder="Amount"
type="text"
value=""
value="0"
/>
<div
class="sc-AxhUy sc-AxheI bYHtKZ"
Expand Down Expand Up @@ -301,7 +301,7 @@ exports[`AddRecipient should render with multiple recipients tab 1`] = `
name="amount"
placeholder="Amount"
type="text"
value=""
value="0"
/>
<div
class="sc-AxhUy sc-AxheI bYHtKZ"
Expand Down Expand Up @@ -473,7 +473,7 @@ exports[`AddRecipient should render with single recipient data 1`] = `
name="amount"
placeholder="Amount"
type="text"
value=""
value="10000000000"
/>
<div
class="sc-AxhUy sc-AxheI bYHtKZ"
Expand Down Expand Up @@ -634,7 +634,7 @@ exports[`AddRecipient should render without recipients 1`] = `
name="amount"
placeholder="Amount"
type="text"
value=""
value="0"
/>
<div
class="sc-AxhUy sc-AxheI bYHtKZ"
Expand Down
Loading

0 comments on commit d092c43

Please sign in to comment.