mirror of
https://github.com/haveno-dex/haveno-ui.git
synced 2024-10-01 07:35:39 -04:00
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:
parent
8eb4694ca2
commit
250d742d48
@ -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),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -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",
|
||||
}
|
||||
|
@ -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",
|
||||
},
|
||||
};
|
||||
|
@ -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),
|
||||
|
@ -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 />
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
);
|
@ -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>
|
||||
);
|
@ -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",
|
||||
},
|
||||
};
|
||||
});
|
@ -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>
|
||||
`;
|
@ -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";
|
@ -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",
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
@ -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",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -42,11 +42,11 @@ interface NodeConnectSwitchProps {
|
||||
}
|
||||
|
||||
export function NodeConnectSwitch({
|
||||
className,
|
||||
onTabChange,
|
||||
active,
|
||||
children,
|
||||
className,
|
||||
initialTab,
|
||||
onTabChange,
|
||||
}: NodeConnectSwitchProps) {
|
||||
const { classes, cx } = useTabsStyles();
|
||||
|
||||
|
@ -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"
|
||||
|
@ -31,7 +31,7 @@ import {
|
||||
getPaymentAccountLogo,
|
||||
getPaymentAccountName,
|
||||
getPaymentAccountNumber,
|
||||
} from "@utils/payment-account";
|
||||
} from "@src/utils/paymentAccount";
|
||||
|
||||
interface PaymentMethodCardProps {
|
||||
data: PaymentAccount;
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
[]
|
||||
|
@ -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,
|
||||
};
|
@ -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();
|
||||
});
|
||||
});
|
100
packages/renderer/src/components/organisms/AddNode/AddNode.tsx
Normal file
100
packages/renderer/src/components/organisms/AddNode/AddNode.tsx
Normal 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(),
|
||||
});
|
@ -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>
|
||||
`;
|
@ -14,4 +14,4 @@
|
||||
// limitations under the License.
|
||||
// =============================================================================
|
||||
|
||||
export * from "./NodeStatus";
|
||||
export * from "./AddNode";
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
};
|
||||
|
@ -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);
|
||||
},
|
||||
}
|
||||
);
|
@ -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() {
|
||||
|
26
packages/renderer/src/hooks/haveno/useGetMoneroConnection.ts
Normal file
26
packages/renderer/src/hooks/haveno/useGetMoneroConnection.ts
Normal 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()
|
||||
);
|
||||
}
|
@ -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,
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
);
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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))
|
||||
);
|
||||
});
|
||||
}
|
@ -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() {
|
||||
|
61
packages/renderer/src/hooks/haveno/useSaveLocalMoneroNode.ts
Normal file
61
packages/renderer/src/hooks/haveno/useSaveLocalMoneroNode.ts
Normal 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);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
@ -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);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
]);
|
||||
},
|
||||
{
|
||||
|
@ -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 };
|
||||
}
|
37
packages/renderer/src/hooks/storage/useSaveRemoteNode.ts
Normal file
37
packages/renderer/src/hooks/storage/useSaveRemoteNode.ts
Normal 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);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
|
||||
import { AccountLayout } from "@templates/AccountLayout";
|
||||
|
||||
export function AccountBackup() {
|
||||
export function Backup() {
|
||||
return (
|
||||
<AccountLayout>
|
||||
<h1>Account Backup</h1>
|
@ -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",
|
||||
},
|
||||
}));
|
@ -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,
|
||||
},
|
||||
}));
|
@ -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}</>
|
||||
);
|
||||
}
|
@ -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",
|
||||
},
|
||||
}));
|
@ -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>
|
||||
);
|
||||
}
|
@ -24,9 +24,7 @@ export function PaymentMethods() {
|
||||
|
||||
return (
|
||||
<NavbarLayout>
|
||||
<PaymentMethodList
|
||||
onAdd={() => navigate(ROUTES.AccountAddPaymentAccount)}
|
||||
/>
|
||||
<PaymentMethodList onAdd={() => navigate(ROUTES.AddPaymentAccount)} />
|
||||
</NavbarLayout>
|
||||
);
|
||||
}
|
||||
|
@ -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 (
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
144
packages/renderer/src/pages/Account/Settings/RemoteNode.tsx
Normal file
144
packages/renderer/src/pages/Account/Settings/RemoteNode.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
104
packages/renderer/src/pages/Account/Settings/Settings.tsx
Normal file
104
packages/renderer/src/pages/Account/Settings/Settings.tsx
Normal 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,
|
||||
},
|
||||
}));
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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(),
|
||||
});
|
||||
}
|
22
packages/renderer/src/pages/Account/Settings/_types.ts
Normal file
22
packages/renderer/src/pages/Account/Settings/_types.ts
Normal 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;
|
||||
}
|
@ -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 || ""),
|
||||
};
|
||||
}
|
@ -14,4 +14,4 @@
|
||||
// limitations under the License.
|
||||
// =============================================================================
|
||||
|
||||
export * from "./NodeSettings";
|
||||
export * from "./Settings";
|
@ -16,7 +16,7 @@
|
||||
|
||||
import { AccountLayout } from "@templates/AccountLayout";
|
||||
|
||||
export function AccountWallet() {
|
||||
export function Wallet() {
|
||||
return (
|
||||
<AccountLayout>
|
||||
<h1>Account Wallet</h1>
|
@ -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";
|
||||
|
@ -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();
|
||||
|
@ -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" />
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user