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

feat: AIP26 handling #2929

Merged
merged 40 commits into from
Oct 14, 2020
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d6a687a
feat: wip deeplink handler and redirector
clucasalcantara Oct 1, 2020
4687bd2
feat: wip handle transfer deeplink
clucasalcantara Oct 1, 2020
34a177e
feat: wip fill form from deeplink state
clucasalcantara Oct 2, 2020
2160ac6
chore: remove unnecessary effect hook dep
clucasalcantara Oct 2, 2020
069c338
refactor: move process url listener to router view
clucasalcantara Oct 5, 2020
4a29f57
chore: improve redirect
clucasalcantara Oct 5, 2020
a5affc5
Merge branch 'develop' into feat/aip-26-handling
clucasalcantara Oct 5, 2020
ac4ade7
refactor: switch to window to avoid rerendering side effect
clucasalcantara Oct 5, 2020
1278343
Merge branch 'feat/aip-26-handling' of github.com:ArkEcosystem/deskto…
clucasalcantara Oct 5, 2020
cb64989
chore: remove ununsed variables
clucasalcantara Oct 5, 2020
9cdd00a
tests: add deeplink test case
clucasalcantara Oct 5, 2020
f9d7839
chore: improve effect and fillup smartbridge
clucasalcantara Oct 5, 2020
195a774
refactor: create deeplinking custom hook
clucasalcantara Oct 5, 2020
af2445e
feat: fill up amount
clucasalcantara Oct 5, 2020
6087073
tests: update snapshots
clucasalcantara Oct 5, 2020
2d12890
chore: remove ununsed variables
clucasalcantara Oct 5, 2020
e5940ee
Merge branch 'develop' into feat/aip-26-handling
clucasalcantara Oct 6, 2020
7f6ef60
tests: add ipcRenderer mock
clucasalcantara Oct 6, 2020
35d03d8
Merge branch 'develop' into feat/aip-26-handling
clucasalcantara Oct 6, 2020
8676968
tests: add deeplink hook initial tests
clucasalcantara Oct 6, 2020
29496c8
tests: update snapshots
clucasalcantara Oct 6, 2020
a40f733
tests: wip ipc renderer mock
clucasalcantara Oct 6, 2020
a0afa83
tests: update snapshots
clucasalcantara Oct 6, 2020
256d96f
refactor: remove ununsed service
clucasalcantara Oct 6, 2020
55cee52
style: resolve style guide violations
clucasalcantara Oct 6, 2020
1397040
tests: increase deeplink test coverage
clucasalcantara Oct 6, 2020
a7340fc
Merge branch 'feat/aip-26-handling' of github.com:ArkEcosystem/deskto…
clucasalcantara Oct 6, 2020
37ae4bb
chore: wip remove virtual mock prop
clucasalcantara Oct 6, 2020
3a912e2
tests: fix undefined ipcRenderer on
clucasalcantara Oct 7, 2020
dbd2f97
Merge branch 'develop' into feat/aip-26-handling
clucasalcantara Oct 7, 2020
fe9ad29
tests: increase coverage
clucasalcantara Oct 7, 2020
3628c90
chore: remove unnecessary url check
clucasalcantara Oct 7, 2020
3f366fa
tests: remove fit flag
clucasalcantara Oct 7, 2020
a7c9c85
tests: update snapshots
clucasalcantara Oct 7, 2020
49454c4
tests: increase transfer coverage
clucasalcantara Oct 7, 2020
e6c4928
chore: remove ununsed variable
clucasalcantara Oct 7, 2020
5c430a0
tests: fix case error
clucasalcantara Oct 7, 2020
7eadb9b
Merge branch 'develop' into feat/aip-26-handling
clucasalcantara Oct 14, 2020
3faef59
style: resolve style guide violations
clucasalcantara Oct 14, 2020
85eca2d
Merge branch 'develop' into feat/aip-26-handling
clucasalcantara Oct 14, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/app/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
/* eslint-disable @typescript-eslint/require-await */
import { translations as errorTranslations } from "domains/error/i18n";
import { translations as profileTranslations } from "domains/profile/i18n";
import { ipcRenderer } from "electron";
import React from "react";
import { act, renderWithRouter, useDefaultNetMocks, waitFor } from "utils/testing-library";

import { App } from "./App";

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

describe("App", () => {
beforeAll(useDefaultNetMocks);

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: 2 additions & 3 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,19 @@ import { StubStorage } from "tests/mocks";

import { middlewares, RouterView, routes } from "../router";
import { EnvironmentProvider, useEnvironmentContext } from "./contexts";
import { useNetworkStatus } from "./hooks";
import { useEnvSynchronizer } from "./hooks/use-synchronizer";
import { useDeeplink, useEnvSynchronizer, useNetworkStatus } from "./hooks";
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 { env, persist } = useEnvironmentContext();
const isOnline = useNetworkStatus();
const { start, runAll } = useEnvSynchronizer();
useDeeplink();

useEffect(() => {
window.scrollTo(0, 0);
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,3 +4,5 @@ export * from "./env";
export * from "./network-status";
export * from "./use-clipboard";
export * from "./use-query-params";
export * from "./use-deeplink";
export * from "./use-synchronizer";
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 @@ -133,6 +133,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
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