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

Commit

Permalink
feat: add auto updater functionality (#3050)
Browse files Browse the repository at this point in the history
  • Loading branch information
goga-m authored Nov 4, 2020
1 parent 0ebb501 commit bc69d83
Show file tree
Hide file tree
Showing 35 changed files with 2,665 additions and 422 deletions.
4 changes: 4 additions & 0 deletions app-update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
owner: ArkEcosystem
repo: desktop-wallet
provider: github
vPrefixedTagName: false
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@
"downshift": "^6.0.2",
"electron-is-dev": "^1.2.0",
"electron-window-state": "^5.0.3",
"electron-updater": "^4.3.5",
"electron-log": "^4.2.4",
"extract-domain": "^2.2.1",
"framer-motion": "^2.1.2",
"hash-wasm": "^3.7.1",
Expand All @@ -174,7 +176,9 @@
"swiper": "^6.0.4",
"twin.macro": "^1.3.0",
"type-fest": "^0.16.0",
"yup": "^0.29.3"
"yup": "^0.29.3",
"pretty-bytes": "^5.4.1",
"async-retry": "^1.3.1"
},
"devDependencies": {
"@babel/preset-env": "^7.10.4",
Expand Down Expand Up @@ -221,7 +225,6 @@
"@types/yup": "^0.29.6",
"@typescript-eslint/eslint-plugin": "^3.1.0",
"@typescript-eslint/parser": "^3.1.0",
"async-retry": "^1.3.1",
"autoprefixer": "^9.8.0",
"babel-loader": "^8.1.0",
"bcrypto": "^5.3.0",
Expand Down
52 changes: 36 additions & 16 deletions src/app/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

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,18 +17,43 @@ import {

import { App } from "./App";

jest.mock("electron", () => ({
ipcRenderer: { on: jest.fn(), send: jest.fn(), removeListener: jest.fn() },
remote: {
nativeTheme: {
shouldUseDarkColors: true,
themeSource: "system",
jest.mock(`electron`, () => {
let isUpdateCalled = false;

return {
ipcRenderer: {
invoke: (event: string, data) => {
if (event === "updater:check-for-updates") {
const response = {
cancellationToken: isUpdateCalled ? null : "1",
updateInfo: { version: "3.0.0" },
};
isUpdateCalled = true;
return response;
}
return true;
},
on: (evt: any, callback: (evt: any, progress: any) => void) => {
if (evt === "updater:download-progress") {
callback(evt, { total: 10, percent: 30, transferred: 3 });
}
},
handle: jest.fn(),
send: jest.fn(),
removeListener: jest.fn(),
},
getCurrentWindow: () => ({
setContentProtection: jest.fn(),
}),
},
}));

remote: {
nativeTheme: {
shouldUseDarkColors: true,
themeSource: "system",
},
getCurrentWindow: () => ({
setContentProtection: jest.fn(),
}),
},
};
});

const dashboardUrl = `/profiles/${getDefaultProfileId()}/dashboard`;

Expand All @@ -44,10 +68,6 @@ 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
7 changes: 7 additions & 0 deletions src/app/components/Link/Link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import { act, fireEvent, renderWithRouter } from "testing-library";
import { Link } from "./Link";

jest.mock("electron", () => ({
ipcRenderer: {
invoke: jest.fn(),
on: jest.fn(),
handle: jest.fn(),
send: jest.fn(),
removeListener: jest.fn(),
},
shell: {
openExternal: jest.fn(),
},
Expand Down
7 changes: 7 additions & 0 deletions src/app/components/NavigationBar/NavigationBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ jest.mock("electron", () => ({
shell: {
openExternal: jest.fn(),
},
ipcRenderer: {
invoke: jest.fn(),
on: jest.fn(),
handle: jest.fn(),
send: jest.fn(),
removeListener: jest.fn(),
},
}));

let profile: Profile;
Expand Down
20 changes: 10 additions & 10 deletions src/app/components/Notifications/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,35 @@ export const Notifications = ({ profile, onNotificationAction, onTransactionClic
const env = useEnvironmentContext();

const byType = useCallback(
(type: string) =>
(types: string[]) =>
profile
.notifications()
.values()
.filter((n) => n.type === type),
.filter((n) => types.includes(n.type)),
[profile],
);

const wrapperRef = useRef();
const plugins = byType("plugin");
const transactions = byType("transaction");
const notifications = byType(["plugin", "wallet"]);
const transactions = byType(["transaction"]);

if (!transactions.length && !plugins.length) {
if (!transactions.length && !notifications.length) {
return <NotificationsSkeleton title={t("COMMON.NOTIFICATIONS.EMPTY")} />;
}

return (
<NotificationsWrapper ref={wrapperRef as React.MutableRefObject<any>} data-testid="NotificationsWrapper">
{plugins.length > 0 && (
{notifications.length > 0 && (
<>
<div className="z-10 py-4 pl-4 pr-8 mb-2 -mx-4 text-sm font-bold text-theme-neutral -top-5">
{t("COMMON.NOTIFICATIONS.PLUGINS_TITLE")}
</div>
<Table hideHeader columns={[{ Header: "-", className: "hidden" }]} data={plugins}>
{(plugin: NotificationItemProps) => (
<Table hideHeader columns={[{ Header: "-", className: "hidden" }]} data={notifications}>
{(notification: NotificationItemProps) => (
<NotificationItem
{...plugin}
{...notification}
onAction={onNotificationAction}
onVisibilityChange={(isVisible) => markAsRead(isVisible, plugin.id, profile, env)}
onVisibilityChange={(isVisible) => markAsRead(isVisible, notification.id, profile, env)}
containmentRef={wrapperRef}
/>
)}
Expand Down
78 changes: 78 additions & 0 deletions src/app/components/Notifications/NotificationsDropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ import { NotificationsDropdown } from "./";
const history = createMemoryHistory();
let profile: Profile;

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

describe("Notifications", () => {
beforeEach(() => {
const dashboardURL = `/profiles/${getDefaultProfileId()}/dashboard`;
Expand Down Expand Up @@ -73,4 +83,72 @@ describe("Notifications", () => {
await waitFor(() => expect(() => getByTestId("modal__inner")).toThrow(/^Unable to find an element by/));
expect(container).toMatchSnapshot();
});

it("should open and close wallet update notification modal", async () => {
const { container, getByTestId, queryAllByTestId, getAllByTestId } = renderWithRouter(
<Route path="/profiles/:profileId/dashboard">
<NotificationsDropdown profile={profile} />
</Route>,
{
routes: [`/profiles/${getDefaultProfileId()}/dashboard`],
history,
},
);

await waitFor(() => expect(getAllByTestId("dropdown__toggle")).toBeTruthy());
act(() => {
fireEvent.click(getByTestId("dropdown__toggle"));
});

await waitFor(() => expect(getAllByTestId("NotificationItem")).toHaveLength(2));
await waitFor(() => expect(queryAllByTestId("TransactionRowMode").length).toBeGreaterThan(0));

act(() => {
fireEvent.click(getAllByTestId("NotificationItem__action")[0]);
});

await waitFor(() => expect(getByTestId("WalletUpdate__first-step")).toBeTruthy());
expect(container).toMatchSnapshot();

act(() => {
fireEvent.click(getByTestId("modal__close-btn"));
});

await waitFor(() => expect(() => getByTestId("modal__inner")).toThrow(/^Unable to find an element by/));
expect(container).toMatchSnapshot();
});

it("should open and cancel wallet update notification modal", async () => {
const { container, getByTestId, queryAllByTestId, getAllByTestId } = renderWithRouter(
<Route path="/profiles/:profileId/dashboard">
<NotificationsDropdown profile={profile} />
</Route>,
{
routes: [`/profiles/${getDefaultProfileId()}/dashboard`],
history,
},
);

await waitFor(() => expect(getAllByTestId("dropdown__toggle")).toBeTruthy());
act(() => {
fireEvent.click(getByTestId("dropdown__toggle"));
});

await waitFor(() => expect(getAllByTestId("NotificationItem")).toHaveLength(2));
await waitFor(() => expect(queryAllByTestId("TransactionRowMode").length).toBeGreaterThan(0));

act(() => {
fireEvent.click(getAllByTestId("NotificationItem__action")[0]);
});

await waitFor(() => expect(getByTestId("WalletUpdate__first-step")).toBeTruthy());
expect(container).toMatchSnapshot();

act(() => {
fireEvent.click(getByTestId("WalletUpdate__cancel-button"));
});

await waitFor(() => expect(() => getByTestId("modal__inner")).toThrow(/^Unable to find an element by/));
expect(container).toMatchSnapshot();
});
});
28 changes: 27 additions & 1 deletion src/app/components/Notifications/NotificationsDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,28 @@ import { Dropdown } from "app/components/Dropdown";
import { Icon } from "app/components/Icon";
import { Notifications } from "app/components/Notifications";
import { TransactionDetailModal } from "domains/transaction/components/TransactionDetailModal";
import { WalletUpdate } from "domains/wallet/components/WalletUpdate";
import React, { useState } from "react";

export const NotificationsDropdown = ({ profile }: { profile: Profile }) => {
const [transactionModalItem, setTransactionModalItem] = useState<ExtendedTransactionData>();
const [isWalletUpdateOpen, setIsWalletUpdateOpen] = useState<boolean>();
const [walletUpdateVersion, setIsWalletUpdateVersion] = useState<string>();

const hasUnread = profile.notifications().unread().length > 0;

const handleNotificationAction = (id: string) => {
const notification = profile.notifications().get(id);
const action = `${notification.type}.${notification.action}`;

switch (action) {
case "wallet.update":
setIsWalletUpdateVersion(notification?.meta?.version);
setIsWalletUpdateOpen(true);
break;
}
};

return (
<div>
<Dropdown
Expand All @@ -33,7 +48,11 @@ export const NotificationsDropdown = ({ profile }: { profile: Profile }) => {
}
>
<div className="mt-2">
<Notifications profile={profile} onTransactionClick={setTransactionModalItem} />
<Notifications
profile={profile}
onTransactionClick={setTransactionModalItem}
onNotificationAction={handleNotificationAction}
/>
</div>
</Dropdown>

Expand All @@ -44,6 +63,13 @@ export const NotificationsDropdown = ({ profile }: { profile: Profile }) => {
onClose={() => setTransactionModalItem(undefined)}
/>
)}

<WalletUpdate
version={walletUpdateVersion}
isOpen={isWalletUpdateOpen}
onClose={() => setIsWalletUpdateOpen(false)}
onCancel={() => setIsWalletUpdateOpen(false)}
/>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ exports[`Notifications should render notification item 1`] = `
<span
class="font-bold text-md text-theme-neutral-600"
>
ARK Explorer
ARK Desktop Wallet
</span>
<span
class="text-md text-theme-neutral-600"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ exports[`Notifications should emit transactionClick event 1`] = `
<span
class="font-bold text-md text-theme-neutral-600"
>
ARK Explorer
ARK Desktop Wallet
</span>
<span
class="text-md text-theme-neutral-600"
Expand Down Expand Up @@ -338,7 +338,7 @@ exports[`Notifications should render with plugins 1`] = `
<span
class="font-bold text-md text-theme-neutral-600"
>
ARK Explorer
ARK Desktop Wallet
</span>
<span
class="text-md text-theme-neutral-600"
Expand Down Expand Up @@ -600,7 +600,7 @@ exports[`Notifications should render with transactions and plugins 1`] = `
<span
class="font-bold text-md text-theme-neutral-600"
>
ARK Explorer
ARK Desktop Wallet
</span>
<span
class="text-md text-theme-neutral-600"
Expand Down
Loading

0 comments on commit bc69d83

Please sign in to comment.