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) => {
store.set(StorageKeys.Preferences_MoneroNode, value);
// set or clear remote node url; empty indicates local node
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(
IpcChannels.GetPreferences,
async (): Promise<IPreferences> => {
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",
SetPrimaryFiat = "store:accountinfo.primaryFiat",
GetPreferences = "store:preferences",
SetMoneroNode = "store:preferences.moneroNode",
SetMoneroNode = "store:preferences.setMoneroNode",
VerifyAuthToken = "verifyAuthToken",
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,19 +17,23 @@
import { describe, expect, it } from "vitest";
import { render } from "@testing-library/react";
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", () => {
const { asFragment } = render(
<AppProviders>
<NodeStatus
<MoneroNodeListItem
isSelected={true}
title="node.moneroworldcom:18089:active"
status={NodeStatusType.Active}
status={NodeStatus.Active}
onClick={() => console.log("clicked")}
/>
<NodeStatus
<MoneroNodeListItem
isSelected={false}
title="node.moneroworldcom:18089:inactive"
status={NodeStatusType.Inactive}
status={NodeStatus.Inactive}
onClick={() => console.log("clicked")}
/>
</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
exports[`atoms::NodeStatus > renders without exploding 1`] = `
exports[`atoms::MoneroNodeListItem > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-1vax8o0"
<button
class="mantine-UnstyledButton-root mantine-pk5m2t"
type="button"
>
<div
class="mantine-Text-root mantine-14byb36"
@ -17,9 +18,10 @@ exports[`atoms::NodeStatus > renders without exploding 1`] = `
class="mantine-zy87za"
/>
</div>
</div>
<div
class="mantine-1vax8o0"
</button>
<button
class="mantine-UnstyledButton-root mantine-1v48mh9"
type="button"
>
<div
class="mantine-Text-root mantine-14byb36"
@ -33,6 +35,6 @@ exports[`atoms::NodeStatus > renders without exploding 1`] = `
class="mantine-19v6ci5"
/>
</div>
</div>
</button>
</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>(() => {
return {
body: {
marginTop: "2.5rem",
},
root: {},
tabsListWrapper: {
display: "flex",
},
body: {
marginTop: "2.5rem",
},
};
});
@ -38,19 +38,21 @@ export const useControlStyles = createStyles<
tabControl: {
backgroundColor:
theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.white,
color:
theme.colorScheme === "dark"
? theme.colors.dark[0]
: theme.colors.gray[9],
border: `2px solid ${
theme.colorScheme === "dark"
? theme.colors.dark[6]
: theme.colors.gray[2]
}`,
fontSize: theme.fontSizes.md,
padding: `${theme.spacing.lg}px ${theme.spacing.xl}px`,
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",
padding: `${theme.spacing.lg}px ${theme.spacing.xl}px`,
position: "relative",
width: "13.85rem",
"&:not(:first-of-type)": {
@ -59,13 +61,11 @@ export const useControlStyles = createStyles<
[`&.${tabActive.ref}`]: {
color: theme.colorScheme === "dark" ? theme.black : theme.white,
},
cursor: "pointer",
position: "relative",
},
tabIcon: {
display: "flex",
fill: "currentColor",
minHeight: "3.8rem",
display: "flex",
svg: {
margin: "auto",
@ -85,18 +85,18 @@ export const useControlStyles = createStyles<
color: theme.white,
},
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
? theme.fn.rgba(theme.white, 0.15)
: theme.fn.rgba(theme.colors.blue[5], 0.15),
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({
className,
onTabChange,
active,
children,
className,
initialTab,
onTabChange,
}: NodeConnectSwitchProps) {
const { classes, cx } = useTabsStyles();

View File

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

View File

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

View File

@ -34,35 +34,35 @@ export const useGetAccountSidebarMenu = () => {
id: LangKeys.AccountSidebarPaymentAccounts,
defaultMessage: "Payment Accounts",
}),
route: ROUTES.AccountPaymentAccounts,
route: ROUTES.PaymentAccounts,
},
{
label: intl.formatMessage({
id: LangKeys.AccountSidebarNodeSettings,
defaultMessage: "Node Settings",
}),
route: ROUTES.AccountNodeSettings,
route: ROUTES.NodeSettings,
},
{
label: intl.formatMessage({
id: LangKeys.AccountSidebarSecurity,
defaultMessage: "Security",
}),
route: ROUTES.AccountSecurity,
route: ROUTES.Security,
},
{
label: intl.formatMessage({
id: LangKeys.AccountSidebarWallet,
defaultMessage: "Wallet",
}),
route: ROUTES.AccountWallet,
route: ROUTES.Wallet,
},
{
label: intl.formatMessage({
id: LangKeys.AccountSidebarBackup,
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.
// =============================================================================
export * from "./NodeStatus";
export * from "./AddNode";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,8 +14,8 @@
// limitations under the License.
// =============================================================================
import { QueryKeys } from "@constants/query-keys";
import { useQuery } from "react-query";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient";
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.
// =============================================================================
import { HavenoClient } from "haveno-ts";
import { useRef } from "react";
import { HavenoClient } from "haveno-ts";
import {
HAVENO_DAEMON_PASSWORD,
HAVENO_DAEMON_URL,

View File

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

View File

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

View File

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

View File

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

View File

@ -14,9 +14,9 @@
// limitations under the License.
// =============================================================================
import { QueryKeys } from "@constants/query-keys";
import type { PaymentAccount } from "haveno-ts";
import { useQuery } from "react-query";
import type { PaymentAccount } from "haveno-ts";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient";
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.
// =============================================================================
import { useQuery } from "react-query";
import type { MarketPriceInfo } from "haveno-ts";
import { QueryKeys } from "@constants/query-keys";
import { useQuery } from "react-query";
import { useHavenoClient } from "./useHavenoClient";
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.
// =============================================================================
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";
interface Variables {
connection: string;
uri: string;
}
export function useSetMoneroConnection() {
const queryClient = useQueryClient();
const { mutateAsync: saveRemoteNode } = useSaveRemoteNode();
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.
// =============================================================================
import { QueryKeys } from "@constants/query-keys";
import type { MoneroNodeSettings } from "haveno-ts";
import { useMutation, useQueryClient } from "react-query";
import type { MoneroNodeSettings } from "haveno-ts";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient";
export function useStartMoneroNode() {
const client = useHavenoClient();
const queryClient = useQueryClient();
const client = useHavenoClient();
return useMutation<void, Error, MoneroNodeSettings>(
(data: MoneroNodeSettings) => client.startMoneroNode(data),
async (data: MoneroNodeSettings) => client.startMoneroNode(data),
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.MoneroNodeIsRunning);

View File

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

View File

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

View File

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

View File

@ -20,7 +20,6 @@ import { useMutation, useQueryClient } from "react-query";
interface Variables {
password: string;
primaryFiat: string;
moneroNode: string;
}
export function useCreateAccount() {
@ -31,7 +30,6 @@ export function useCreateAccount() {
await Promise.all([
window.electronStore.setPassword({ newPassword: variables.password }),
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";
export function AccountBackup() {
export function Backup() {
return (
<AccountLayout>
<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 { AccountLayout } from "@templates/AccountLayout";
export function AccountPaymentAccounts() {
export function PaymentAccounts() {
const navigate = useNavigate();
return (
<AccountLayout>
<PaymentMethodList
onAdd={() => navigate(ROUTES.AccountAddPaymentAccount)}
/>
<PaymentMethodList onAdd={() => navigate(ROUTES.AddPaymentAccount)} />
</AccountLayout>
);
}

View File

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

View File

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

View File

@ -14,70 +14,82 @@
// limitations under the License.
// =============================================================================
import { Box, Stack, Grid, Group } from "@mantine/core";
import { joiResolver, useForm } from "@mantine/form";
import { useEffect } from "react";
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 { Button } from "@atoms/Buttons";
import { LangKeys } from "@constants/lang";
import { TextInput } from "@atoms/TextInput";
import { useMoneroNodeSettings } from "@hooks/haveno/useMoneroNodeSettings";
import { useSetMoneroNodeSettings } from "@hooks/haveno/useSetMoneroNodeSettings";
import { NodeLocalStopDaemon } from "./NodeLocalStopDaemon";
import type { NodeLocalFormValues } from "./_hooks";
import { useNodeLocalFormValidation } from "./_hooks";
import { useSaveLocalMoneroNode } from "@hooks/haveno/useSaveLocalMoneroNode";
import { LangKeys } from "@constants/lang";
import { StartStopDaemon } from "./StartStopDaemon";
import type { LocalSettingsFormValues } from "./_types";
import { useLocalSettingsValidation } from "./_hooks";
import { transformSettingsRequestToForm } from "./_utils";
export function NodeLocalForm() {
export function LocalNode() {
const { data: nodeSettings } = useMoneroNodeSettings();
const { mutateAsync: updateNodeSettings } = useSetMoneroNodeSettings();
const { mutate: saveLocalNode, isLoading: isSaving } =
useSaveLocalMoneroNode();
const intl = useIntl();
const validation = useNodeLocalFormValidation();
const validation = useLocalSettingsValidation();
const form = useForm<NodeLocalFormValues>({
const { getInputProps, onSubmit, setValues } =
useForm<LocalSettingsFormValues>({
initialValues: {
blockchainLocation: "",
startupFlags: "",
daemonAddress: "",
bootstrapUrl: "",
port: "",
...(nodeSettings
? transformSettingsRequestToForm(nodeSettings.toObject())
: {}),
},
validate: joiResolver(validation),
});
const handleFormSubmit = (values: NodeLocalFormValues) => {
updateNodeSettings({
const handleSubmit = (values: LocalSettingsFormValues) => {
saveLocalNode(
{
blockchainPath: values.blockchainLocation,
startupFlags: values.startupFlags.split(", "),
bootstrapUrl: `${values.daemonAddress}:${values.port}`,
})
.then(() => {
startupFlags: values.startupFlags.split(/\s|=/),
bootstrapUrl: values.bootstrapUrl
? (new URL(values.bootstrapUrl).port = values.port)
: "",
},
{
onSuccess: () => {
showNotification({
color: "green",
message: intl.formatMessage({
id: LangKeys.AccountNodeLocalSaveNotification,
defaultMessage: "Local node settings updated successfully",
defaultMessage: "Local node settings saved successfully",
}),
});
})
.catch((err) => {
},
onError: (err: Error) => {
console.dir(err);
showNotification({
color: "red",
message: err.message,
title: "Something went wrong",
});
});
},
}
);
};
return (
<Box>
<NodeLocalStopDaemon />
useEffect(() => {
if (nodeSettings) {
setValues(transformSettingsRequestToForm(nodeSettings.toObject()));
}
}, [nodeSettings]);
<form onSubmit={form.onSubmit(handleFormSubmit)}>
return (
<>
<StartStopDaemon />
<form onSubmit={onSubmit(handleSubmit)}>
<Stack spacing="lg">
<TextInput
id="blockchainLocation"
@ -87,7 +99,7 @@ export function NodeLocalForm() {
defaultMessage="Blockchain location"
/>
}
{...form.getInputProps("blockchainLocation")}
{...getInputProps("blockchainLocation")}
/>
<TextInput
id="startupFlags"
@ -97,20 +109,19 @@ export function NodeLocalForm() {
defaultMessage="Daemon startup flags"
/>
}
{...form.getInputProps("startupFlags")}
{...getInputProps("startupFlags")}
/>
<Grid>
<Grid.Col span={9}>
<TextInput
id="daemonAddress"
id="bootstrapUrl"
label={
<FormattedMessage
id={LangKeys.AccountNodeFieldDaemonAddress}
defaultMessage="Daemon Address"
id={LangKeys.AccountNodeFieldBootstrapUrl}
defaultMessage="Bootstrap URL"
/>
}
required
{...form.getInputProps("daemonAddress")}
{...getInputProps("bootstrapUrl")}
/>
</Grid.Col>
<Grid.Col span={3}>
@ -122,19 +133,23 @@ export function NodeLocalForm() {
defaultMessage="Port"
/>
}
required
{...form.getInputProps("port")}
{...getInputProps("port")}
/>
</Grid.Col>
</Grid>
<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" />
</Button>
</Group>
</Stack>
</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 { createStyles } from "@mantine/core";
import { Box, createStyles } from "@mantine/core";
import { showNotification } from "@mantine/notifications";
import { Button } from "@atoms/Buttons";
import { LangKeys } from "@constants/lang";
@ -24,18 +24,19 @@ import { useStopMoneroNode } from "@hooks/haveno/useStopMoneroNode";
import { useIsMoneroNodeRunning } from "@hooks/haveno/useIsMoneroNodeRunning";
import { useStartMoneroNode } from "@hooks/haveno/useStartMoneroNode";
export function NodeLocalStopDaemon() {
export function StartStopDaemon() {
const { classes } = useStyles();
const intl = useIntl();
const { mutateAsync: stopMoneroNode } = useStopMoneroNode();
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 } =
useMoneroNodeSettings();
// handle the stop button click.
const handleStopBtnClick = () => {
const handleStop = () => {
stopMoneroNode()
.then(() => {
showNotification({
@ -55,8 +56,8 @@ export function NodeLocalStopDaemon() {
});
});
};
// Handle the start button click.
const handleStartBtnClick = () => {
const handleStart = () => {
if (!nodeSettings) {
return;
}
@ -81,9 +82,14 @@ export function NodeLocalStopDaemon() {
};
return (
<div className={classes.actions}>
<Box className={classes.actions}>
{isMoneroNodeRunning ? (
<Button flavor="neutral" onClick={handleStopBtnClick}>
<Button
flavor="neutral"
loading={isStopping}
loaderPosition="right"
onClick={handleStop}
>
<FormattedMessage
id={LangKeys.AccountNodeStopDaemon}
defaultMessage="Stop daemon"
@ -91,17 +97,18 @@ export function NodeLocalStopDaemon() {
</Button>
) : (
<Button
flavor="neutral"
onClick={handleStartBtnClick}
disabled={Boolean(isNodeSettingsLoading || !nodeSettings)}
disabled={isNodeSettingsLoading || !nodeSettings || isStarting}
loading={isStarting}
loaderPosition="right"
onClick={handleStart}
>
<FormattedMessage
id={LangKeys.AccountNodeStopDaemon}
id={LangKeys.AccountNodeStartDaemon}
defaultMessage="Start daemon"
/>
</Button>
)}
</div>
</Box>
);
}

View File

@ -14,20 +14,14 @@
// limitations under the License.
// =============================================================================
import * as Joi from "joi";
import Joi from "joi";
import type { LocalSettingsFormValues } from "./_types";
export interface NodeLocalFormValues {
blockchainLocation: string;
startupFlags: string;
daemonAddress: string;
port: string;
}
export function useNodeLocalFormValidation() {
return Joi.object<NodeLocalFormValues>({
export function useLocalSettingsValidation() {
return Joi.object<LocalSettingsFormValues>({
blockchainLocation: Joi.string().empty("").uri({ relativeOnly: true }),
startupFlags: Joi.string().empty(""),
daemonAddress: Joi.string().uri({ allowRelative: false }),
port: Joi.number().port(),
bootstrapUrl: Joi.string().allow("").uri({ allowRelative: false }),
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 { NodeLocalFormValues } from "./_hooks";
import type { LocalSettingsFormValues } from "./_types";
/**
* Transformes the settings request values to form.
* @param {MoneroNodeSettings.AsObject} nodeSettings
* @returns {NodeLocalFormValues}
* @returns {LocalSettingsFormValues}
*/
export function transformSettingsRequestToForm(
nodeSettings: MoneroNodeSettings.AsObject
): NodeLocalFormValues {
): LocalSettingsFormValues {
return {
blockchainLocation: nodeSettings?.blockchainPath || "",
startupFlags: nodeSettings?.startupFlagsList.join(", ") || "",
daemonAddress: transfromBootstrapUrl(nodeSettings?.bootstrapUrl || ""),
startupFlags: nodeSettings?.startupFlagsList.join(" ") || "",
bootstrapUrl: transfromBootstrapUrl(nodeSettings?.bootstrapUrl || ""),
port: transformPort(nodeSettings?.bootstrapUrl || ""),
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -42,27 +42,56 @@ exports[`pages::Login > renders without exploding 1`] = `
class="mantine-63n06h"
/>
<div
class="mantine-TextInput-root mantine-14qek68"
class="mantine-PasswordInput-root mantine-14qek68"
>
<label
class="mantine-TextInput-label mantine-1bjo575"
class="mantine-PasswordInput-label mantine-1bjo575"
for="password"
id="password-label"
>
Password
</label>
<div
class="mantine-TextInput-wrapper mantine-12sbrde"
class="mantine-PasswordInput-wrapper mantine-12sbrde"
>
<div
aria-invalid="false"
class="mantine-PasswordInput-defaultVariant mantine-PasswordInput-input mantine-PasswordInput-input mantine-nakpsh"
>
<input
aria-invalid="false"
aria-label="Password"
class="mantine-TextInput-defaultVariant mantine-TextInput-input mantine-nk8491"
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
class="mantine-63n06h"

View File

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