feat: node settings wired to haveno daemon

- wired node settings to daemon and storage
- updated tests and storybook
- refactored for simplicity and code quality

---

Reviewed-by: localredhead
This commit is contained in:
Subir 2022-05-24 03:35:16 +05:30
parent 8eb4694ca2
commit 250d742d48
No known key found for this signature in database
GPG Key ID: 2D633D8047FD3FF0
73 changed files with 1436 additions and 680 deletions

View File

@ -117,15 +117,21 @@ export function registerStoreHandlers() {
} }
); );
ipcMain.handle(IpcChannels.SetMoneroNode, async (_, value: string) => { // set or clear remote node url; empty indicates local node
store.set(StorageKeys.Preferences_MoneroNode, value); ipcMain.handle(IpcChannels.SetMoneroNode, async (_, uri?: string) => {
if (!uri) {
store.delete(StorageKeys.Preferences_SelectedNode);
} else {
store.set(StorageKeys.Preferences_SelectedNode, uri);
}
}); });
// fetch the complete set of user preferences
ipcMain.handle( ipcMain.handle(
IpcChannels.GetPreferences, IpcChannels.GetPreferences,
async (): Promise<IPreferences> => { async (): Promise<IPreferences> => {
return { return {
moneroNode: store.get(StorageKeys.Preferences_MoneroNode), selectedNode: store.get(StorageKeys.Preferences_SelectedNode),
}; };
} }
); );

View File

@ -21,7 +21,7 @@ export enum IpcChannels {
VerifyPassword = "store:accountinfo.verifyPassword", VerifyPassword = "store:accountinfo.verifyPassword",
SetPrimaryFiat = "store:accountinfo.primaryFiat", SetPrimaryFiat = "store:accountinfo.primaryFiat",
GetPreferences = "store:preferences", GetPreferences = "store:preferences",
SetMoneroNode = "store:preferences.moneroNode", SetMoneroNode = "store:preferences.setMoneroNode",
VerifyAuthToken = "verifyAuthToken", VerifyAuthToken = "verifyAuthToken",
} }

View File

@ -19,14 +19,14 @@ import type { Schema } from "electron-store";
export enum StorageKeys { export enum StorageKeys {
AccountInfo_Password = "accounInfo.password", AccountInfo_Password = "accounInfo.password",
AccountInfo_PrimaryFiat = "accounInfo.primaryFiat", AccountInfo_PrimaryFiat = "accounInfo.primaryFiat",
Preferences_MoneroNode = "preferences.moneroNode", Preferences_SelectedNode = "preferences.selectedNode",
} }
// TS types for StoreSchema // TS types for StoreSchema
export interface IStoreSchema { export interface IStoreSchema {
[StorageKeys.AccountInfo_Password]: IAccountInfo["password"]; [StorageKeys.AccountInfo_Password]: IAccountInfo["password"];
[StorageKeys.AccountInfo_PrimaryFiat]: IAccountInfo["primaryFiat"]; [StorageKeys.AccountInfo_PrimaryFiat]: IAccountInfo["primaryFiat"];
[StorageKeys.Preferences_MoneroNode]: IPreferences["moneroNode"]; // TODO: change to object {url, password} [StorageKeys.Preferences_SelectedNode]: IPreferences["selectedNode"];
} }
export interface IAccountInfo { export interface IAccountInfo {
@ -39,7 +39,7 @@ export interface AccountInfoDto extends Omit<IAccountInfo, "password"> {
} }
export interface IPreferences { export interface IPreferences {
moneroNode: string; selectedNode?: string; // empty for local; id for remote
} }
// this schema is used by electron-store // this schema is used by electron-store
@ -51,7 +51,7 @@ export const StoreSchema: Schema<IStoreSchema> = {
[StorageKeys.AccountInfo_PrimaryFiat]: { [StorageKeys.AccountInfo_PrimaryFiat]: {
type: "string", type: "string",
}, },
[StorageKeys.Preferences_MoneroNode]: { [StorageKeys.Preferences_SelectedNode]: {
type: "string", type: "string",
}, },
}; };

View File

@ -46,8 +46,9 @@ export const store = {
getAccountInfo: async (): Promise<AccountInfoDto> => getAccountInfo: async (): Promise<AccountInfoDto> =>
ipcRenderer.invoke(IpcChannels.GetAccountInfo), ipcRenderer.invoke(IpcChannels.GetAccountInfo),
setMoneroNode: async (value: string): Promise<void> => // sets the selected monero node url; empty indicates local node
ipcRenderer.invoke(IpcChannels.SetMoneroNode, value), setMoneroNode: async (uri?: string): Promise<void> =>
ipcRenderer.invoke(IpcChannels.SetMoneroNode, uri),
getPreferences: async (): Promise<IPreferences> => getPreferences: async (): Promise<IPreferences> =>
ipcRenderer.invoke(IpcChannels.GetPreferences), ipcRenderer.invoke(IpcChannels.GetPreferences),

View File

@ -21,11 +21,11 @@ import { Home } from "@pages/Home";
import { Login } from "@pages/Login"; import { Login } from "@pages/Login";
import { CreateAccount, Welcome } from "@pages/Onboarding"; import { CreateAccount, Welcome } from "@pages/Onboarding";
import { import {
AccountBackup, Backup,
AccountNodeSettings, Settings,
AccountPaymentAccounts, PaymentAccounts,
AccountSecurity, Security,
AccountWallet, Wallet,
AddPaymentAccount, AddPaymentAccount,
PaymentMethods, PaymentMethods,
} from "@pages/Account"; } from "@pages/Account";
@ -38,47 +38,47 @@ export function AppRoutes() {
<Route path={ROUTES.Welcome} element={<Welcome />} /> <Route path={ROUTES.Welcome} element={<Welcome />} />
<Route path={ROUTES.CreateAccount} element={<CreateAccount />} /> <Route path={ROUTES.CreateAccount} element={<CreateAccount />} />
<Route <Route
path={ROUTES.AccountPaymentAccounts} path={ROUTES.PaymentAccounts}
element={ element={
<ProtectedRoute> <ProtectedRoute>
<AccountPaymentAccounts /> <PaymentAccounts />
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route <Route
path={ROUTES.AccountNodeSettings} path={ROUTES.NodeSettings}
element={ element={
<ProtectedRoute> <ProtectedRoute>
<AccountNodeSettings /> <Settings />
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route <Route
path={ROUTES.AccountBackup} path={ROUTES.Backup}
element={ element={
<ProtectedRoute> <ProtectedRoute>
<AccountBackup /> <Backup />
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route <Route
path={ROUTES.AccountWallet} path={ROUTES.Wallet}
element={ element={
<ProtectedRoute> <ProtectedRoute>
<AccountWallet /> <Wallet />
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route <Route
path={ROUTES.AccountSecurity} path={ROUTES.Security}
element={ element={
<ProtectedRoute> <ProtectedRoute>
<AccountSecurity /> <Security />
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route <Route
path={ROUTES.AccountPaymentAccounts} path={ROUTES.PaymentAccounts}
element={ element={
<ProtectedRoute> <ProtectedRoute>
<PaymentMethods /> <PaymentMethods />
@ -86,7 +86,7 @@ export function AppRoutes() {
} }
/> />
<Route <Route
path={ROUTES.AccountAddPaymentAccount} path={ROUTES.AddPaymentAccount}
element={ element={
<ProtectedRoute> <ProtectedRoute>
<AddPaymentAccount /> <AddPaymentAccount />

View File

@ -14,13 +14,13 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import type { ReactText } from "react"; import type { ReactNode } from "react";
import type { UnstyledButtonProps } from "@mantine/core"; import type { UnstyledButtonProps } from "@mantine/core";
import { UnstyledButton } from "@mantine/core"; import { UnstyledButton } from "@mantine/core";
import { BodyText } from "@atoms/Typography"; import { BodyText } from "@atoms/Typography";
interface TextButtonProps extends UnstyledButtonProps<"button"> { interface TextButtonProps extends UnstyledButtonProps<"button"> {
children: ReactText; children: ReactNode;
} }
export function TextButton(props: TextButtonProps) { export function TextButton(props: TextButtonProps) {

View File

@ -16,24 +16,33 @@
import { Stack } from "@mantine/core"; import { Stack } from "@mantine/core";
import type { ComponentStory, ComponentMeta } from "@storybook/react"; import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { NodeStatus, NodeStatusType } from "."; import { MoneroNodeListItem, NodeStatus } from ".";
export default { export default {
title: "atoms/NodeStatus", title: "atoms/MoneroNodeListItem",
component: NodeStatus, component: MoneroNodeListItem,
} as ComponentMeta<typeof NodeStatus>; } as ComponentMeta<typeof MoneroNodeListItem>;
const Template: ComponentStory<typeof NodeStatus> = () => { const Template: ComponentStory<typeof MoneroNodeListItem> = () => {
return ( return (
<Stack> <Stack>
<NodeStatus <MoneroNodeListItem
isSelected={true}
title="node.moneroworldcom:18089" title="node.moneroworldcom:18089"
status={NodeStatusType.Active} status={NodeStatus.Active}
onClick={() => console.log("clicked")}
/> />
<NodeStatus title="node.xmr.pt:18081" status={NodeStatusType.Inactive} /> <MoneroNodeListItem
<NodeStatus isSelected={false}
title="node.xmr.pt:18081"
status={NodeStatus.Inactive}
onClick={() => console.log("clicked")}
/>
<MoneroNodeListItem
isSelected={false}
title="node.monero.net:18081" title="node.monero.net:18081"
status={NodeStatusType.Active} status={NodeStatus.Active}
onClick={() => console.log("clicked")}
/> />
</Stack> </Stack>
); );

View File

@ -17,19 +17,23 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { render } from "@testing-library/react"; import { render } from "@testing-library/react";
import { AppProviders } from "@atoms/AppProviders"; import { AppProviders } from "@atoms/AppProviders";
import { NodeStatus, NodeStatusType } from "./NodeStatus"; import { MoneroNodeListItem, NodeStatus } from "./MoneroNodeListItem";
describe("atoms::NodeStatus", () => { describe("atoms::MoneroNodeListItem", () => {
it("renders without exploding", () => { it("renders without exploding", () => {
const { asFragment } = render( const { asFragment } = render(
<AppProviders> <AppProviders>
<NodeStatus <MoneroNodeListItem
isSelected={true}
title="node.moneroworldcom:18089:active" title="node.moneroworldcom:18089:active"
status={NodeStatusType.Active} status={NodeStatus.Active}
onClick={() => console.log("clicked")}
/> />
<NodeStatus <MoneroNodeListItem
isSelected={false}
title="node.moneroworldcom:18089:inactive" title="node.moneroworldcom:18089:inactive"
status={NodeStatusType.Inactive} status={NodeStatus.Inactive}
onClick={() => console.log("clicked")}
/> />
</AppProviders> </AppProviders>
); );

View File

@ -0,0 +1,82 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { createStyles, Box, Text, UnstyledButton } from "@mantine/core";
export enum NodeStatus {
Active = "active",
Inactive = "inactive",
}
export interface MoneroNodeListItemProps {
isSelected?: boolean;
onClick: () => void;
status: NodeStatus;
title: string;
}
export function MoneroNodeListItem(props: MoneroNodeListItemProps) {
const { isSelected = false, onClick, status, title } = props;
const { classes } = useStyles({ isSelected, status });
return (
<UnstyledButton className={classes.root} onClick={onClick}>
<Text className={classes.title}>{title}</Text>
<Box className={classes.status}>
<Box className={classes.statusInner} />
</Box>
</UnstyledButton>
);
}
export const useStyles = createStyles<
string,
{ isSelected: boolean; status: NodeStatus }
>((theme, { isSelected, status }) => {
return {
root: {
backgroundColor: isSelected
? theme.colors.blue[0]
: theme.colorScheme === "dark"
? theme.colors.dark[8]
: theme.white,
border: `1px solid ${theme.colors.gray[2]}`,
borderRadius: theme.radius.md,
padding: "0.875rem",
display: "flex",
transition: "background-color 0.1s ease-in-out",
},
title: {
fontWeight: 600,
fontSize: theme.fontSizes.sm,
lineHeight: 1,
width: "100%",
},
status: {
display: "flex",
},
statusInner: {
height: "0.625rem",
width: "0.625rem",
borderRadius: "0.625rem",
background:
status === NodeStatus.Active
? theme.colors.green[4]
: theme.colors.gray[4],
margin: "auto",
},
};
});

View File

@ -1,9 +1,10 @@
// Vitest Snapshot v1 // Vitest Snapshot v1
exports[`atoms::NodeStatus > renders without exploding 1`] = ` exports[`atoms::MoneroNodeListItem > renders without exploding 1`] = `
<DocumentFragment> <DocumentFragment>
<div <button
class="mantine-1vax8o0" class="mantine-UnstyledButton-root mantine-pk5m2t"
type="button"
> >
<div <div
class="mantine-Text-root mantine-14byb36" class="mantine-Text-root mantine-14byb36"
@ -17,9 +18,10 @@ exports[`atoms::NodeStatus > renders without exploding 1`] = `
class="mantine-zy87za" class="mantine-zy87za"
/> />
</div> </div>
</div> </button>
<div <button
class="mantine-1vax8o0" class="mantine-UnstyledButton-root mantine-1v48mh9"
type="button"
> >
<div <div
class="mantine-Text-root mantine-14byb36" class="mantine-Text-root mantine-14byb36"
@ -33,6 +35,6 @@ exports[`atoms::NodeStatus > renders without exploding 1`] = `
class="mantine-19v6ci5" class="mantine-19v6ci5"
/> />
</div> </div>
</div> </button>
</DocumentFragment> </DocumentFragment>
`; `;

View File

@ -0,0 +1,17 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
export * from "./MoneroNodeListItem";

View File

@ -1,78 +0,0 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { createStyles, Box, Text } from "@mantine/core";
export enum NodeStatusType {
Active = "active",
Inactive = "inactive",
}
export interface NodeStatusProps {
/** Node title */
title: string;
/** Node status */
status: NodeStatusType;
}
export function NodeStatus({ title, status }: NodeStatusProps) {
const { classes } = useStyles({ status });
return (
<Box className={classes.root}>
<Text className={classes.title}>{title}</Text>
<Box className={classes.status}>
<Box className={classes.statusInner} />
</Box>
</Box>
);
}
export const useStyles = createStyles<string, { status: NodeStatusType }>(
(theme, { status }) => {
return {
root: {
backgroundColor:
theme.colorScheme === "dark" ? theme.colors.dark[8] : theme.white,
border: `1px solid ${theme.colors.gray[2]}`,
borderRadius: theme.radius.md,
padding: "0.875rem",
display: "flex",
transition: "background-color 0.1s ease-in-out",
},
title: {
fontWeight: 600,
fontSize: theme.fontSizes.sm,
lineHeight: 1,
width: "100%",
},
status: {
display: "flex",
},
statusInner: {
height: "0.625rem",
width: "0.625rem",
borderRadius: "0.625rem",
background:
status === NodeStatusType.Active
? theme.colors.green[4]
: theme.colors.gray[4],
margin: "auto",
},
};
}
);

View File

@ -18,13 +18,13 @@ import { createStyles } from "@mantine/core";
export const useTabsStyles = createStyles<string, void>(() => { export const useTabsStyles = createStyles<string, void>(() => {
return { return {
body: {
marginTop: "2.5rem",
},
root: {}, root: {},
tabsListWrapper: { tabsListWrapper: {
display: "flex", display: "flex",
}, },
body: {
marginTop: "2.5rem",
},
}; };
}); });
@ -38,19 +38,21 @@ export const useControlStyles = createStyles<
tabControl: { tabControl: {
backgroundColor: backgroundColor:
theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.white, theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.white,
color:
theme.colorScheme === "dark"
? theme.colors.dark[0]
: theme.colors.gray[9],
border: `2px solid ${ border: `2px solid ${
theme.colorScheme === "dark" theme.colorScheme === "dark"
? theme.colors.dark[6] ? theme.colors.dark[6]
: theme.colors.gray[2] : theme.colors.gray[2]
}`, }`,
fontSize: theme.fontSizes.md,
padding: `${theme.spacing.lg}px ${theme.spacing.xl}px`,
borderRadius: theme.radius.lg, borderRadius: theme.radius.lg,
color:
theme.colorScheme === "dark"
? theme.colors.dark[0]
: theme.colors.gray[9],
cursor: "pointer",
fontSize: theme.fontSizes.md,
height: "8.45rem", height: "8.45rem",
padding: `${theme.spacing.lg}px ${theme.spacing.xl}px`,
position: "relative",
width: "13.85rem", width: "13.85rem",
"&:not(:first-of-type)": { "&:not(:first-of-type)": {
@ -59,13 +61,11 @@ export const useControlStyles = createStyles<
[`&.${tabActive.ref}`]: { [`&.${tabActive.ref}`]: {
color: theme.colorScheme === "dark" ? theme.black : theme.white, color: theme.colorScheme === "dark" ? theme.black : theme.white,
}, },
cursor: "pointer",
position: "relative",
}, },
tabIcon: { tabIcon: {
display: "flex",
fill: "currentColor", fill: "currentColor",
minHeight: "3.8rem", minHeight: "3.8rem",
display: "flex",
svg: { svg: {
margin: "auto", margin: "auto",
@ -85,18 +85,18 @@ export const useControlStyles = createStyles<
color: theme.white, color: theme.white,
}, },
tabCurrent: { tabCurrent: {
display: "inline-block",
position: "absolute",
fontSize: theme.fontSizes.xs,
lineHeight: 1,
padding: "0.38rem 1.15rem",
borderRadius: theme.radius.sm,
top: "0.6rem",
left: "0.7rem",
background: active background: active
? theme.fn.rgba(theme.white, 0.15) ? theme.fn.rgba(theme.white, 0.15)
: theme.fn.rgba(theme.colors.blue[5], 0.15), : theme.fn.rgba(theme.colors.blue[5], 0.15),
color: active ? theme.white : theme.black, color: active ? theme.white : theme.black,
borderRadius: theme.radius.sm,
display: "inline-block",
fontSize: theme.fontSizes.xs,
left: "0.7rem",
lineHeight: 1,
padding: "0.38rem 1.15rem",
position: "absolute",
top: "0.6rem",
}, },
}; };
}); });

View File

@ -42,11 +42,11 @@ interface NodeConnectSwitchProps {
} }
export function NodeConnectSwitch({ export function NodeConnectSwitch({
className,
onTabChange,
active, active,
children, children,
className,
initialTab, initialTab,
onTabChange,
}: NodeConnectSwitchProps) { }: NodeConnectSwitchProps) {
const { classes, cx } = useTabsStyles(); const { classes, cx } = useTabsStyles();

View File

@ -9,7 +9,7 @@ exports[`molecules::NodeConnectSwitch > renders without exploding 1`] = `
class="mantine-1d0mff5" class="mantine-1d0mff5"
> >
<button <button
class="mantine-1lhe3fe" class="mantine-m2616v"
role="tab" role="tab"
tabindex="-1" tabindex="-1"
type="button" type="button"
@ -18,12 +18,12 @@ exports[`molecules::NodeConnectSwitch > renders without exploding 1`] = `
class="mantine-199rwtt" class="mantine-199rwtt"
> >
<div <div
class="mantine-1jn9p7a" class="mantine-b2d3ff"
> >
Current Current
</div> </div>
<div <div
class="mantine-9bd5vi" class="mantine-prfd4k"
> >
<svg <svg
height="62px" height="62px"
@ -57,7 +57,7 @@ exports[`molecules::NodeConnectSwitch > renders without exploding 1`] = `
</div> </div>
</button> </button>
<button <button
class="mantine-1lhe3fe" class="mantine-m2616v"
role="tab" role="tab"
tabindex="-1" tabindex="-1"
type="button" type="button"
@ -66,7 +66,7 @@ exports[`molecules::NodeConnectSwitch > renders without exploding 1`] = `
class="mantine-199rwtt" class="mantine-199rwtt"
> >
<div <div
class="mantine-9bd5vi" class="mantine-prfd4k"
> >
<svg <svg
fill="none" fill="none"

View File

@ -31,7 +31,7 @@ import {
getPaymentAccountLogo, getPaymentAccountLogo,
getPaymentAccountName, getPaymentAccountName,
getPaymentAccountNumber, getPaymentAccountNumber,
} from "@utils/payment-account"; } from "@src/utils/paymentAccount";
interface PaymentMethodCardProps { interface PaymentMethodCardProps {
data: PaymentAccount; data: PaymentAccount;

View File

@ -34,35 +34,35 @@ export const useGetAccountSidebarMenu = () => {
id: LangKeys.AccountSidebarPaymentAccounts, id: LangKeys.AccountSidebarPaymentAccounts,
defaultMessage: "Payment Accounts", defaultMessage: "Payment Accounts",
}), }),
route: ROUTES.AccountPaymentAccounts, route: ROUTES.PaymentAccounts,
}, },
{ {
label: intl.formatMessage({ label: intl.formatMessage({
id: LangKeys.AccountSidebarNodeSettings, id: LangKeys.AccountSidebarNodeSettings,
defaultMessage: "Node Settings", defaultMessage: "Node Settings",
}), }),
route: ROUTES.AccountNodeSettings, route: ROUTES.NodeSettings,
}, },
{ {
label: intl.formatMessage({ label: intl.formatMessage({
id: LangKeys.AccountSidebarSecurity, id: LangKeys.AccountSidebarSecurity,
defaultMessage: "Security", defaultMessage: "Security",
}), }),
route: ROUTES.AccountSecurity, route: ROUTES.Security,
}, },
{ {
label: intl.formatMessage({ label: intl.formatMessage({
id: LangKeys.AccountSidebarWallet, id: LangKeys.AccountSidebarWallet,
defaultMessage: "Wallet", defaultMessage: "Wallet",
}), }),
route: ROUTES.AccountWallet, route: ROUTES.Wallet,
}, },
{ {
label: intl.formatMessage({ label: intl.formatMessage({
id: LangKeys.AccountSidebarBackup, id: LangKeys.AccountSidebarBackup,
defaultMessage: "Backup", defaultMessage: "Backup",
}), }),
route: ROUTES.AccountBackup, route: ROUTES.Backup,
}, },
], ],
[] []

View File

@ -0,0 +1,34 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { AddNode } from ".";
export default {
title: "organisms/Add Node",
component: AddNode,
} as ComponentMeta<typeof AddNode>;
const Template: ComponentStory<typeof AddNode> = (args) => {
return <AddNode {...args} />;
};
export const Default = Template.bind({});
Default.args = {
isLoading: false,
onSubmit: (values) => console.log(values),
showTitle: true,
};

View File

@ -0,0 +1,65 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { AddNode } from ".";
describe("organisms::AddNode", () => {
it("renders without exploding", () => {
const submitSpy = vi.fn();
const { asFragment, unmount } = render(<AddNode onSubmit={submitSpy} />);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("can render the form without the title", () => {
const submitSpy = vi.fn();
const { asFragment, unmount } = render(
<AddNode showTitle={false} onSubmit={submitSpy} />
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("blocks onSubmit on validation failure", async () => {
const submitSpy = vi.fn();
const { unmount } = render(
<AddNode showTitle={false} onSubmit={submitSpy} />
);
fireEvent.submit(screen.getByRole("button", { name: "Save" }));
expect(submitSpy).not.toHaveBeenCalled();
unmount();
});
it("calls onSubmit on successful validation", async () => {
const submitSpy = vi.fn();
const user = userEvent.setup();
const { unmount } = render(
<AddNode showTitle={false} onSubmit={submitSpy} />
);
expect(submitSpy).to.not.toHaveBeenCalled();
await user.type(
screen.getByLabelText("Node address"),
"http://haveno.network"
);
await user.type(screen.getByLabelText("Port"), "58080");
fireEvent.submit(screen.getByRole("button", { name: "Save" }));
expect(submitSpy).to.toHaveBeenCalledTimes(1);
unmount();
});
});

View File

@ -0,0 +1,100 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { Group, Space, Stack } from "@mantine/core";
import Joi from "joi";
import { joiResolver, useForm } from "@mantine/form";
import { Heading } from "@atoms/Typography";
import { TextInput } from "@atoms/TextInput";
import { PasswordInput } from "@atoms/PasswordInput";
import { Button } from "@atoms/Buttons";
interface AddNodeProps {
isLoading?: boolean;
onSubmit: (values: AddNodeFormValues) => void;
showTitle?: boolean;
}
export function AddNode(props: AddNodeProps) {
const { isLoading = false, onSubmit, showTitle = true } = props;
const { getInputProps, onSubmit: onFormSubmit } = useForm<AddNodeFormValues>({
initialValues: {
address: "",
port: "",
user: "",
password: "",
},
validate: joiResolver(validation),
});
const handleSubmit = (values: AddNodeFormValues) => {
onSubmit(values);
};
return (
<Stack spacing="sm">
{showTitle && <Heading order={3}>Add a new node</Heading>}
<form onSubmit={onFormSubmit(handleSubmit)}>
<Stack>
<TextInput
aria-label="Node address"
id="address"
label="Node address"
required
{...getInputProps("address")}
/>
<TextInput
aria-label="Port"
id="port"
label="Port"
required
{...getInputProps("port")}
/>
<TextInput
id="user"
label="Login (optional)"
{...getInputProps("user")}
/>
<PasswordInput
id="password"
label="Password (optional)"
{...getInputProps("password")}
/>
<Space h="md" />
<Group position="right">
<Button loaderPosition="right" loading={isLoading} type="submit">
Save
</Button>
</Group>
</Stack>
</form>
</Stack>
);
}
export interface AddNodeFormValues {
address: string;
port: string;
user?: string;
password?: string;
}
const validation = Joi.object<AddNodeFormValues>({
address: Joi.string().uri({ allowRelative: false }),
port: Joi.number().port(),
user: Joi.string().allow("").optional(),
password: Joi.string().allow("").optional(),
});

View File

@ -0,0 +1,340 @@
// Vitest Snapshot v1
exports[`organisms::AddNode > can render the form without the title 1`] = `
<DocumentFragment>
<div
class="mantine-Stack-root mantine-ngu3vw"
>
<form>
<div
class="mantine-Stack-root mantine-lfk3cq"
>
<div
class="mantine-TextInput-root mantine-18udhi"
>
<label
class="mantine-TextInput-label mantine-7802ha"
for="address"
id="address-label"
>
Node address
<span
class="mantine-1m203yh mantine-TextInput-required"
>
*
</span>
</label>
<div
class="mantine-TextInput-wrapper mantine-12sbrde"
>
<input
aria-invalid="false"
aria-label="Node address"
class="mantine-TextInput-defaultVariant mantine-TextInput-input mantine-dagq8e"
id="address"
required=""
type="text"
value=""
/>
</div>
</div>
<div
class="mantine-TextInput-root mantine-18udhi"
>
<label
class="mantine-TextInput-label mantine-7802ha"
for="port"
id="port-label"
>
Port
<span
class="mantine-1m203yh mantine-TextInput-required"
>
*
</span>
</label>
<div
class="mantine-TextInput-wrapper mantine-12sbrde"
>
<input
aria-invalid="false"
aria-label="Port"
class="mantine-TextInput-defaultVariant mantine-TextInput-input mantine-dagq8e"
id="port"
required=""
type="text"
value=""
/>
</div>
</div>
<div
class="mantine-TextInput-root mantine-18udhi"
>
<label
class="mantine-TextInput-label mantine-7802ha"
for="user"
id="user-label"
>
Login (optional)
</label>
<div
class="mantine-TextInput-wrapper mantine-12sbrde"
>
<input
aria-invalid="false"
class="mantine-TextInput-defaultVariant mantine-TextInput-input mantine-dagq8e"
id="user"
type="text"
value=""
/>
</div>
</div>
<div
class="mantine-PasswordInput-root mantine-18udhi"
>
<label
class="mantine-PasswordInput-label mantine-7802ha"
for="password"
id="password-label"
>
Password (optional)
</label>
<div
class="mantine-PasswordInput-wrapper mantine-12sbrde"
>
<div
aria-invalid="false"
class="mantine-PasswordInput-defaultVariant mantine-PasswordInput-input mantine-PasswordInput-input mantine-1i2duzg"
>
<input
class="mantine-PasswordInput-innerInput mantine-1bj8gkk"
id="password"
type="password"
value=""
/>
</div>
<div
class="mantine-o3oqoy mantine-PasswordInput-rightSection"
>
<button
aria-hidden="true"
class="mantine-ActionIcon-hover mantine-ActionIcon-root mantine-PasswordInput-visibilityToggle mantine-vao037"
tabindex="-1"
type="button"
>
<svg
fill="none"
height="15"
viewBox="0 0 15 15"
width="15"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M7.5 11C4.80285 11 2.52952 9.62184 1.09622 7.50001C2.52952 5.37816 4.80285 4 7.5 4C10.1971 4 12.4705 5.37816 13.9038 7.50001C12.4705 9.62183 10.1971 11 7.5 11ZM7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C1.65639 10.2936 4.30786 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C13.3436 4.70638 10.6921 3 7.5 3ZM7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5C9.5 6.39543 8.60457 5.5 7.5 5.5C6.39543 5.5 5.5 6.39543 5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5Z"
fill="currentColor"
fill-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
<div
class="mantine-1ne3752"
/>
<div
class="mantine-Group-root mantine-147cgkf"
>
<button
class="mantine-Button-filled mantine-Button-root mantine-Group-child mantine-1bqp2m7"
type="submit"
>
<div
class="mantine-3xbgk5 mantine-Button-inner"
>
<span
class="mantine-qo1k2 mantine-Button-label"
>
Save
</span>
</div>
</button>
</div>
</div>
</form>
</div>
</DocumentFragment>
`;
exports[`organisms::AddNode > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-Stack-root mantine-ngu3vw"
>
<h3
class="mantine-Title-root mantine-ha7ih2"
>
Add a new node
</h3>
<form>
<div
class="mantine-Stack-root mantine-lfk3cq"
>
<div
class="mantine-TextInput-root mantine-18udhi"
>
<label
class="mantine-TextInput-label mantine-7802ha"
for="address"
id="address-label"
>
Node address
<span
class="mantine-1m203yh mantine-TextInput-required"
>
*
</span>
</label>
<div
class="mantine-TextInput-wrapper mantine-12sbrde"
>
<input
aria-invalid="false"
aria-label="Node address"
class="mantine-TextInput-defaultVariant mantine-TextInput-input mantine-dagq8e"
id="address"
required=""
type="text"
value=""
/>
</div>
</div>
<div
class="mantine-TextInput-root mantine-18udhi"
>
<label
class="mantine-TextInput-label mantine-7802ha"
for="port"
id="port-label"
>
Port
<span
class="mantine-1m203yh mantine-TextInput-required"
>
*
</span>
</label>
<div
class="mantine-TextInput-wrapper mantine-12sbrde"
>
<input
aria-invalid="false"
aria-label="Port"
class="mantine-TextInput-defaultVariant mantine-TextInput-input mantine-dagq8e"
id="port"
required=""
type="text"
value=""
/>
</div>
</div>
<div
class="mantine-TextInput-root mantine-18udhi"
>
<label
class="mantine-TextInput-label mantine-7802ha"
for="user"
id="user-label"
>
Login (optional)
</label>
<div
class="mantine-TextInput-wrapper mantine-12sbrde"
>
<input
aria-invalid="false"
class="mantine-TextInput-defaultVariant mantine-TextInput-input mantine-dagq8e"
id="user"
type="text"
value=""
/>
</div>
</div>
<div
class="mantine-PasswordInput-root mantine-18udhi"
>
<label
class="mantine-PasswordInput-label mantine-7802ha"
for="password"
id="password-label"
>
Password (optional)
</label>
<div
class="mantine-PasswordInput-wrapper mantine-12sbrde"
>
<div
aria-invalid="false"
class="mantine-PasswordInput-defaultVariant mantine-PasswordInput-input mantine-PasswordInput-input mantine-1i2duzg"
>
<input
class="mantine-PasswordInput-innerInput mantine-1bj8gkk"
id="password"
type="password"
value=""
/>
</div>
<div
class="mantine-o3oqoy mantine-PasswordInput-rightSection"
>
<button
aria-hidden="true"
class="mantine-ActionIcon-hover mantine-ActionIcon-root mantine-PasswordInput-visibilityToggle mantine-vao037"
tabindex="-1"
type="button"
>
<svg
fill="none"
height="15"
viewBox="0 0 15 15"
width="15"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M7.5 11C4.80285 11 2.52952 9.62184 1.09622 7.50001C2.52952 5.37816 4.80285 4 7.5 4C10.1971 4 12.4705 5.37816 13.9038 7.50001C12.4705 9.62183 10.1971 11 7.5 11ZM7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C1.65639 10.2936 4.30786 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C13.3436 4.70638 10.6921 3 7.5 3ZM7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5C9.5 6.39543 8.60457 5.5 7.5 5.5C6.39543 5.5 5.5 6.39543 5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5Z"
fill="currentColor"
fill-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
<div
class="mantine-1ne3752"
/>
<div
class="mantine-Group-root mantine-147cgkf"
>
<button
class="mantine-Button-filled mantine-Button-root mantine-Group-child mantine-1bqp2m7"
type="submit"
>
<div
class="mantine-3xbgk5 mantine-Button-inner"
>
<span
class="mantine-qo1k2 mantine-Button-label"
>
Save
</span>
</div>
</button>
</div>
</div>
</form>
</div>
</DocumentFragment>
`;

View File

@ -14,4 +14,4 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
export * from "./NodeStatus"; export * from "./AddNode";

View File

@ -14,11 +14,15 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import type { FormEvent } from "react";
import { Stack, Space, Group } from "@mantine/core"; import { Stack, Space, Group } from "@mantine/core";
import { BodyText, Heading } from "@atoms/Typography"; import { BodyText, Heading } from "@atoms/Typography";
import { Button, TextButton } from "@atoms/Buttons"; import { Button, TextButton } from "@atoms/Buttons";
import { Select } from "@atoms/Select"; import { Select } from "@atoms/Select";
import type { FormEvent } from "react"; import {
HAVENO_DAEMON_PASSWORD,
HAVENO_DAEMON_URL,
} from "@constants/haveno-daemon";
interface SelectMoneroNodeProps { interface SelectMoneroNodeProps {
onGoBack: () => void; onGoBack: () => void;
@ -32,8 +36,8 @@ export function SelectMoneroNode(props: SelectMoneroNodeProps) {
ev.preventDefault(); ev.preventDefault();
// TODO: fix // TODO: fix
onNext({ onNext({
url: "http://192.168.29.59:8080", url: HAVENO_DAEMON_URL,
password: "apitest", password: HAVENO_DAEMON_PASSWORD,
}); });
}; };

View File

@ -35,7 +35,7 @@ export enum LangKeys {
AccountNodeSettingsLocal = "account.nodeSecurity.local.title", AccountNodeSettingsLocal = "account.nodeSecurity.local.title",
AccountNodeSettingsRemote = "account.nodeSecurity.remote.title", AccountNodeSettingsRemote = "account.nodeSecurity.remote.title",
AccountNodeFieldBlockchainLocation = "account.nodeSecurity.blockchainLocation", AccountNodeFieldBlockchainLocation = "account.nodeSecurity.blockchainLocation",
AccountNodeFieldDaemonAddress = "account.nodeSecurity.daemonAddress", AccountNodeFieldBootstrapUrl = "account.nodeSecurity.bootstrapUrl",
AccountNodeFieldDaemonFlags = "account.nodeSecurity.daemonFlags", AccountNodeFieldDaemonFlags = "account.nodeSecurity.daemonFlags",
AccountNodeFieldPort = "account.nodeSecurity.port", AccountNodeFieldPort = "account.nodeSecurity.port",
AccountNodeStopDaemon = "account.nodeSecurity.stopDaemon", AccountNodeStopDaemon = "account.nodeSecurity.stopDaemon",

View File

@ -39,7 +39,7 @@ const LangPackEN: { [key in LangKeys]: string } = {
[LangKeys.AccountNodeSettingsLocal]: "Local Node", [LangKeys.AccountNodeSettingsLocal]: "Local Node",
[LangKeys.AccountNodeSettingsRemote]: "Remote Node", [LangKeys.AccountNodeSettingsRemote]: "Remote Node",
[LangKeys.AccountNodeFieldBlockchainLocation]: "Blockchain location", [LangKeys.AccountNodeFieldBlockchainLocation]: "Blockchain location",
[LangKeys.AccountNodeFieldDaemonAddress]: "Daemon Address", [LangKeys.AccountNodeFieldBootstrapUrl]: "Bootstrap URL",
[LangKeys.AccountNodeFieldPort]: "Port", [LangKeys.AccountNodeFieldPort]: "Port",
[LangKeys.AccountNodeFieldDaemonFlags]: "Daemon startup flags", [LangKeys.AccountNodeFieldDaemonFlags]: "Daemon startup flags",
[LangKeys.AccountNodeStopDaemon]: "Stop daemon", [LangKeys.AccountNodeStopDaemon]: "Stop daemon",

View File

@ -40,7 +40,7 @@ const LangPackES: { [key in LangKeys]: string } = {
[LangKeys.AccountNodeSettingsRemote]: "Nodo Remoto", [LangKeys.AccountNodeSettingsRemote]: "Nodo Remoto",
[LangKeys.AccountNodeFieldBlockchainLocation]: [LangKeys.AccountNodeFieldBlockchainLocation]:
"Ubicación de cadena de bloques", "Ubicación de cadena de bloques",
[LangKeys.AccountNodeFieldDaemonAddress]: "Dirección del demonio", [LangKeys.AccountNodeFieldBootstrapUrl]: "Dirección URL de arranque",
[LangKeys.AccountNodeFieldPort]: "Puerto", [LangKeys.AccountNodeFieldPort]: "Puerto",
[LangKeys.AccountNodeFieldDaemonFlags]: "Indicadores de inicio de daemon", [LangKeys.AccountNodeFieldDaemonFlags]: "Indicadores de inicio de daemon",
[LangKeys.AccountNodeStopDaemon]: "Detener demonio", [LangKeys.AccountNodeStopDaemon]: "Detener demonio",

View File

@ -18,9 +18,10 @@ export enum QueryKeys {
// Haveno // Haveno
Balances = "Haveno.Balances", Balances = "Haveno.Balances",
HavenoVersion = "Haveno.Version", HavenoVersion = "Haveno.Version",
MoneroConnection = "Haveno.MoneroConnection",
MoneroConnections = "Haveno.MoneroConnections",
MoneroNodeIsRunning = "Haveno.MoneroNodeIsRunning", MoneroNodeIsRunning = "Haveno.MoneroNodeIsRunning",
MoneroNodeSettings = "Haveno.MoneroNodeSettings", MoneroNodeSettings = "Haveno.MoneroNodeSettings",
MoneroRemoteNodes = "Haveno.MoneroRemoteNodes",
PaymentAccounts = "Haveno.PaymentAccounts", PaymentAccounts = "Haveno.PaymentAccounts",
Prices = "Haveno.Prices", Prices = "Haveno.Prices",
PrimaryAddress = "Haveno.PrimaryAddress", PrimaryAddress = "Haveno.PrimaryAddress",
@ -29,6 +30,7 @@ export enum QueryKeys {
// Storage // Storage
StorageAccountInfo = "Storage.AccountInfo", StorageAccountInfo = "Storage.AccountInfo",
StoragePreferences = "Storage.Preferences", StoragePreferences = "Storage.Preferences",
StorageRemoteMoneroNode = "Storage.RemoteMoneroNode",
// Others // Others
AuthSession = "AuthSession", AuthSession = "AuthSession",

View File

@ -23,10 +23,10 @@ export const ROUTES = {
RestoreBackup: "/onboarding/restore-backup", RestoreBackup: "/onboarding/restore-backup",
// Account routes // Account routes
AccountPaymentAccounts: "/account/payment-accounts", PaymentAccounts: "/account/payment-accounts",
AccountAddPaymentAccount: "/account/payment-accounts/add", AddPaymentAccount: "/account/payment-accounts/add",
AccountNodeSettings: "/account/node-settings", NodeSettings: "/account/node-settings",
AccountBackup: "/account/backup", Backup: "/account/backup",
AccountWallet: "/account/wallet", Wallet: "/account/wallet",
AccountSecurity: "/account/security", Security: "/account/security",
}; };

View File

@ -15,31 +15,39 @@
// ============================================================================= // =============================================================================
import { useMutation, useQueryClient } from "react-query"; import { useMutation, useQueryClient } from "react-query";
import { UrlConnection } from "haveno-ts";
import { QueryKeys } from "@constants/query-keys"; import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient"; import { useHavenoClient } from "./useHavenoClient";
interface SetMeneroNodeSettingsVariables { interface Variables {
blockchainPath?: string; address: string;
bootstrapUrl?: string; port: string;
startupFlags?: Array<string>; user?: string;
password?: string;
} }
export function useSetMoneroNodeSettings() { export function useAddMoneroNode() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const client = useHavenoClient(); const client = useHavenoClient();
return useMutation( return useMutation<void, Error, Variables>(
async (data: SetMeneroNodeSettingsVariables) => { async (data: Variables) => {
const nodeSettings = await client.getMoneroNodeSettings(); const url = new URL(data.address);
if (data.port) {
data.blockchainPath && url.port = data.port + "";
nodeSettings?.setBlockchainPath(data.blockchainPath); }
data.startupFlags && nodeSettings?.setStartupFlagsList(data.startupFlags); const conn = new UrlConnection().setUrl(url.toString()).setPriority(1);
data.bootstrapUrl && nodeSettings?.setBootstrapUrl(data.bootstrapUrl); if (data.user) {
conn.setUsername(data.user);
}
if (data.password) {
conn.setPassword(data.password);
}
client.addMoneroConnection(conn);
}, },
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.MoneroNodeSettings); queryClient.invalidateQueries(QueryKeys.MoneroConnections);
}, },
} }
); );

View File

@ -14,8 +14,8 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import { QueryKeys } from "@constants/query-keys";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient"; import { useHavenoClient } from "./useHavenoClient";
export function useAddress() { export function useAddress() {

View File

@ -0,0 +1,26 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useQuery } from "react-query";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient";
export function useGetMoneroConnection() {
const client = useHavenoClient();
return useQuery(QueryKeys.MoneroConnection, async () =>
client.getMoneroConnection()
);
}

View File

@ -14,8 +14,8 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import { HavenoClient } from "haveno-ts";
import { useRef } from "react"; import { useRef } from "react";
import { HavenoClient } from "haveno-ts";
import { import {
HAVENO_DAEMON_PASSWORD, HAVENO_DAEMON_PASSWORD,
HAVENO_DAEMON_URL, HAVENO_DAEMON_URL,

View File

@ -14,8 +14,8 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import { QueryKeys } from "@constants/query-keys";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient"; import { useHavenoClient } from "./useHavenoClient";
export function useHavenoVersion() { export function useHavenoVersion() {

View File

@ -20,13 +20,18 @@ import { useHavenoClient } from "./useHavenoClient";
export function useIsMoneroNodeRunning() { export function useIsMoneroNodeRunning() {
const client = useHavenoClient(); const client = useHavenoClient();
return useQuery<boolean, Error>(
return useQuery<boolean, Error>(QueryKeys.MoneroNodeIsRunning, async () => { QueryKeys.MoneroNodeIsRunning,
try { async () => {
const value = await client.isMoneroNodeRunning(); try {
return value; const value = await client.isMoneroNodeRunning();
} catch { return value;
return false; } catch {
return false;
}
},
{
staleTime: 10_000, // 10 sec
} }
}); );
} }

View File

@ -15,25 +15,17 @@
// ============================================================================= // =============================================================================
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import type { UrlConnection } from "haveno-ts";
import { QueryKeys } from "@constants/query-keys"; import { QueryKeys } from "@constants/query-keys";
// import { useHavenoClient } from "./useHavenoClient"; import { useHavenoClient } from "./useHavenoClient";
interface MoneroRemoteNodes { export function useMoneroConnections() {
title: string; const client = useHavenoClient();
isActive: boolean; return useQuery<Array<UrlConnection.AsObject>, Error>(
} QueryKeys.MoneroConnections,
export function useMoneroRemoteNodes() {
// const client = useHavenoClient();
return useQuery<MoneroRemoteNodes[], Error>(
QueryKeys.MoneroRemoteNodes,
async () => { async () => {
return Promise.resolve([ const connections = await client.getMoneroConnections();
{ title: "node.moneroworldcom:18089", isActive: true }, return connections.map((conn) => conn.toObject());
{ title: "node.xmr.pt:18081", isActive: true },
{ title: "node.monero.net:18081", isActive: true },
]);
} }
); );
} }

View File

@ -14,18 +14,15 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import { QueryKeys } from "@constants/query-keys";
import type { MoneroNodeSettings } from "haveno-ts";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import type { MoneroNodeSettings } from "haveno-ts";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient"; import { useHavenoClient } from "./useHavenoClient";
export function useMoneroNodeSettings() { export function useMoneroNodeSettings() {
const client = useHavenoClient(); const client = useHavenoClient();
return useQuery<MoneroNodeSettings | undefined, Error>( return useQuery<MoneroNodeSettings | undefined, Error>(
QueryKeys.MoneroNodeSettings, QueryKeys.MoneroNodeSettings,
async () => { async () => client.getMoneroNodeSettings()
return client.getMoneroNodeSettings();
}
); );
} }

View File

@ -14,9 +14,9 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import { QueryKeys } from "@constants/query-keys";
import type { PaymentAccount } from "haveno-ts";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import type { PaymentAccount } from "haveno-ts";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient"; import { useHavenoClient } from "./useHavenoClient";
export function usePaymentAccounts() { export function usePaymentAccounts() {

View File

@ -1,37 +0,0 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { QueryKeys } from "@constants/query-keys";
import { useQuery } from "react-query";
import { useHavenoClient } from "./useHavenoClient";
export function usePaymentMethods() {
const client = useHavenoClient();
return useQuery(QueryKeys.PaymentMethods, async () => {
// TODO: replace with getSupportedAssets(): TradeCurrency[]
// const mns = await client.getMoneroNodeSettings();
const mns = await client._moneroNodeClient.getMoneroNodeSettings({}, {});
console.log("monero node settings: ", mns?.toObject());
if (mns) {
const mns2 = mns.setStartupFlagsList(["foo1"]);
mns;
}
const assetCodes = await client.getSupportedAssetCodes();
return await Promise.all(
assetCodes.map((assetCode) => client.getPaymentMethods(assetCode))
);
});
}

View File

@ -14,9 +14,9 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import { useQuery } from "react-query";
import type { MarketPriceInfo } from "haveno-ts"; import type { MarketPriceInfo } from "haveno-ts";
import { QueryKeys } from "@constants/query-keys"; import { QueryKeys } from "@constants/query-keys";
import { useQuery } from "react-query";
import { useHavenoClient } from "./useHavenoClient"; import { useHavenoClient } from "./useHavenoClient";
export function usePrices() { export function usePrices() {

View File

@ -0,0 +1,61 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useMutation, useQueryClient } from "react-query";
import { MoneroNodeSettings } from "haveno-ts";
import { QueryKeys } from "@constants/query-keys";
import { useSaveRemoteNode } from "@hooks/storage/useSaveRemoteNode";
import { useHavenoClient } from "./useHavenoClient";
interface Variables {
blockchainPath: string;
bootstrapUrl: string;
startupFlags: Array<string>;
}
export function useSaveLocalMoneroNode() {
const queryClient = useQueryClient();
const client = useHavenoClient();
const { mutateAsync: saveRemoteNode } = useSaveRemoteNode();
return useMutation<void, Error, Variables>(
async (data: Variables) => {
const nodeSettings = new MoneroNodeSettings();
nodeSettings.setBlockchainPath(data.blockchainPath);
nodeSettings.setStartupFlagsList(data.startupFlags);
nodeSettings.setBootstrapUrl(data.bootstrapUrl);
if (await client.isMoneroNodeRunning()) {
// stop the node if it's running
await client.stopMoneroNode();
}
// start the node with new settings
try {
await client.startMoneroNode(nodeSettings);
await saveRemoteNode({}); // clear the saved remote node
} catch (ex) {
console.log(ex);
throw new Error("Failed to start the monero node");
}
},
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.MoneroConnections);
queryClient.invalidateQueries(QueryKeys.MoneroNodeIsRunning);
},
}
);
}

View File

@ -14,16 +14,30 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import { useMutation } from "react-query"; import { useMutation, useQueryClient } from "react-query";
import { useSaveRemoteNode } from "@hooks/storage/useSaveRemoteNode";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient"; import { useHavenoClient } from "./useHavenoClient";
interface Variables { interface Variables {
connection: string; uri: string;
} }
export function useSetMoneroConnection() { export function useSetMoneroConnection() {
const queryClient = useQueryClient();
const { mutateAsync: saveRemoteNode } = useSaveRemoteNode();
const client = useHavenoClient(); const client = useHavenoClient();
return useMutation(async (variables: Variables) =>
client.setMoneroConnection(variables.connection) return useMutation<void, Error, Variables>(
async (variables: Variables) => {
await client.setMoneroConnection(variables.uri);
// save to storage
await saveRemoteNode({ uri: variables.uri });
},
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.MoneroConnection);
},
}
); );
} }

View File

@ -14,17 +14,17 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import { QueryKeys } from "@constants/query-keys";
import type { MoneroNodeSettings } from "haveno-ts";
import { useMutation, useQueryClient } from "react-query"; import { useMutation, useQueryClient } from "react-query";
import type { MoneroNodeSettings } from "haveno-ts";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient"; import { useHavenoClient } from "./useHavenoClient";
export function useStartMoneroNode() { export function useStartMoneroNode() {
const client = useHavenoClient();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const client = useHavenoClient();
return useMutation<void, Error, MoneroNodeSettings>( return useMutation<void, Error, MoneroNodeSettings>(
(data: MoneroNodeSettings) => client.startMoneroNode(data), async (data: MoneroNodeSettings) => client.startMoneroNode(data),
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.MoneroNodeIsRunning); queryClient.invalidateQueries(QueryKeys.MoneroNodeIsRunning);

View File

@ -14,15 +14,15 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import { QueryKeys } from "@constants/query-keys";
import { useMutation, useQueryClient } from "react-query"; import { useMutation, useQueryClient } from "react-query";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient"; import { useHavenoClient } from "./useHavenoClient";
export function useStopMoneroNode() { export function useStopMoneroNode() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const client = useHavenoClient(); const client = useHavenoClient();
return useMutation(() => client.stopMoneroNode(), { return useMutation(async () => client.stopMoneroNode(), {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.MoneroNodeIsRunning); queryClient.invalidateQueries(QueryKeys.MoneroNodeIsRunning);
}, },

View File

@ -16,8 +16,8 @@
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { QueryKeys } from "@constants/query-keys"; import { QueryKeys } from "@constants/query-keys";
// import { useHavenoClient } from "./useHavenoClient";
import { SyncStatus } from "@constants/sync-status"; import { SyncStatus } from "@constants/sync-status";
// import { useHavenoClient } from "./useHavenoClient";
export function useSyncStatus() { export function useSyncStatus() {
// const client = useHavenoClient(); // const client = useHavenoClient();
@ -28,7 +28,7 @@ export function useSyncStatus() {
return SyncStatus.NotSynced; return SyncStatus.NotSynced;
}, },
{ {
staleTime: 10000, staleTime: 10_000,
} }
); );
} }

View File

@ -16,7 +16,7 @@
import { useMutation, useQueryClient } from "react-query"; import { useMutation, useQueryClient } from "react-query";
import { QueryKeys } from "@constants/query-keys"; import { QueryKeys } from "@constants/query-keys";
import { getIpcError } from "@utils/get-ipc-error"; import { getIpcError } from "@src/utils/getIpcError";
import { createSession } from "@utils/session"; import { createSession } from "@utils/session";
interface Variables { interface Variables {

View File

@ -20,7 +20,6 @@ import { useMutation, useQueryClient } from "react-query";
interface Variables { interface Variables {
password: string; password: string;
primaryFiat: string; primaryFiat: string;
moneroNode: string;
} }
export function useCreateAccount() { export function useCreateAccount() {
@ -31,7 +30,6 @@ export function useCreateAccount() {
await Promise.all([ await Promise.all([
window.electronStore.setPassword({ newPassword: variables.password }), window.electronStore.setPassword({ newPassword: variables.password }),
window.electronStore.setPrimaryFiat(variables.primaryFiat), window.electronStore.setPrimaryFiat(variables.primaryFiat),
window.electronStore.setMoneroNode(variables.moneroNode),
]); ]);
}, },
{ {

View File

@ -0,0 +1,27 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useEffect, useState } from "react";
import { usePreferences } from "./usePreferences";
export function useIsLocalNodeSelected() {
const [data, setData] = useState(false);
const { data: preferences, ...rest } = usePreferences();
useEffect(() => {
setData(!preferences?.selectedNode);
}, [preferences]);
return { data, ...rest };
}

View File

@ -0,0 +1,37 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { QueryKeys } from "@constants/query-keys";
import { useMutation, useQueryClient } from "react-query";
interface Variables {
uri?: string;
}
export function useSaveRemoteNode() {
const queryClient = useQueryClient();
return useMutation<void, Error, Variables>(
async (variables: Variables) => {
return window.electronStore.setMoneroNode(variables.uri);
},
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.StoragePreferences);
},
}
);
}

View File

@ -16,7 +16,7 @@
import { AccountLayout } from "@templates/AccountLayout"; import { AccountLayout } from "@templates/AccountLayout";
export function AccountBackup() { export function Backup() {
return ( return (
<AccountLayout> <AccountLayout>
<h1>Account Backup</h1> <h1>Account Backup</h1>

View File

@ -1,79 +0,0 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { Stack, createStyles, Group } from "@mantine/core";
import { FormattedMessage } from "react-intl";
import { Button } from "@atoms/Buttons";
import { NodeStatus, NodeStatusType } from "@atoms/NodeStatus";
import { LangKeys } from "@constants/lang";
import { useMoneroRemoteNodes } from "@hooks/haveno/useMoneroRemoteNodes";
export function NodeRemoteStatus() {
const { data: remoteNodes } = useMoneroRemoteNodes();
return (
<Stack>
{remoteNodes?.map((node) => (
<NodeStatus
key={node.title}
title={node.title}
status={
node.isActive ? NodeStatusType.Active : NodeStatusType.Inactive
}
/>
))}
<AddNewNodeButton />
<Group position="right" mt="sm">
<Button size="md">
<FormattedMessage id={LangKeys.Save} defaultMessage="Save" />
</Button>
</Group>
</Stack>
);
}
function AddNewNodeButton({ ...rest }) {
const { classes } = useStyles();
return (
<Button
variant="subtle"
color="dark"
classNames={{
root: classes.root,
inner: classes.inner,
}}
{...rest}
>
+{" "}
<FormattedMessage
id={LangKeys.AccountSettingsAddNode}
defaultMessage="Add a new node"
/>
</Button>
);
}
const useStyles = createStyles(() => ({
root: {
padding: "0.8rem",
height: 45,
},
inner: {
justifyContent: "start",
},
}));

View File

@ -1,56 +0,0 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { Stack, Box, createStyles } from "@mantine/core";
import { AccountLayout } from "@templates/AccountLayout";
import { LangKeys } from "@constants/lang";
import { NodeSettingsSwitch } from "./NodeSettingsSwitch";
import { BodyText, Heading } from "@atoms/Typography";
export function AccountNodeSettings() {
const { classes } = useStyles();
return (
<AccountLayout>
<Box className={classes.content}>
<Stack spacing="sm">
<Heading stringId={LangKeys.AccountNodeSettingsTitle} order={3}>
Your node settings
</Heading>
<BodyText
stringId={LangKeys.AccountNodeSettingsDesc}
size="md"
className={classes.paragraph}
>
Using a local node is recommended, but does require loading the
entire blockchain. Choose remote node if you prefer a faster but
less secure experience.
</BodyText>
<NodeSettingsSwitch />
</Stack>
</Box>
</AccountLayout>
);
}
const useStyles = createStyles((theme) => ({
content: {
maxWidth: theme.other.contentWidthMd,
},
paragraph: {
marginBottom: theme.spacing.xl,
},
}));

View File

@ -1,52 +0,0 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ReactNode } from "react";
import { BodyText } from "@atoms/Typography";
import { useMoneroNodeSettings } from "@hooks/haveno/useMoneroNodeSettings";
import { useIsMoneroNodeRunning } from "@hooks/haveno/useIsMoneroNodeRunning";
import { useMoneroRemoteNodes } from "@hooks/haveno/useMoneroRemoteNodes";
interface NodeSettingsBootProps {
children: ReactNode;
}
export function LocalNodeSettingsBoot({ children }: NodeSettingsBootProps) {
const { isLoading: isNodeSettingsLoading } = useMoneroNodeSettings();
const { isLoading: isMoneroNodeIsLoading } = useIsMoneroNodeRunning();
return isNodeSettingsLoading || isMoneroNodeIsLoading ? (
<BodyText>Loading settings...</BodyText>
) : (
<>{children}</>
);
}
interface RemoteNodeSettingsBootProps {
children: ReactNode;
}
export function RemoteNodeSettingsBoot({
children,
}: RemoteNodeSettingsBootProps) {
const { isLoading: isMoneroRemoteLoading } = useMoneroRemoteNodes();
return isMoneroRemoteLoading ? (
<BodyText>Loading settings...</BodyText>
) : (
<>{children}</>
);
}

View File

@ -1,77 +0,0 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { createStyles } from "@mantine/core";
import { FormattedMessage } from "react-intl";
import { LangKeys } from "@constants/lang";
import { NodeConnectSwitch } from "@molecules/NodeConnectSwitch";
import { ReactComponent as CloudIcon } from "@assets/setting-cloud.svg";
import { ReactComponent as ServerIcon } from "@assets/setting-server.svg";
import { NodeLocalForm } from "./NodeLocalForm";
import { NodeRemoteStatus } from "./NodeRemoteStatus";
import {
LocalNodeSettingsBoot,
RemoteNodeSettingsBoot,
} from "./NodeSettingsBoot";
export function NodeSettingsSwitch() {
const { classes } = useStyles();
return (
<NodeConnectSwitch
initialTab="local-node"
className={classes.connectSwitch}
>
<NodeConnectSwitch.Method
active={true}
current={true}
tabKey="local-node"
label={
<FormattedMessage
id={LangKeys.AccountNodeSettingsLocal}
defaultMessage="Local Node"
/>
}
icon={<ServerIcon width={32} height={62} />}
>
<LocalNodeSettingsBoot>
<NodeLocalForm />
</LocalNodeSettingsBoot>
</NodeConnectSwitch.Method>
<NodeConnectSwitch.Method
tabKey="remote-node"
label={
<FormattedMessage
id={LangKeys.AccountNodeSettingsRemote}
defaultMessage="Remote Node"
/>
}
icon={<CloudIcon width={58} height={54} />}
>
<RemoteNodeSettingsBoot>
<NodeRemoteStatus />
</RemoteNodeSettingsBoot>
</NodeConnectSwitch.Method>
</NodeConnectSwitch>
);
}
const useStyles = createStyles(() => ({
connectSwitch: {
marginBottom: "2rem",
},
}));

View File

@ -19,13 +19,11 @@ import { ROUTES } from "@constants/routes";
import { PaymentMethodList } from "@organisms/PaymentMethodList"; import { PaymentMethodList } from "@organisms/PaymentMethodList";
import { AccountLayout } from "@templates/AccountLayout"; import { AccountLayout } from "@templates/AccountLayout";
export function AccountPaymentAccounts() { export function PaymentAccounts() {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<AccountLayout> <AccountLayout>
<PaymentMethodList <PaymentMethodList onAdd={() => navigate(ROUTES.AddPaymentAccount)} />
onAdd={() => navigate(ROUTES.AccountAddPaymentAccount)}
/>
</AccountLayout> </AccountLayout>
); );
} }

View File

@ -24,9 +24,7 @@ export function PaymentMethods() {
return ( return (
<NavbarLayout> <NavbarLayout>
<PaymentMethodList <PaymentMethodList onAdd={() => navigate(ROUTES.AddPaymentAccount)} />
onAdd={() => navigate(ROUTES.AccountAddPaymentAccount)}
/>
</NavbarLayout> </NavbarLayout>
); );
} }

View File

@ -20,7 +20,7 @@ import { AccountLayout } from "@templates/AccountLayout";
import { Heading, BodyText } from "@atoms/Typography"; import { Heading, BodyText } from "@atoms/Typography";
import { ChangePassword } from "@organisms/ChangePassword"; import { ChangePassword } from "@organisms/ChangePassword";
export function AccountSecurity() { export function Security() {
const { classes } = useStyles(); const { classes } = useStyles();
return ( return (

View File

@ -14,70 +14,82 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import { Box, Stack, Grid, Group } from "@mantine/core"; import { useEffect } from "react";
import { joiResolver, useForm } from "@mantine/form";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { Stack, Grid, Group } from "@mantine/core";
import { joiResolver, useForm } from "@mantine/form";
import { showNotification } from "@mantine/notifications"; import { showNotification } from "@mantine/notifications";
import { Button } from "@atoms/Buttons"; import { Button } from "@atoms/Buttons";
import { LangKeys } from "@constants/lang";
import { TextInput } from "@atoms/TextInput"; import { TextInput } from "@atoms/TextInput";
import { useMoneroNodeSettings } from "@hooks/haveno/useMoneroNodeSettings"; import { useMoneroNodeSettings } from "@hooks/haveno/useMoneroNodeSettings";
import { useSetMoneroNodeSettings } from "@hooks/haveno/useSetMoneroNodeSettings"; import { useSaveLocalMoneroNode } from "@hooks/haveno/useSaveLocalMoneroNode";
import { NodeLocalStopDaemon } from "./NodeLocalStopDaemon"; import { LangKeys } from "@constants/lang";
import type { NodeLocalFormValues } from "./_hooks"; import { StartStopDaemon } from "./StartStopDaemon";
import { useNodeLocalFormValidation } from "./_hooks"; import type { LocalSettingsFormValues } from "./_types";
import { useLocalSettingsValidation } from "./_hooks";
import { transformSettingsRequestToForm } from "./_utils"; import { transformSettingsRequestToForm } from "./_utils";
export function NodeLocalForm() { export function LocalNode() {
const { data: nodeSettings } = useMoneroNodeSettings(); const { data: nodeSettings } = useMoneroNodeSettings();
const { mutateAsync: updateNodeSettings } = useSetMoneroNodeSettings(); const { mutate: saveLocalNode, isLoading: isSaving } =
useSaveLocalMoneroNode();
const intl = useIntl(); const intl = useIntl();
const validation = useNodeLocalFormValidation(); const validation = useLocalSettingsValidation();
const form = useForm<NodeLocalFormValues>({ const { getInputProps, onSubmit, setValues } =
initialValues: { useForm<LocalSettingsFormValues>({
blockchainLocation: "", initialValues: {
startupFlags: "", blockchainLocation: "",
daemonAddress: "", startupFlags: "",
port: "", bootstrapUrl: "",
...(nodeSettings port: "",
? transformSettingsRequestToForm(nodeSettings.toObject()) },
: {}), validate: joiResolver(validation),
}, });
validate: joiResolver(validation),
});
const handleFormSubmit = (values: NodeLocalFormValues) => { const handleSubmit = (values: LocalSettingsFormValues) => {
updateNodeSettings({ saveLocalNode(
blockchainPath: values.blockchainLocation, {
startupFlags: values.startupFlags.split(", "), blockchainPath: values.blockchainLocation,
bootstrapUrl: `${values.daemonAddress}:${values.port}`, startupFlags: values.startupFlags.split(/\s|=/),
}) bootstrapUrl: values.bootstrapUrl
.then(() => { ? (new URL(values.bootstrapUrl).port = values.port)
showNotification({ : "",
color: "green", },
message: intl.formatMessage({ {
id: LangKeys.AccountNodeLocalSaveNotification, onSuccess: () => {
defaultMessage: "Local node settings updated successfully", showNotification({
}), color: "green",
}); message: intl.formatMessage({
}) id: LangKeys.AccountNodeLocalSaveNotification,
.catch((err) => { defaultMessage: "Local node settings saved successfully",
console.dir(err); }),
showNotification({ });
color: "red", },
message: err.message, onError: (err: Error) => {
title: "Something went wrong", console.dir(err);
}); showNotification({
}); color: "red",
message: err.message,
title: "Something went wrong",
});
},
}
);
}; };
return ( useEffect(() => {
<Box> if (nodeSettings) {
<NodeLocalStopDaemon /> setValues(transformSettingsRequestToForm(nodeSettings.toObject()));
}
}, [nodeSettings]);
<form onSubmit={form.onSubmit(handleFormSubmit)}> return (
<>
<StartStopDaemon />
<form onSubmit={onSubmit(handleSubmit)}>
<Stack spacing="lg"> <Stack spacing="lg">
<TextInput <TextInput
id="blockchainLocation" id="blockchainLocation"
@ -87,7 +99,7 @@ export function NodeLocalForm() {
defaultMessage="Blockchain location" defaultMessage="Blockchain location"
/> />
} }
{...form.getInputProps("blockchainLocation")} {...getInputProps("blockchainLocation")}
/> />
<TextInput <TextInput
id="startupFlags" id="startupFlags"
@ -97,20 +109,19 @@ export function NodeLocalForm() {
defaultMessage="Daemon startup flags" defaultMessage="Daemon startup flags"
/> />
} }
{...form.getInputProps("startupFlags")} {...getInputProps("startupFlags")}
/> />
<Grid> <Grid>
<Grid.Col span={9}> <Grid.Col span={9}>
<TextInput <TextInput
id="daemonAddress" id="bootstrapUrl"
label={ label={
<FormattedMessage <FormattedMessage
id={LangKeys.AccountNodeFieldDaemonAddress} id={LangKeys.AccountNodeFieldBootstrapUrl}
defaultMessage="Daemon Address" defaultMessage="Bootstrap URL"
/> />
} }
required {...getInputProps("bootstrapUrl")}
{...form.getInputProps("daemonAddress")}
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={3}> <Grid.Col span={3}>
@ -122,19 +133,23 @@ export function NodeLocalForm() {
defaultMessage="Port" defaultMessage="Port"
/> />
} }
required {...getInputProps("port")}
{...form.getInputProps("port")}
/> />
</Grid.Col> </Grid.Col>
</Grid> </Grid>
<Group position="right" mt="md"> <Group position="right" mt="md">
<Button size="md" type="submit"> <Button
loaderPosition="right"
loading={isSaving}
size="md"
type="submit"
>
<FormattedMessage id={LangKeys.Save} defaultMessage="Save" /> <FormattedMessage id={LangKeys.Save} defaultMessage="Save" />
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</form> </form>
</Box> </>
); );
} }

View File

@ -0,0 +1,144 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { Stack, Group, Modal } from "@mantine/core";
import { showNotification } from "@mantine/notifications";
import { Button, TextButton } from "@atoms/Buttons";
import { MoneroNodeListItem, NodeStatus } from "@atoms/MoneroNodeListItem";
import type { AddNodeFormValues } from "@organisms/AddNode";
import { AddNode } from "@organisms/AddNode";
import { useMoneroConnections } from "@hooks/haveno/useMoneroConnections";
import { useAddMoneroNode } from "@hooks/haveno/useAddMoneroNode";
import { useSetMoneroConnection } from "@hooks/haveno/useSetMoneroConnection";
import { useGetMoneroConnection } from "@hooks/haveno/useGetMoneroConnection";
import { LangKeys } from "@constants/lang";
export function RemoteNode() {
const [isAdding, setAdding] = useState(false);
const [selectedNode, setSelectedNode] = useState<string>();
const { data: connections } = useMoneroConnections();
const { data: selectedConnection } = useGetMoneroConnection();
const { mutate: addMoneroNode } = useAddMoneroNode();
const { mutate: setMoneroConnection } = useSetMoneroConnection();
const handleAddNode = (data: AddNodeFormValues) => {
const { address, port, user, password } = data;
addMoneroNode(
{
address,
port,
user,
password,
},
{
onError: (err) => {
console.log(err);
showNotification({
color: "red",
message: err.message,
title: "Something went wrong",
});
},
onSuccess: () => {
setAdding(false);
showNotification({
color: "green",
message: "Saved",
});
},
}
);
};
const handleSave = () => {
if (!selectedNode) {
return;
}
setMoneroConnection(
{ uri: selectedNode },
{
onError: (err) => {
console.dir(err);
showNotification({
color: "red",
message: err.message,
title: "Something went wrong",
});
},
onSuccess: () => {
showNotification({
color: "green",
message: "Saved",
});
},
}
);
};
useEffect(() => {
if (selectedConnection) {
setSelectedNode(selectedConnection.getUrl());
}
}, [selectedConnection]);
return (
<>
<Stack>
{connections?.map((conn) => (
<MoneroNodeListItem
key={conn.url}
title={conn.url}
status={
conn.onlineStatus === 1 ? NodeStatus.Active : NodeStatus.Inactive
}
isSelected={conn.url === selectedNode}
onClick={() => setSelectedNode(conn.url)}
/>
))}
<Group position="apart" mt="sm">
<TextButton onClick={() => setAdding(true)}>
<FormattedMessage
id={LangKeys.AccountSettingsAddNode}
defaultMessage="Add a new node"
/>
</TextButton>
<Button
disabled={
!selectedNode || selectedNode === selectedConnection?.getUrl()
}
size="md"
onClick={handleSave}
>
<FormattedMessage id={LangKeys.Save} defaultMessage="Save" />
</Button>
</Group>
</Stack>
<Modal
opened={isAdding}
withCloseButton
onClose={() => setAdding(false)}
size="sm"
radius="md"
title="Add a new node"
>
<AddNode onSubmit={handleAddNode} showTitle={false} />
</Modal>
</>
);
}

View File

@ -0,0 +1,104 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { Stack, createStyles } from "@mantine/core";
import { FormattedMessage } from "react-intl";
import { NodeConnectSwitch } from "@molecules/NodeConnectSwitch";
import { BodyText, Heading } from "@atoms/Typography";
import { AccountLayout } from "@templates/AccountLayout";
import { ReactComponent as CloudIcon } from "@assets/setting-cloud.svg";
import { ReactComponent as ServerIcon } from "@assets/setting-server.svg";
import { useIsLocalNodeSelected } from "@hooks/storage/useIsLocalNodeSelected";
import { LangKeys } from "@constants/lang";
import { LocalNode } from "./LocalNode";
import { RemoteNode } from "./RemoteNode";
enum NodeTypes {
Local = "local",
Remote = "remote",
}
export function Settings() {
const { classes } = useStyles();
const { data: isLocalNodeSelected, isSuccess } = useIsLocalNodeSelected();
return (
<AccountLayout>
<Stack className={classes.content} spacing="sm">
<Heading stringId={LangKeys.AccountNodeSettingsTitle} order={3}>
Your node settings
</Heading>
<BodyText
stringId={LangKeys.AccountNodeSettingsDesc}
size="md"
className={classes.paragraph}
>
Using a local node is recommended, but does require loading the entire
blockchain. Choose remote node if you prefer a faster but less
secure experience.
</BodyText>
{isSuccess && (
<NodeConnectSwitch
initialTab={
isLocalNodeSelected ? NodeTypes.Local : NodeTypes.Remote
}
className={classes.connectSwitch}
>
<NodeConnectSwitch.Method
current={isLocalNodeSelected}
tabKey={NodeTypes.Local}
label={
<FormattedMessage
id={LangKeys.AccountNodeSettingsLocal}
defaultMessage="Local Node"
/>
}
icon={<ServerIcon width={32} height={62} />}
>
<LocalNode />
</NodeConnectSwitch.Method>
<NodeConnectSwitch.Method
current={!isLocalNodeSelected}
tabKey={NodeTypes.Remote}
label={
<FormattedMessage
id={LangKeys.AccountNodeSettingsRemote}
defaultMessage="Remote Node"
/>
}
icon={<CloudIcon height={54} width={58} />}
>
<RemoteNode />
</NodeConnectSwitch.Method>
</NodeConnectSwitch>
)}
</Stack>
</AccountLayout>
);
}
const useStyles = createStyles((theme) => ({
connectSwitch: {
marginBottom: "2rem",
},
content: {
maxWidth: theme.other.contentWidthMd,
},
paragraph: {
marginBottom: theme.spacing.xl,
},
}));

View File

@ -15,7 +15,7 @@
// ============================================================================= // =============================================================================
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { createStyles } from "@mantine/core"; import { Box, createStyles } from "@mantine/core";
import { showNotification } from "@mantine/notifications"; import { showNotification } from "@mantine/notifications";
import { Button } from "@atoms/Buttons"; import { Button } from "@atoms/Buttons";
import { LangKeys } from "@constants/lang"; import { LangKeys } from "@constants/lang";
@ -24,18 +24,19 @@ import { useStopMoneroNode } from "@hooks/haveno/useStopMoneroNode";
import { useIsMoneroNodeRunning } from "@hooks/haveno/useIsMoneroNodeRunning"; import { useIsMoneroNodeRunning } from "@hooks/haveno/useIsMoneroNodeRunning";
import { useStartMoneroNode } from "@hooks/haveno/useStartMoneroNode"; import { useStartMoneroNode } from "@hooks/haveno/useStartMoneroNode";
export function NodeLocalStopDaemon() { export function StartStopDaemon() {
const { classes } = useStyles(); const { classes } = useStyles();
const intl = useIntl(); const intl = useIntl();
const { mutateAsync: stopMoneroNode } = useStopMoneroNode();
const { data: isMoneroNodeRunning } = useIsMoneroNodeRunning(); const { data: isMoneroNodeRunning } = useIsMoneroNodeRunning();
const { mutateAsync: startMoneroNode } = useStartMoneroNode(); const { mutateAsync: stopMoneroNode, isLoading: isStopping } =
useStopMoneroNode();
const { mutateAsync: startMoneroNode, isLoading: isStarting } =
useStartMoneroNode();
const { isLoading: isNodeSettingsLoading, data: nodeSettings } = const { isLoading: isNodeSettingsLoading, data: nodeSettings } =
useMoneroNodeSettings(); useMoneroNodeSettings();
// handle the stop button click. const handleStop = () => {
const handleStopBtnClick = () => {
stopMoneroNode() stopMoneroNode()
.then(() => { .then(() => {
showNotification({ showNotification({
@ -55,8 +56,8 @@ export function NodeLocalStopDaemon() {
}); });
}); });
}; };
// Handle the start button click.
const handleStartBtnClick = () => { const handleStart = () => {
if (!nodeSettings) { if (!nodeSettings) {
return; return;
} }
@ -81,9 +82,14 @@ export function NodeLocalStopDaemon() {
}; };
return ( return (
<div className={classes.actions}> <Box className={classes.actions}>
{isMoneroNodeRunning ? ( {isMoneroNodeRunning ? (
<Button flavor="neutral" onClick={handleStopBtnClick}> <Button
flavor="neutral"
loading={isStopping}
loaderPosition="right"
onClick={handleStop}
>
<FormattedMessage <FormattedMessage
id={LangKeys.AccountNodeStopDaemon} id={LangKeys.AccountNodeStopDaemon}
defaultMessage="Stop daemon" defaultMessage="Stop daemon"
@ -91,17 +97,18 @@ export function NodeLocalStopDaemon() {
</Button> </Button>
) : ( ) : (
<Button <Button
flavor="neutral" disabled={isNodeSettingsLoading || !nodeSettings || isStarting}
onClick={handleStartBtnClick} loading={isStarting}
disabled={Boolean(isNodeSettingsLoading || !nodeSettings)} loaderPosition="right"
onClick={handleStart}
> >
<FormattedMessage <FormattedMessage
id={LangKeys.AccountNodeStopDaemon} id={LangKeys.AccountNodeStartDaemon}
defaultMessage="Start daemon" defaultMessage="Start daemon"
/> />
</Button> </Button>
)} )}
</div> </Box>
); );
} }

View File

@ -14,20 +14,14 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import * as Joi from "joi"; import Joi from "joi";
import type { LocalSettingsFormValues } from "./_types";
export interface NodeLocalFormValues { export function useLocalSettingsValidation() {
blockchainLocation: string; return Joi.object<LocalSettingsFormValues>({
startupFlags: string;
daemonAddress: string;
port: string;
}
export function useNodeLocalFormValidation() {
return Joi.object<NodeLocalFormValues>({
blockchainLocation: Joi.string().empty("").uri({ relativeOnly: true }), blockchainLocation: Joi.string().empty("").uri({ relativeOnly: true }),
startupFlags: Joi.string().empty(""), startupFlags: Joi.string().empty(""),
daemonAddress: Joi.string().uri({ allowRelative: false }), bootstrapUrl: Joi.string().allow("").uri({ allowRelative: false }),
port: Joi.number().port(), port: Joi.number().allow("").port(),
}); });
} }

View File

@ -0,0 +1,22 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
export interface LocalSettingsFormValues {
blockchainLocation: string;
startupFlags: string;
bootstrapUrl: string;
port: string;
}

View File

@ -15,20 +15,20 @@
// ============================================================================= // =============================================================================
import type { MoneroNodeSettings } from "haveno-ts"; import type { MoneroNodeSettings } from "haveno-ts";
import type { NodeLocalFormValues } from "./_hooks"; import type { LocalSettingsFormValues } from "./_types";
/** /**
* Transformes the settings request values to form. * Transformes the settings request values to form.
* @param {MoneroNodeSettings.AsObject} nodeSettings * @param {MoneroNodeSettings.AsObject} nodeSettings
* @returns {NodeLocalFormValues} * @returns {LocalSettingsFormValues}
*/ */
export function transformSettingsRequestToForm( export function transformSettingsRequestToForm(
nodeSettings: MoneroNodeSettings.AsObject nodeSettings: MoneroNodeSettings.AsObject
): NodeLocalFormValues { ): LocalSettingsFormValues {
return { return {
blockchainLocation: nodeSettings?.blockchainPath || "", blockchainLocation: nodeSettings?.blockchainPath || "",
startupFlags: nodeSettings?.startupFlagsList.join(", ") || "", startupFlags: nodeSettings?.startupFlagsList.join(" ") || "",
daemonAddress: transfromBootstrapUrl(nodeSettings?.bootstrapUrl || ""), bootstrapUrl: transfromBootstrapUrl(nodeSettings?.bootstrapUrl || ""),
port: transformPort(nodeSettings?.bootstrapUrl || ""), port: transformPort(nodeSettings?.bootstrapUrl || ""),
}; };
} }

View File

@ -14,4 +14,4 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
export * from "./NodeSettings"; export * from "./Settings";

View File

@ -16,7 +16,7 @@
import { AccountLayout } from "@templates/AccountLayout"; import { AccountLayout } from "@templates/AccountLayout";
export function AccountWallet() { export function Wallet() {
return ( return (
<AccountLayout> <AccountLayout>
<h1>Account Wallet</h1> <h1>Account Wallet</h1>

View File

@ -16,8 +16,8 @@
export * from "./AddPaymentAccount"; export * from "./AddPaymentAccount";
export * from "./PaymentMethods"; export * from "./PaymentMethods";
export * from "./AccountBackup"; export * from "./Backup";
export * from "./NodeSettings"; export * from "./Settings";
export * from "./AccountPaymentAccounts"; export * from "./PaymentAccounts";
export * from "./AccountSecurity"; export * from "./Security";
export * from "./AccountWallet"; export * from "./Wallet";

View File

@ -105,7 +105,7 @@ describe("pages::Login", () => {
await user.type(screen.getByLabelText("Password"), PASSWORD); await user.type(screen.getByLabelText("Password"), PASSWORD);
fireEvent.submit(screen.getByRole("button", { name: "Login" })); fireEvent.submit(screen.getByRole("button", { name: "Login" }));
expect(navSpy).to.toHaveBeenCalledTimes(1); expect(navSpy).to.toHaveBeenCalledTimes(1);
expect(navSpy).toHaveBeenCalledWith(ROUTES.AccountPaymentAccounts, { expect(navSpy).toHaveBeenCalledWith(ROUTES.PaymentAccounts, {
replace: true, replace: true,
}); });
unmount(); unmount();

View File

@ -23,7 +23,7 @@ import { useLogin } from "@hooks/session/useLogin";
import { CenteredLayout } from "@templates/CenteredLayout"; import { CenteredLayout } from "@templates/CenteredLayout";
import { BodyText, Heading } from "@atoms/Typography"; import { BodyText, Heading } from "@atoms/Typography";
import { Button } from "@atoms/Buttons"; import { Button } from "@atoms/Buttons";
import { TextInput } from "@atoms/TextInput"; import { PasswordInput } from "@atoms/PasswordInput";
import { ROUTES } from "@constants/routes"; import { ROUTES } from "@constants/routes";
import { CONTENT_MAX_WIDTH } from "./_constants"; import { CONTENT_MAX_WIDTH } from "./_constants";
@ -41,7 +41,7 @@ export function Login() {
const handleSubmit = (values: FormValues) => { const handleSubmit = (values: FormValues) => {
login(values, { login(values, {
onSuccess: () => { onSuccess: () => {
navigate(ROUTES.AccountPaymentAccounts, { replace: true }); navigate(ROUTES.PaymentAccounts, { replace: true });
}, },
onError: (err) => { onError: (err) => {
showNotification({ showNotification({
@ -66,11 +66,10 @@ export function Login() {
solely a password. solely a password.
</BodyText> </BodyText>
<Space h="lg" /> <Space h="lg" />
<TextInput <PasswordInput
aria-label="Password" aria-label="Password"
id="password" id="password"
label="Password" label="Password"
type="password"
{...getInputProps("password")} {...getInputProps("password")}
/> />
<Space h="lg" /> <Space h="lg" />

View File

@ -42,26 +42,55 @@ exports[`pages::Login > renders without exploding 1`] = `
class="mantine-63n06h" class="mantine-63n06h"
/> />
<div <div
class="mantine-TextInput-root mantine-14qek68" class="mantine-PasswordInput-root mantine-14qek68"
> >
<label <label
class="mantine-TextInput-label mantine-1bjo575" class="mantine-PasswordInput-label mantine-1bjo575"
for="password" for="password"
id="password-label" id="password-label"
> >
Password Password
</label> </label>
<div <div
class="mantine-TextInput-wrapper mantine-12sbrde" class="mantine-PasswordInput-wrapper mantine-12sbrde"
> >
<input <div
aria-invalid="false" aria-invalid="false"
aria-label="Password" class="mantine-PasswordInput-defaultVariant mantine-PasswordInput-input mantine-PasswordInput-input mantine-nakpsh"
class="mantine-TextInput-defaultVariant mantine-TextInput-input mantine-nk8491" >
id="password" <input
type="password" aria-label="Password"
value="" class="mantine-PasswordInput-innerInput mantine-17c0t6q"
/> id="password"
type="password"
value=""
/>
</div>
<div
class="mantine-o3oqoy mantine-PasswordInput-rightSection"
>
<button
aria-hidden="true"
class="mantine-ActionIcon-hover mantine-ActionIcon-root mantine-PasswordInput-visibilityToggle mantine-910bvd"
tabindex="-1"
type="button"
>
<svg
fill="none"
height="15"
viewBox="0 0 15 15"
width="15"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M7.5 11C4.80285 11 2.52952 9.62184 1.09622 7.50001C2.52952 5.37816 4.80285 4 7.5 4C10.1971 4 12.4705 5.37816 13.9038 7.50001C12.4705 9.62183 10.1971 11 7.5 11ZM7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C1.65639 10.2936 4.30786 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C13.3436 4.70638 10.6921 3 7.5 3ZM7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5C9.5 6.39543 8.60457 5.5 7.5 5.5C6.39543 5.5 5.5 6.39543 5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5Z"
fill="currentColor"
fill-rule="evenodd"
/>
</svg>
</button>
</div>
</div> </div>
</div> </div>
<div <div

View File

@ -14,17 +14,17 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
import { Stack, Container } from "@mantine/core";
import { useNavigate } from "react-router-dom";
import { CenteredLayout } from "@templates/CenteredLayout";
import { ROUTES } from "@constants/routes";
import { CONTENT_MAX_WIDTH } from "./_constants";
import { useCreateAccount } from "@src/hooks/storage/useCreateAccount";
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Stack, Container } from "@mantine/core";
import { CenteredLayout } from "@templates/CenteredLayout";
import { SetPassword } from "@organisms/SetPassword"; import { SetPassword } from "@organisms/SetPassword";
import { SetPrimaryFiat } from "@organisms/SetPrimaryFiat"; import { SetPrimaryFiat } from "@organisms/SetPrimaryFiat";
import { SelectMoneroNode } from "@organisms/SelectMoneroNode"; import { SelectMoneroNode } from "@organisms/SelectMoneroNode";
import { ReadyToUse } from "@molecules/ReadyToUse"; import { ReadyToUse } from "@molecules/ReadyToUse";
import { useCreateAccount } from "@hooks/storage/useCreateAccount";
import { ROUTES } from "@constants/routes";
import { CONTENT_MAX_WIDTH } from "./_constants";
enum Steps { enum Steps {
CreatePassword = "CreatePassword", CreatePassword = "CreatePassword",
@ -50,13 +50,9 @@ export function CreateAccount() {
setStep(Steps.SelectNode); setStep(Steps.SelectNode);
}; };
const handleCreateAccount = (moneroNode: { const handleCreateAccount = () => {
url: string;
password: string;
}) => {
createAccount( createAccount(
{ {
moneroNode: moneroNode.url,
password, password,
primaryFiat: fiat, primaryFiat: fiat,
}, },
@ -94,9 +90,7 @@ export function CreateAccount() {
)} )}
{step === Steps.Completed && ( {step === Steps.Completed && (
<ReadyToUse <ReadyToUse onSubmit={() => navigate(ROUTES.PaymentAccounts)} />
onSubmit={() => navigate(ROUTES.AccountPaymentAccounts)}
/>
)} )}
</Container> </Container>
</Stack> </Stack>