feat: haveno daemon integration

- create account
- login
- change password
- haveno hooks
- electron-store hooks
- haveno-ts

---

Authored-by: schowdhuri
Reviewed-by: localredhead
This commit is contained in:
Subir 2022-05-10 01:36:01 +05:30
parent 7bcf36d595
commit a0c7875391
No known key found for this signature in database
GPG Key ID: 2D633D8047FD3FF0
109 changed files with 2276 additions and 573 deletions

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
VITE_HAVENO_URL=http://127.0.0.1:8080
VITE_HAVENO_PASSWORD=daemon-password

2
.gitignore vendored
View File

@ -54,3 +54,5 @@ thumbs.db
# Editor-based Rest Client
.idea/httpRequests
/.idea/csv-plugin.xml
.env

View File

@ -30,7 +30,7 @@ export default {
return Array.from(
filenames.reduce((set, filename) => {
const pack = filename.replace(pathToPackages, "").split(sep)[0];
set.add(`yarn typecheck:${pack} --if-present`);
set.add(`npm run typecheck:${pack} --if-present`);
return set;
}, new Set())
);

25
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,25 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "TS Watch",
"type": "process",
"command": "./.vscode/tscwatch.sh",
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "never",
"echo": false,
"focus": false,
"panel": "shared"
},
"problemMatcher": "$tsc-watch",
"runOptions": {
"runOn": "folderOpen"
}
}
]
}

8
.vscode/tscwatch.sh vendored Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
./node_modules/.bin/tsc --watch --noEmit --project packages/main &
P1=$!
./node_modules/.bin/tsc --watch --noEmit --project packages/preload &
P2=$!
./node_modules/.bin/tsc --watch --noEmit --project packages/renderer &
P3=$!
wait $P1 $P2 $P3

View File

@ -46,6 +46,7 @@
"@storybook/react": "^6.4.22",
"@storybook/testing-library": "^0.0.10",
"@testing-library/react": "^12",
"@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.182",
"@types/react": "<18.0.0",
"@types/react-dom": "<18.0.0",
@ -54,6 +55,7 @@
"@vitejs/plugin-react": "^1.3.0",
"babel-loader": "^8.2.5",
"cross-env": "7.0.3",
"dotenv": "^16.0.0",
"electron": "17.1.0",
"electron-builder": "22.14.13",
"electron-devtools-installer": "3.2.0",
@ -82,7 +84,9 @@
"dayjs": "^1.11.0",
"electron-store": "^8.0.1",
"electron-updater": "4.6.5",
"haveno-ts": "0.0.2",
"joi": "^17.6.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",
"react": "<18.0.0",
"react-dom": "<18.0.0",

View File

@ -17,7 +17,7 @@
import { app } from "electron";
import "./security-restrictions";
import { restoreOrCreateWindow } from "@src/mainWindow";
import { registerStoreHandlers } from "./services/store";
import { registerStoreHandlers } from "@src/services/store";
/**
* Prevent multiple instances

View File

@ -17,44 +17,116 @@
import { ipcMain, safeStorage } from "electron";
import Store from "electron-store";
import type {
AccountInfoDto,
ChangePasswordInput,
IPreferences,
IStoreSchema,
IUserInfo,
UserInfoInputType,
IUserPermission,
SetPasswordInput,
} from "@src/types";
import { StoreKeys, StoreSchema } from "@src/types";
import { StorageKeys } from "@src/types";
import { IpcChannels, StoreSchema } from "@src/types";
import {
createAuthToken,
hashPassword,
verifyAuthAuthToken,
verifyPassword,
} from "@src/utils/password";
const store = new Store<IStoreSchema>({ schema: StoreSchema });
export function registerStoreHandlers() {
ipcMain.handle("store:userinfo", async (_, payload?: UserInfoInputType) => {
const prevData = store.get(StoreKeys.UserInfo);
// retrieve encrypted data like so:
// safeStorage.decryptString(Buffer.from(prevData.password));
if (!payload) {
return prevData;
ipcMain.handle(IpcChannels.SetPassword, async (_, data: SetPasswordInput) => {
const encryptedPassword = store.get(StorageKeys.AccountInfo_Password);
if (encryptedPassword) {
throw new Error("[[Can't set password]]");
}
const userInfo: IUserInfo = {
...payload,
// encrypt sensitive data before storage
password: safeStorage.encryptString(payload.password),
};
store.set(StoreKeys.UserInfo, {
...(prevData ?? {}),
...userInfo,
});
return store.get(StoreKeys.UserInfo);
const hash = await hashPassword(data.newPassword);
store.set(
StorageKeys.AccountInfo_Password,
safeStorage.encryptString(hash)
);
});
ipcMain.handle(
"store:permissions",
async (_, permissions?: Array<IUserPermission>) => {
const prevData = store.get(StoreKeys.Permissions);
if (!permissions) {
return prevData;
IpcChannels.ChangePassword,
async (_, data: ChangePasswordInput): Promise<string> => {
const encryptedPassword = store.get(StorageKeys.AccountInfo_Password);
if (!encryptedPassword) {
throw new Error("[[No password currently set]]");
}
store.set(StoreKeys.Permissions, [...(prevData || []), ...permissions]);
return store.get(StoreKeys.Permissions);
// verify old password
const oldPassHash = safeStorage.decryptString(
Buffer.from(encryptedPassword)
);
if (!("currentPassword" in data)) {
throw new Error("[[Current password required]]");
}
if (!(await verifyPassword(data.currentPassword, oldPassHash))) {
throw new Error("[[Current password doesn't match]]");
}
const hash = await hashPassword(data.newPassword);
store.set(
StorageKeys.AccountInfo_Password,
safeStorage.encryptString(hash)
);
// generate and return a new authToken
return createAuthToken(hash);
}
);
ipcMain.handle(IpcChannels.SetPrimaryFiat, (_, value: string) => {
store.set(StorageKeys.AccountInfo_PrimaryFiat, value);
});
ipcMain.handle(IpcChannels.GetAccountInfo, (): AccountInfoDto | null => {
const encryptedPassword = store.get(StorageKeys.AccountInfo_Password);
if (!encryptedPassword) {
return null;
}
return {
passwordHash: safeStorage.decryptString(Buffer.from(encryptedPassword)),
primaryFiat: store.get(StorageKeys.AccountInfo_PrimaryFiat),
};
});
// returns null if password is incorrect. returns jwt if password is correct
ipcMain.handle(
IpcChannels.VerifyPassword,
async (_, plainText: string): Promise<string | null> => {
const encryptedPassword = store.get(StorageKeys.AccountInfo_Password);
if (!encryptedPassword) {
return null;
}
const hash = safeStorage.decryptString(Buffer.from(encryptedPassword));
if (!(await verifyPassword(plainText, hash))) {
return null;
}
return createAuthToken(hash);
}
);
ipcMain.handle(
IpcChannels.VerifyAuthToken,
async (_, token: string): Promise<boolean> => {
const encryptedPassword = store.get(StorageKeys.AccountInfo_Password);
if (!encryptedPassword) {
return false;
}
const hash = safeStorage.decryptString(Buffer.from(encryptedPassword));
return verifyAuthAuthToken(token, hash);
}
);
ipcMain.handle(IpcChannels.SetMoneroNode, async (_, value: string) => {
store.set(StorageKeys.Preferences_MoneroNode, value);
});
ipcMain.handle(
IpcChannels.GetPreferences,
async (): Promise<IPreferences> => {
return {
moneroNode: store.get(StorageKeys.Preferences_MoneroNode),
};
}
);
}

View File

@ -14,4 +14,5 @@
// limitations under the License.
// =============================================================================
export * from "./ipc";
export * from "./store";

View File

@ -0,0 +1,27 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
export enum IpcChannels {
GetAccountInfo = "store:accountInfo",
SetPassword = "store:accountinfo.setPassword",
ChangePassword = "store:accountinfo.changePassword",
VerifyPassword = "store:accountinfo.verifyPassword",
SetPrimaryFiat = "store:accountinfo.primaryFiat",
GetPreferences = "store:preferences",
SetMoneroNode = "store:preferences.moneroNode",
VerifyAuthToken = "verifyAuthToken",
}

View File

@ -16,49 +16,51 @@
import type { Schema } from "electron-store";
export enum StoreKeys {
UserInfo = "UserInfo",
Permissions = "Permissions",
export enum StorageKeys {
AccountInfo_Password = "accounInfo.password",
AccountInfo_PrimaryFiat = "accounInfo.primaryFiat",
Preferences_MoneroNode = "preferences.moneroNode",
}
// TS types for StoreSchema
export interface IStoreSchema {
[StoreKeys.UserInfo]: IUserInfo;
[StoreKeys.Permissions]: Array<IUserPermission>;
[StorageKeys.AccountInfo_Password]: IAccountInfo["password"];
[StorageKeys.AccountInfo_PrimaryFiat]: IAccountInfo["primaryFiat"];
[StorageKeys.Preferences_MoneroNode]: IPreferences["moneroNode"]; // TODO: change to object {url, password}
}
export interface IUserInfo {
username: string;
export interface IAccountInfo {
password: Buffer;
primaryFiat: string;
}
export type UserInfoInputType = Omit<IUserInfo, "password"> & {
password: string;
};
export interface AccountInfoDto extends Omit<IAccountInfo, "password"> {
passwordHash: string;
}
export interface IUserPermission {
name: string;
export interface IPreferences {
moneroNode: string;
}
// this schema is used by electron-store
// must mirror IStoreSchema
export const StoreSchema: Schema<IStoreSchema> = {
[StoreKeys.UserInfo]: {
type: "object",
required: [],
properties: {
username: { type: "string" },
},
[StorageKeys.AccountInfo_Password]: {
type: "string",
},
[StoreKeys.Permissions]: {
type: "array",
default: [],
items: {
type: "object",
required: [],
properties: {
name: { type: "string" },
},
},
[StorageKeys.AccountInfo_PrimaryFiat]: {
type: "string",
},
[StorageKeys.Preferences_MoneroNode]: {
type: "string",
},
};
export interface SetPasswordInput {
newPassword: string;
}
export interface ChangePasswordInput {
currentPassword: string;
newPassword: string;
}

View File

@ -0,0 +1,84 @@
// =============================================================================
// 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 { randomBytes, scrypt, timingSafeEqual } from "crypto";
import jwt from "jsonwebtoken";
const SALT_LENGTH = 32;
const KEY_LENGTH = 64;
export async function hashPassword(plainText: string): Promise<string> {
const salt = randomBytes(SALT_LENGTH).toString("hex");
return new Promise((resolve, reject) => {
scrypt(plainText, salt, KEY_LENGTH, (err, derivedKey) => {
if (err) {
return reject(err);
}
resolve(`${salt}:${derivedKey.toString("hex")}`);
});
});
}
export async function verifyPassword(
plainText: string,
storedHash: string
): Promise<boolean> {
const [salt, key] = storedHash.split(":");
return new Promise((resolve, reject) => {
const origKey = Buffer.from(key, "hex");
scrypt(plainText, salt, KEY_LENGTH, (err, derivedKey) => {
if (err) {
return reject(err);
}
resolve(timingSafeEqual(origKey, derivedKey));
});
});
}
interface AuthToken {
createdAt: number;
randomStr: string;
}
export async function createAuthToken(
saltAndPassword: string
): Promise<string> {
const [randomStr, secret] = saltAndPassword.split(":");
const token = jwt.sign(
{
createdAt: new Date().getTime(),
randomStr,
},
secret
);
return token;
}
export async function verifyAuthAuthToken(
token: string,
saltAndPassword: string
): Promise<boolean> {
if (!token) {
return false;
}
const [randomStr, secret] = saltAndPassword.split(":");
try {
const payload = jwt.verify(token, secret) as AuthToken;
return payload.randomStr === randomStr;
} catch (_ex) {
return false;
}
}

View File

@ -16,12 +16,41 @@
import { ipcRenderer } from "electron";
import { exposeInMainWorld } from "./exposeInMainWorld";
import type { UserInfoInputType } from "./types";
import type {
AccountInfoDto,
ChangePasswordInput,
IPreferences,
SetPasswordInput,
} from "./types";
import { IpcChannels } from "./types";
// Export for types in contracts.d.ts
export const store = {
storeUserinfo: async (data?: UserInfoInputType) =>
ipcRenderer.invoke("store:userinfo", data),
setPassword: async (data: SetPasswordInput): Promise<void> =>
ipcRenderer.invoke(IpcChannels.SetPassword, data),
// returns jwt on success
changePassword: async (data: ChangePasswordInput): Promise<string> =>
ipcRenderer.invoke(IpcChannels.ChangePassword, data),
// returns jwt on success; null on failure
verifyPassword: async (plainText: string): Promise<string | null> =>
ipcRenderer.invoke(IpcChannels.VerifyPassword, plainText),
verifyAuthToken: async (token: string): Promise<boolean> =>
ipcRenderer.invoke(IpcChannels.VerifyAuthToken, token),
setPrimaryFiat: async (value: string): Promise<void> =>
ipcRenderer.invoke(IpcChannels.SetPrimaryFiat, value),
getAccountInfo: async (): Promise<AccountInfoDto> =>
ipcRenderer.invoke(IpcChannels.GetAccountInfo),
setMoneroNode: async (value: string): Promise<void> =>
ipcRenderer.invoke(IpcChannels.SetMoneroNode, value),
getPreferences: async (): Promise<IPreferences> =>
ipcRenderer.invoke(IpcChannels.GetPreferences),
};
exposeInMainWorld("electronStore", store);

View File

@ -14,4 +14,4 @@
// limitations under the License.
// =============================================================================
export * from "./store";
export * from "../../../main/src/types";

View File

@ -1,64 +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 { Schema } from "electron-store";
export enum StoreKeys {
UserInfo = "UserInfo",
Permissions = "Permissions",
}
// TS types for StoreSchema
export interface IStoreSchema {
[StoreKeys.UserInfo]: IUserInfo;
[StoreKeys.Permissions]: Array<IUserPermission>;
}
export interface IUserInfo {
username: string;
password: Buffer;
}
export type UserInfoInputType = Omit<IUserInfo, "password"> & {
password: string;
};
export interface IUserPermission {
name: string;
}
// this schema is used by electron-store
// must mirror IStoreSchema
export const StoreSchema: Schema<IStoreSchema> = {
[StoreKeys.UserInfo]: {
type: "object",
required: [],
properties: {
username: { type: "string" },
},
},
[StoreKeys.Permissions]: {
type: "array",
default: [],
items: {
type: "object",
required: [],
properties: {
name: { type: "string" },
},
},
},
};

View File

@ -19,7 +19,8 @@
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/consistent-type-imports": "error",
"prettier/prettier": "error"
"prettier/prettier": "error",
"react/jsx-curly-brace-presence": "error"
},
"settings": {
"react": {

View File

@ -0,0 +1,3 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M27.5805 17.3866C25.7106 24.8866 18.1143 29.4511 10.6133 27.5808C3.11545 25.7109 -1.44898 18.1141 0.421768 10.6145C2.29077 3.11358 9.88708 -1.45129 17.3858 0.418582C24.8863 2.28846 29.4503 9.88608 27.5805 17.3866Z" fill="#DBDBDB"/>
</svg>

After

Width:  |  Height:  |  Size: 344 B

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' blob:"
content="script-src 'self' 'unsafe-eval' blob:"
/>
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>Haveno</title>

View File

@ -15,44 +15,84 @@
// =============================================================================
import { Routes, Route } from "react-router-dom";
import { Home, Welcome } from "@pages/Onboarding";
import { Wallet } from "@pages/Wallet";
import { AccountPaymentAccounts } from "@pages/Account/AccountPaymentAccounts";
import { AccountNodeSettings } from "@pages/Account/NodeSettings";
import { AccountBackup } from "@pages/Account/AccountBackup";
import { AccountWallet } from "@pages/Account/AccountWallet";
import { AccountSecurity } from "@pages/Account/Security";
import { ROUTES } from "@constants/routes";
import { PaymentMethods } from "@pages/Account";
import { AddPaymentMethod } from "@organisms/AddPaymentMethod";
import { ProtectedRoute } from "@atoms/ProtectedRoute";
import { Home } from "@pages/Home";
import { Login } from "@pages/Login";
import { CreateAccount, Welcome } from "@pages/Onboarding";
import {
AccountBackup,
AccountNodeSettings,
AccountPaymentAccounts,
AccountSecurity,
AccountWallet,
AddPaymentAccount,
PaymentMethods,
} from "@pages/Account";
export function AppRoutes() {
return (
<Routes>
<Route path={ROUTES.Home} element={<Home />} />
<Route path={ROUTES.Login} element={<Login />} />
<Route path={ROUTES.Welcome} element={<Welcome />} />
<Route path={ROUTES.Wallet} element={<Wallet />} />
<Route path={ROUTES.Account}>
<Route
path={ROUTES.AccountPaymentAccounts}
element={<AccountPaymentAccounts />}
/>
<Route
path={ROUTES.AccountNodeSettings}
element={<AccountNodeSettings />}
/>
<Route path={ROUTES.AccountBackup} element={<AccountBackup />} />
<Route path={ROUTES.AccountWallet} element={<AccountWallet />} />
<Route path={ROUTES.AccountSecurity} element={<AccountSecurity />} />
<Route
path={ROUTES.AccountPaymentMethods}
element={<PaymentMethods />}
/>
<Route
path={ROUTES.AccountAddPaymentMethod}
element={<AddPaymentMethod />}
/>
</Route>
<Route path={ROUTES.CreateAccount} element={<CreateAccount />} />
<Route
path={ROUTES.AccountPaymentAccounts}
element={
<ProtectedRoute>
<AccountPaymentAccounts />
</ProtectedRoute>
}
/>
<Route
path={ROUTES.AccountNodeSettings}
element={
<ProtectedRoute>
<AccountNodeSettings />
</ProtectedRoute>
}
/>
<Route
path={ROUTES.AccountBackup}
element={
<ProtectedRoute>
<AccountBackup />
</ProtectedRoute>
}
/>
<Route
path={ROUTES.AccountWallet}
element={
<ProtectedRoute>
<AccountWallet />
</ProtectedRoute>
}
/>
<Route
path={ROUTES.AccountSecurity}
element={
<ProtectedRoute>
<AccountSecurity />
</ProtectedRoute>
}
/>
<Route
path={ROUTES.AccountPaymentAccounts}
element={
<ProtectedRoute>
<PaymentMethods />
</ProtectedRoute>
}
/>
<Route
path={ROUTES.AccountAddPaymentAccount}
element={
<ProtectedRoute>
<AddPaymentAccount />
</ProtectedRoute>
}
/>
</Routes>
);
}

View File

@ -17,6 +17,7 @@
import type { FC } from "react";
import { RecoilRoot } from "recoil";
import { HashRouter } from "react-router-dom";
import { NotificationsProvider } from "@mantine/notifications";
import { QueryClientProvider } from "./QueryClientProvider";
import { IntlProvider } from "./IntlProvider";
import { ThemeProvider } from "./ThemeProvider";
@ -26,7 +27,9 @@ export const AppProviders: FC = ({ children }) => (
<RecoilRoot>
<IntlProvider>
<QueryClientProvider>
<ThemeProvider>{children}</ThemeProvider>
<ThemeProvider>
<NotificationsProvider>{children}</NotificationsProvider>
</ThemeProvider>
</QueryClientProvider>
</IntlProvider>
</RecoilRoot>

View File

@ -43,7 +43,6 @@ export function Button<TComponent = "button">(props: ButtonProps<TComponent>) {
const useStyles = createStyles((theme) => ({
common: {
borderRadius: 10,
fontSize: "0.875rem",
fontWeight: 600,
height: theme.other.buttonHeight,

View File

@ -0,0 +1,35 @@
// =============================================================================
// 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 { ReactText } 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;
}
export function TextButton(props: TextButtonProps) {
const { children, ...rest } = props;
return (
<UnstyledButton {...rest}>
<BodyText component="span" heavy sx={{ textDecoration: "underline" }}>
{children}
</BodyText>
</UnstyledButton>
);
}

View File

@ -0,0 +1,77 @@
// Vitest Snapshot v1
exports[`atoms::Buttons > renders error button 1`] = `
<DocumentFragment>
<button
class="mantine-Button-filled mantine-Button-root mantine-17e9v6f"
type="button"
>
<div
class="mantine-3xbgk5 mantine-Button-inner"
>
<span
class="mantine-qo1k2 mantine-Button-label"
>
Error
</span>
</div>
</button>
</DocumentFragment>
`;
exports[`atoms::Buttons > renders neutral button 1`] = `
<DocumentFragment>
<button
class="mantine-Button-filled mantine-Button-root mantine-13v2nwn"
type="button"
>
<div
class="mantine-3xbgk5 mantine-Button-inner"
>
<span
class="mantine-qo1k2 mantine-Button-label"
>
Neutral
</span>
</div>
</button>
</DocumentFragment>
`;
exports[`atoms::Buttons > renders primary button by default 1`] = `
<DocumentFragment>
<button
class="mantine-Button-filled mantine-Button-root mantine-pfssi"
type="button"
>
<div
class="mantine-3xbgk5 mantine-Button-inner"
>
<span
class="mantine-qo1k2 mantine-Button-label"
>
Primary
</span>
</div>
</button>
</DocumentFragment>
`;
exports[`atoms::Buttons > renders success button 1`] = `
<DocumentFragment>
<button
class="mantine-Button-filled mantine-Button-root mantine-1phtj0c"
type="button"
>
<div
class="mantine-3xbgk5 mantine-Button-inner"
>
<span
class="mantine-qo1k2 mantine-Button-label"
>
Success
</span>
</div>
</button>
</DocumentFragment>
`;

View File

@ -14,4 +14,5 @@
// limitations under the License.
// =============================================================================
export * from "./Buttons";
export * from "./Button";
export * from "./TextButton";

View File

@ -0,0 +1,35 @@
// =============================================================================
// 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 { LinkProps as RouterLinkProps } from "react-router-dom";
import { Link as RouterLink } from "react-router-dom";
import { BodyText } from "@atoms/Typography";
import type { ReactText } from "react";
interface LinkProps extends RouterLinkProps {
children: ReactText;
}
export function Link(props: LinkProps) {
const { children, ...rest } = props;
return (
<RouterLink {...rest}>
<BodyText component="span" heavy>
{children}
</BodyText>
</RouterLink>
);
}

View File

@ -14,4 +14,4 @@
// limitations under the License.
// =============================================================================
export const WIDTH = 470;
export * from "./Link";

View File

@ -30,7 +30,7 @@ const Template: ComponentStory<typeof NodeConnectSwitch> = () => {
<NodeConnectSwitch.Method
active={true}
current={true}
tabKey={"local-node"}
tabKey="local-node"
label="Local Node"
icon={<ServerIcon width="32px" height="62px" />}
>
@ -38,7 +38,7 @@ const Template: ComponentStory<typeof NodeConnectSwitch> = () => {
</NodeConnectSwitch.Method>
<NodeConnectSwitch.Method
tabKey={"remote-node"}
tabKey="remote-node"
label="Remote Node"
icon={<CloudIcon width="58px" height="54px" />}
>

View File

@ -62,7 +62,7 @@ export function NodeConnectSwitchMethod({
<Box className={cx(classes.tabCurrent)}>
<FormattedMessage
id={LangKeys.AccountSettingsCurrent}
defaultMessage={"Current"}
defaultMessage="Current"
/>
</Box>
)}

View File

@ -27,15 +27,12 @@ const Template: ComponentStory<typeof NodeStatus> = () => {
return (
<Stack>
<NodeStatus
title={"node.moneroworldcom:18089"}
title="node.moneroworldcom:18089"
status={NodeStatusType.Active}
/>
<NodeStatus title="node.xmr.pt:18081" status={NodeStatusType.Inactive} />
<NodeStatus
title={"node.xmr.pt:18081"}
status={NodeStatusType.Inactive}
/>
<NodeStatus
title={"node.monero.net:18081"}
title="node.monero.net:18081"
status={NodeStatusType.Active}
/>
</Stack>

View File

@ -24,11 +24,11 @@ describe("atoms::NodeStatus", () => {
const { asFragment } = render(
<AppProviders>
<NodeStatus
title={"node.moneroworldcom:18089:active"}
title="node.moneroworldcom:18089:active"
status={NodeStatusType.Active}
/>
<NodeStatus
title={"node.moneroworldcom:18089:inactive"}
title="node.moneroworldcom:18089:inactive"
status={NodeStatusType.Inactive}
/>
</AppProviders>

View File

@ -0,0 +1,39 @@
// =============================================================================
// 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 { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "@hooks/session/useAuth";
import { deleteSession } from "@src/utils/session";
import { ROUTES } from "@constants/routes";
export function ProtectedRoute({ children }: { children: ReactNode }) {
const { data: isAuthed, isLoading, isSuccess } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (isLoading) {
return;
}
if (!isAuthed) {
deleteSession();
navigate(ROUTES.Login);
}
}, [isLoading, isAuthed]);
return isSuccess ? <>{children}</> : null;
}

View File

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

View File

@ -25,7 +25,7 @@ describe("molecules::AccountSidebar", () => {
const { asFragment } = render(
<AppProviders>
<Routes>
<Route path={"/"} element={<AccountSidebar />} />
<Route path="/" element={<AccountSidebar />} />
</Routes>
</AppProviders>
);

View File

@ -16,7 +16,7 @@
import { useNavigate } from "react-router-dom";
import { SecondarySidebarItem } from "@molecules/SecondarySidebar";
import { useNavLinkActive } from "@src/hooks/useNavLinkActive";
import { useNavLinkActive } from "@src/hooks/misc/useNavLinkActive";
interface AccountSidebarItemProps {
label: string;

View File

@ -14,6 +14,7 @@
// limitations under the License.
// =============================================================================
import { useMemo } from "react";
import {
Box,
createStyles,
@ -22,37 +23,40 @@ import {
Text,
UnstyledButton,
} from "@mantine/core";
import type { PaymentAccount } from "haveno-ts";
import { ReactComponent as MenuIcon } from "@assets/ellipsis.svg";
import { HEIGHT, WIDTH, CurrencyLogos } from "./_constants";
import type { SupportedCurrencies } from "./_types";
import { HEIGHT, WIDTH } from "./_constants";
import { BodyText } from "@atoms/Typography";
import { useMemo } from "react";
import {
getPaymentAccountLogo,
getPaymentAccountName,
getPaymentAccountNumber,
} from "@src/utils/payment-account";
interface PaymentMethodCardProps {
currency: SupportedCurrencies;
accountId: string;
data: PaymentAccount;
}
export function PaymentMethodCard(props: PaymentMethodCardProps) {
const { accountId, currency } = props;
const { data } = props;
const { classes } = useStyles();
const Logo = useMemo(() => CurrencyLogos[currency].Logo, [currency]);
const Logo = useMemo(() => getPaymentAccountLogo(data), [data]);
return (
<Box className={classes.card}>
<Stack>
<Group position="apart">
<Group>
<Logo className={classes.logo} />
<Text className={classes.name}>{CurrencyLogos[currency].name}</Text>
<Logo />
<Text className={classes.name}>{getPaymentAccountName(data)}</Text>
</Group>
<UnstyledButton>
<MenuIcon className={classes.menuIcon} />
</UnstyledButton>
</Group>
<BodyText heavy sx={{ wordBreak: "break-word" }}>
{accountId}
{getPaymentAccountNumber(data)}
</BodyText>
</Stack>
</Box>

View File

@ -0,0 +1,44 @@
// =============================================================================
// 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, Space, Group } from "@mantine/core";
import { Button } from "@atoms/Buttons";
import { Heading, BodyText } from "@atoms/Typography";
interface ReadyToUseProps {
onSubmit: () => void;
}
export function ReadyToUse(props: ReadyToUseProps) {
const { onSubmit } = props;
return (
<Stack>
<Heading order={1}>Haveno is ready for use.</Heading>
<BodyText size="lg">
Youve succesfully set up Haveno. Please note that to be able to trade,
you need to deposit Monero in your Haveno wallet and set up a payment
account.
</BodyText>
<Space h="lg" />
<Group position="apart">
<Button type="submit" onClick={onSubmit}>
Start using Haveno
</Button>
</Group>
</Stack>
);
}

View File

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

View File

@ -24,9 +24,9 @@ describe("molecules::SecondarySidebar", () => {
const { asFragment } = render(
<ThemeProvider>
<SecondarySidebar>
<SecondarySidebarItem label={"Active item"} isActive={true} />
<SecondarySidebarItem label={"Inactive item"} isActive={false} />
<SecondarySidebarItem label={"Active item"} isActive={true} />
<SecondarySidebarItem label="Active item" isActive={true} />
<SecondarySidebarItem label="Inactive item" isActive={false} />
<SecondarySidebarItem label="Active item" isActive={true} />
</SecondarySidebar>
</ThemeProvider>
);

View File

@ -114,13 +114,13 @@ export function AddPaymentMethod() {
);
}
const schema = Joi.object({
const schema = Joi.object<FormValues>({
currency: Joi.string().required(),
paymentMethod: Joi.string().required(),
accountNumber: Joi.string().required(),
});
const Currencies = SupportedCurrencies.map((curr) => ({
value: curr.id,
label: curr.name,
value: curr.id,
}));

View File

@ -172,7 +172,7 @@ exports[`organisms::AddPaymentMethod > renders without exploding 1`] = `
class="mantine-Group-root mantine-mk7hdv"
>
<button
class="mantine-Button-filled mantine-Button-root mantine-Group-child mantine-1i7r9hr"
class="mantine-Button-filled mantine-Button-root mantine-Group-child mantine-1bqp2m7"
type="submit"
>
<div

View File

@ -17,66 +17,96 @@
import { FormattedMessage } from "react-intl";
import { Stack, Box, Group } from "@mantine/core";
import { useForm, joiResolver } from "@mantine/form";
import { showNotification } from "@mantine/notifications";
import { TextInput } from "@atoms/TextInput";
import { LangKeys } from "@constants/lang";
import { useAccountSecurityFormSchema } from "./_hooks";
import { Button } from "@atoms/Buttons";
import { useChangePassword } from "@hooks/storage/useChangePassword";
import { useAccountSecurityFormSchema } from "./_hooks";
import type { ChangePasswordFormValues } from "./_types";
export function AccountSecurityForm() {
export function ChangePassword() {
const accountSecurityFormSchema = useAccountSecurityFormSchema();
const { mutate: changePassword } = useChangePassword();
const form = useForm({
const form = useForm<ChangePasswordFormValues>({
initialValues: {
currentPassword: "",
password: "",
newPassword: "",
confirmPassword: "",
},
schema: joiResolver(accountSecurityFormSchema),
});
const handleSubmit = (values: ChangePasswordFormValues) => {
changePassword(
{
currentPassword: values.currentPassword,
newPassword: values.newPassword,
},
{
onError: (err) => {
console.dir(err);
showNotification({
color: "red",
message: err.message,
title: "Something went wrong",
});
},
onSuccess: () => {
showNotification({
color: "green",
message: "Password updated successfully",
});
form.reset();
},
}
);
};
return (
<Box>
<form onSubmit={form.onSubmit((values) => console.log(values))}>
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack spacing="lg">
<TextInput
id={"password"}
type={"password"}
id="password"
type="password"
required
label={
<FormattedMessage
id={LangKeys.AccountSecurityFieldPassword}
defaultMessage={"Password"}
defaultMessage="Password"
/>
}
{...form.getInputProps("password")}
{...form.getInputProps("newPassword")}
/>
<TextInput
id={"confirmPassword"}
id="confirmPassword"
required
type={"password"}
type="password"
label={
<FormattedMessage
id={LangKeys.AccountSecurityFieldRepeatPassword}
defaultMessage={"Repeat new password"}
defaultMessage="Repeat new password"
/>
}
{...form.getInputProps("confirmPassword")}
/>
<TextInput
id={"currentPassword"}
type={"password"}
id="currentPassword"
type="password"
required
label={
<FormattedMessage
id={LangKeys.AccountSecurityFieldCurrentPassword}
defaultMessage={"Current password"}
defaultMessage="Current password"
/>
}
{...form.getInputProps("currentPassword")}
/>
<Group position="right" mt="md">
<Button size="md" type={"submit"}>
<FormattedMessage id={LangKeys.Save} defaultMessage={"Save"} />
<Button size="md" type="submit">
<FormattedMessage id={LangKeys.Save} defaultMessage="Save" />
</Button>
</Group>
</Stack>

View File

@ -0,0 +1,64 @@
// =============================================================================
// 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 { LangKeys } from "@constants/lang";
import Joi from "joi";
import { useIntl } from "react-intl";
import type { ChangePasswordFormValues } from "./_types";
const MIN_PASSWORD_CHARS = 8;
const getPasswordRegex = () => {
return RegExp(
`^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{${MIN_PASSWORD_CHARS},})`,
"i"
);
};
export const useAccountSecurityFormSchema = () => {
const { formatMessage } = useIntl();
return Joi.object<ChangePasswordFormValues>({
newPassword: Joi.string()
.required()
.regex(getPasswordRegex())
.options({
messages: {
"string.pattern.base": formatMessage({
id: LangKeys.AccountSecurityFieldPasswordFormatMsg,
defaultMessage: "This password is too weak",
}),
},
}),
confirmPassword: Joi.string()
.required()
.valid(Joi.ref("newPassword"))
.messages({
"any.only": formatMessage({
id: LangKeys.AccountSecurityFieldRepeatPasswordMatchMsg,
defaultMessage: "Passwords don't match",
}),
}),
currentPassword: Joi.string()
.required()
.label(
formatMessage({
id: LangKeys.AccountSecurityFieldCurrentPassword,
defaultMessage: "Current password",
})
),
});
};

View File

@ -14,12 +14,8 @@
// limitations under the License.
// =============================================================================
import { AccountLayout } from "@templates/AccountLayout";
export function Account() {
return (
<AccountLayout>
<h1>Payment accounts</h1>
</AccountLayout>
);
export interface ChangePasswordFormValues {
currentPassword: string;
newPassword: string;
confirmPassword: string;
}

View File

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

View File

@ -20,12 +20,15 @@ import {
AddPaymentMethodButton,
PaymentMethodCard,
} from "@molecules/PaymentMethodCard";
import { usePaymentAccounts } from "@hooks/haveno/usePaymentAccounts";
interface PaymentMethodsProps {
onAdd: () => void;
}
export function PaymentMethodList({ onAdd }: PaymentMethodsProps) {
const { data: paymentAccounts, isLoading } = usePaymentAccounts();
return (
<Stack spacing="lg">
<Stack sx={{ maxWidth: "32rem" }}>
@ -37,15 +40,15 @@ export function PaymentMethodList({ onAdd }: PaymentMethodsProps) {
</BodyText>
</Stack>
<Space h="xl" />
<Group spacing="xl">
<PaymentMethodCard
accountId="1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2"
currency="BTC"
/>
<PaymentMethodCard accountId="tqTFn5Au4m4GFg7x" currency="ETH" />
<PaymentMethodCard accountId="112233" currency="EUR" />
<AddPaymentMethodButton onClick={onAdd} />
</Group>
{isLoading && <BodyText>Loading accounts ...</BodyText>}
{!isLoading && paymentAccounts?.length && (
<Group spacing="xl">
{paymentAccounts.map((account) => (
<PaymentMethodCard key={account.getId()} data={account} />
))}
<AddPaymentMethodButton onClick={onAdd} />
</Group>
)}
</Stack>
);
}

View File

@ -0,0 +1,58 @@
// =============================================================================
// 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, 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";
interface SelectMoneroNodeProps {
onGoBack: () => void;
onNext: ({ url, password }: { url: string; password: string }) => void;
}
export function SelectMoneroNode(props: SelectMoneroNodeProps) {
const { onGoBack, onNext } = props;
const handleSubmit = (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();
// TODO: fix
onNext({
url: "http://192.168.29.59:8080",
password: "apitest",
});
};
return (
<form onSubmit={handleSubmit}>
<Stack>
<Heading order={1}>Select a node</Heading>
<BodyText size="lg">
We found a local node running on your machine, its recommended to use
this one. Alternatively you can select one of the curated nodes below
add another node.
</BodyText>
<Select id="fiat" data={[]} placeholder="Pick one" />
<Space h="lg" />
<Group position="apart">
<TextButton onClick={onGoBack}>Go Back</TextButton>
<Button type="submit">Next</Button>
</Group>
</Stack>
</form>
);
}

View File

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

View File

@ -0,0 +1,93 @@
// =============================================================================
// 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, Space, Group, Container } from "@mantine/core";
import { joiResolver, useForm } from "@mantine/form";
import Joi from "joi";
import { BodyText, Heading } from "@atoms/Typography";
import { TextInput } from "@atoms/TextInput";
import { Button, TextButton } from "@atoms/Buttons";
import { LangKeys } from "@constants/lang";
interface SetPasswordProps {
value: string;
onGoBack: () => void;
onNext: (password: string) => void;
}
export function SetPassword(props: SetPasswordProps) {
const { value, onGoBack, onNext } = props;
const { getInputProps, onSubmit } = useForm<FormValues>({
schema: joiResolver(schema),
initialValues: {
password: value ?? "",
repeatPassword: "",
},
});
const handleSubmit = (values: FormValues) => {
onNext(values.password);
};
return (
<form onSubmit={onSubmit(handleSubmit)}>
<Stack>
<Container>
<Heading order={1} stringId={LangKeys.CreatePassword}>
Create a password
</Heading>
</Container>
<BodyText size="lg">
All your data is stored locally on your machine. Haveno uses solely a
password.
</BodyText>
<TextInput
id="password"
label="Password"
type="password"
{...getInputProps("password")}
/>
<TextInput
id="repeatPassword"
label="Repeat password"
type="password"
{...getInputProps("repeatPassword")}
/>
<Space h="lg" />
<Group position="apart">
<TextButton onClick={onGoBack}>Go Back</TextButton>
<Button type="submit">Next</Button>
</Group>
</Stack>
</form>
);
}
interface FormValues {
password: string;
repeatPassword: string;
}
const schema = Joi.object({
password: Joi.string().min(6).required().messages({
"string.min": "Password too short",
"string.empty": "Password can't be empty",
}),
repeatPassword: Joi.string().required().valid(Joi.ref("password")).messages({
"any.only": "Passwords don't match",
"string.empty": "Please type the password again",
}),
});

View File

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

View File

@ -0,0 +1,83 @@
// =============================================================================
// 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, Space, Group } from "@mantine/core";
import { joiResolver, useForm } from "@mantine/form";
import Joi from "joi";
import { BodyText, Heading } from "@atoms/Typography";
import { Button, TextButton } from "@atoms/Buttons";
import { Select } from "@atoms/Select";
import { SupportedCurrencies } from "@constants/currencies";
interface SetSetPrimaryFiatProps {
onGoBack: () => void;
onNext: (fiat: string) => void;
value: string;
}
export function SetPrimaryFiat(props: SetSetPrimaryFiatProps) {
const { onGoBack, onNext, value } = props;
const { getInputProps, onSubmit } = useForm<FormValues>({
schema: joiResolver(schema),
initialValues: {
fiat: value ?? "",
},
});
const handleSubmit = (values: FormValues) => {
onNext(values.fiat);
};
return (
<form onSubmit={onSubmit(handleSubmit)}>
<Stack>
<Heading order={1}>
Choose the fiat currency you want to primarily use.
</Heading>
<BodyText size="lg">
Haveno uses this to show you conversion rates of your funds. You can
still trade every pair of Monero/Fiat.
</BodyText>
<Select
id="fiat"
data={CURRENCIES}
placeholder="Pick one"
{...getInputProps("fiat")}
/>
<Space h="lg" />
<Group position="apart">
<TextButton onClick={onGoBack}>Go Back</TextButton>
<Button type="submit">Next</Button>
</Group>
</Stack>
</form>
);
}
interface FormValues {
fiat: string;
}
const schema = Joi.object({
fiat: Joi.string().required().messages({
"string.empty": "Please select a currency",
}),
});
const CURRENCIES = SupportedCurrencies.filter((cur) => cur.fiat).map((cur) => ({
label: cur.name,
value: cur.id,
}));

View File

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

View File

@ -20,14 +20,15 @@ import { HeaderWithLogo } from "@atoms/Header";
interface CenteredLayoutProps {
showHeader?: boolean;
size?: number;
}
export const CenteredLayout: FC<CenteredLayoutProps> = (props) => {
const { children, showHeader = false } = props;
const { children, showHeader = false, size } = props;
return (
<Stack sx={{ width: "100%" }}>
{showHeader && <HeaderWithLogo />}
<Container p="sm" sx={{ display: "flex", flex: 1 }}>
<Container p="sm" size={size} sx={{ display: "flex", flex: 1 }}>
{children}
</Container>
</Stack>

View File

@ -23,18 +23,21 @@ export const SupportedCurrencies = [
{
id: "BTC",
name: "Bitcoin",
fiat: false,
logo: BtcLogo,
paymentMethods: [PaymentMethodIds.BLOCK_CHAINS_ID],
},
{
id: "ETH",
name: "Ethereum",
fiat: false,
logo: EthLogo,
paymentMethods: [PaymentMethodIds.BLOCK_CHAINS_ID],
},
{
id: "EUR",
name: "Euro",
fiat: true,
logo: EurLogo,
paymentMethods: [
// EUR
@ -84,6 +87,7 @@ export const SupportedCurrencies = [
{
id: "USD",
name: "US Dollar",
fiat: true,
logo: EurLogo,
paymentMethods: [
// US

View File

@ -0,0 +1,20 @@
// =============================================================================
// 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 const HAVENO_DAEMON_URL =
import.meta.env.VITE_HAVENO_URL ?? "http://localhost:8080";
export const HAVENO_DAEMON_PASSWORD =
import.meta.env.VITE_HAVENO_PASSWORD ?? "apitest";

View File

@ -21,6 +21,7 @@ export enum LangKeys {
ConnectingToNetwork = "app.connectingToNetwork",
WelcomeToHaveno = "app.welcomeToHaveno",
Save = "app.save",
CreatePassword = "onboarding.createPassword",
AccountTitle = "account.title",
AccountSidebarPaymentAccounts = "account.sidebar.paymentAccounts",
AccountSidebarSecurity = "account.sidebar.security",

View File

@ -45,13 +45,14 @@ const LangPackEN: { [key in LangKeys]: string } = {
[LangKeys.AccountNodeStopDeamon]: "Stop deamon",
[LangKeys.AccountSettingsAddNode]: "Add a new node",
[LangKeys.AccountSettingsCurrent]: "Current",
[LangKeys.AccountSecurityFieldPassword]: "Password",
[LangKeys.AccountSecurityFieldPassword]: "Update account password",
[LangKeys.AccountSecurityFieldRepeatPassword]: "Repeat new password",
[LangKeys.AccountSecurityFieldCurrentPassword]: "Current password",
[LangKeys.AccountSecurityFieldPasswordFormatMsg]:
"contain atleast {minChars} characters, one uppercase, one lowercase and one number.",
"Password must contain atleast {minChars} characters, one uppercase, one lowercase and one number.",
[LangKeys.AccountSecurityFieldRepeatPasswordMatchMsg]:
"Password confirmation doesn't match Password.",
"Passwords don't match",
[LangKeys.CreatePassword]: "Create password",
};
export default LangPackEN;

View File

@ -53,6 +53,7 @@ const LangPackES: { [key in LangKeys]: string } = {
"contener al menos {minChars} caracteres, una mayúscula, una minúscula y un número.",
[LangKeys.AccountSecurityFieldRepeatPasswordMatchMsg]:
"La confirmación de la contraseña no coincide con la contraseña.",
[LangKeys.CreatePassword]: "Crear contraseña",
};
export default LangPackES;

View File

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

View File

@ -15,19 +15,18 @@
// =============================================================================
export const ROUTES = {
Home: "/",
Home: "",
HomeAlias: "/",
Login: "/login",
Welcome: "/onboarding/welcome",
CreateAccount: "/onboarding/create-account",
RestoreBackup: "/onboarding/restore-backup",
SetupAccount: "/onboarding/setup",
Wallet: "/wallet",
// Account routes.
Account: "/account",
// Account routes
AccountPaymentAccounts: "/account/payment-accounts",
AccountAddPaymentAccount: "/account/payment-accounts/add",
AccountNodeSettings: "/account/node-settings",
AccountBackup: "/account/backup",
AccountWallet: "/account/wallet",
AccountSecurity: "/account/security",
AccountPaymentMethods: "/account/payment-methods",
AccountAddPaymentMethod: "/account/payment-methods/add",
};

View File

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

View File

@ -0,0 +1,29 @@
// =============================================================================
// 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 } from "react-query";
import { useHavenoClient } from "./useHavenoClient";
interface Variables {
password: string;
}
export function useCreateAccount() {
const client = useHavenoClient();
return useMutation(async (variables: Variables) =>
client.createAccount(variables.password)
);
}

View File

@ -0,0 +1,35 @@
// =============================================================================
// 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 { HavenoClient } from "haveno-ts";
import { useRef } from "react";
import {
HAVENO_DAEMON_PASSWORD,
HAVENO_DAEMON_URL,
} from "@constants/haveno-daemon";
let havenoClient: HavenoClient;
export function useHavenoClient() {
const client = useRef<HavenoClient>(havenoClient);
if (!client.current) {
client.current = havenoClient = new HavenoClient(
HAVENO_DAEMON_URL,
HAVENO_DAEMON_PASSWORD
);
}
return client.current;
}

View File

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

View File

@ -0,0 +1,36 @@
// =============================================================================
// 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 type { PaymentAccount } from "haveno-ts";
import { useQuery } from "react-query";
import { useHavenoClient } from "./useHavenoClient";
export function usePaymentAccounts() {
const client = useHavenoClient();
return useQuery<Array<PaymentAccount>, Error>(
QueryKeys.PaymentAccounts,
async () => {
try {
const accounts = await client.getPaymentAccounts();
return accounts.map((acc) => acc);
} catch (ex) {
console.error(ex);
return [];
}
}
);
}

View File

@ -0,0 +1,29 @@
// =============================================================================
// 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 } from "react-query";
import { useHavenoClient } from "./useHavenoClient";
interface Variables {
connection: string;
}
export function useSetMoneroConnection() {
const client = useHavenoClient();
return useMutation(async (variables: Variables) =>
client.setMoneroConnection(variables.connection)
);
}

View File

@ -0,0 +1,35 @@
// =============================================================================
// 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 { validateSession } from "@src/utils/session";
import { useQuery } from "react-query";
export function useAuth() {
return useQuery(
QueryKeys.AuthSession,
async () => {
if (await validateSession()) {
return true;
}
return false;
},
{
staleTime: 60000,
retry: false,
}
);
}

View File

@ -0,0 +1,34 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { createSession } from "@src/utils/session";
import { useMutation } from "react-query";
interface Variables {
password: string;
}
export function useLogin() {
return useMutation<void, Error, Variables>(async (variables: Variables) => {
const authToken = await window.electronStore.verifyPassword(
variables.password
);
if (!authToken) {
throw new Error("Invalid password");
}
createSession(authToken);
});
}

View File

@ -0,0 +1,48 @@
// =============================================================================
// 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 { getIpcError } from "@src/utils/get-ipc-error";
import { createSession } from "@src/utils/session";
import { useMutation, useQueryClient } from "react-query";
interface Variables {
currentPassword: string;
newPassword: string;
}
export function useChangePassword() {
const queryClient = useQueryClient();
return useMutation<string, Error, Variables>(
async (variables: Variables) => {
try {
const authToken = await window.electronStore.changePassword(variables);
return authToken;
} catch (ex) {
throw new Error(getIpcError(ex as Error));
}
},
{
onSuccess: (authToken) => {
// update the session jwt
createSession(authToken).then(() => {
queryClient.invalidateQueries(QueryKeys.StorageAccountInfo);
});
},
}
);
}

View File

@ -0,0 +1,44 @@
// =============================================================================
// 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 {
password: string;
primaryFiat: string;
moneroNode: string;
}
export function useCreateAccount() {
const queryClient = useQueryClient();
return useMutation<void, Error, Variables>(
async (variables: Variables) => {
await Promise.all([
window.electronStore.setPassword({ newPassword: variables.password }),
window.electronStore.setPrimaryFiat(variables.primaryFiat),
window.electronStore.setMoneroNode(variables.moneroNode),
]);
},
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.StorageAccountInfo);
queryClient.invalidateQueries(QueryKeys.StoragePreferences);
},
}
);
}

View File

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

View File

@ -0,0 +1,25 @@
// =============================================================================
// 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 type { IPreferences } from "@src/types";
export function usePreferences() {
return useQuery<IPreferences, Error>(QueryKeys.StoragePreferences, async () =>
window.electronStore.getPreferences()
);
}

View File

@ -0,0 +1,36 @@
// =============================================================================
// 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 {
fiat: string;
}
export function useSetPrimaryFiat() {
const queryClient = useQueryClient();
return useMutation<void, Error, Variables>(
async (variables: Variables) =>
window.electronStore.setPrimaryFiat(variables.fiat),
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.StorageAccountInfo);
},
}
);
}

View File

@ -14,12 +14,19 @@
// limitations under the License.
// =============================================================================
import { useNavigate } from "react-router-dom";
import { ROUTES } from "@constants/routes";
import { PaymentMethodList } from "@organisms/PaymentMethodList";
import { AccountLayout } from "@templates/AccountLayout";
export function AccountPaymentAccounts() {
const navigate = useNavigate();
return (
<AccountLayout>
<h1>Payment accounts</h1>
<PaymentMethodList
onAdd={() => navigate(ROUTES.AccountAddPaymentAccount)}
/>
</AccountLayout>
);
}

View File

@ -17,24 +17,8 @@
import { LangKeys } from "@constants/lang";
import { Stack, Box, createStyles, Group } from "@mantine/core";
import { AccountLayout } from "@templates/AccountLayout";
import { AccountSecurityForm } from "./AccountSecurityForm";
import { Heading, BodyText } from "@atoms/Typography";
import { WIDTH } from "./_constants";
function AccountSecurityHeader() {
return (
<Group spacing="sm">
<Heading stringId={LangKeys.AccountSecurityTitle} order={3}>
Account Security
</Heading>
<BodyText stringId={LangKeys.AccountSecurityDesc} size="md">
Haveno does not store any of your data, this happens solely locally on
your device. Its not possible to restore your password when lost.
Please make sure you store a copy of it on a safe place.
</BodyText>
</Group>
);
}
import { ChangePassword } from "@organisms/ChangePassword";
export function AccountSecurity() {
const { classes } = useStyles();
@ -43,16 +27,26 @@ export function AccountSecurity() {
<AccountLayout>
<Box className={classes.content}>
<Stack spacing="lg">
<AccountSecurityHeader />
<AccountSecurityForm />
<Group spacing="sm">
<Heading stringId={LangKeys.AccountSecurityTitle} order={3}>
Account Security
</Heading>
<BodyText heavy stringId={LangKeys.AccountSecurityDesc} size="md">
Haveno does not store any of your data, this happens solely
locally on your device. Its not possible to restore your password
when lost. Please make sure you store a copy of it on a safe
place.
</BodyText>
</Group>
<ChangePassword />
</Stack>
</Box>
</AccountLayout>
);
}
const useStyles = createStyles(() => ({
const useStyles = createStyles((theme) => ({
content: {
maxWidth: WIDTH,
maxWidth: theme.other.contentWidthMd,
},
}));

View File

@ -15,12 +15,12 @@
// =============================================================================
import { AddPaymentMethod as AddPaymentMethodOrganism } from "@organisms/AddPaymentMethod";
import { NavbarLayout } from "@templates/NavbarLayout";
import { AccountLayout } from "@templates/AccountLayout";
export function AddPaymentMethod() {
export function AddPaymentAccount() {
return (
<NavbarLayout>
<AccountLayout>
<AddPaymentMethodOrganism />
</NavbarLayout>
</AccountLayout>
);
}

View File

@ -36,23 +36,23 @@ export function NodeLocalForm() {
<NodeLocalStopDeamon />
<form onSubmit={form.onSubmit((values) => console.log(values))}>
<Stack spacing={"lg"}>
<Stack spacing="lg">
<TextInput
id={"blockchainLocation"}
id="blockchainLocation"
label={
<FormattedMessage
id={LangKeys.AccountNodeFieldBlockchainLocation}
defaultMessage={"Blockchain location"}
defaultMessage="Blockchain location"
/>
}
{...form.getInputProps("blockchainLocation")}
/>
<TextInput
id={"deamonFlags"}
id="deamonFlags"
label={
<FormattedMessage
id={LangKeys.AccountNodeFieldDeamonFlags}
defaultMessage={"Deamon startup flags"}
defaultMessage="Deamon startup flags"
/>
}
{...form.getInputProps("startupFlags")}
@ -60,11 +60,11 @@ export function NodeLocalForm() {
<Grid>
<Grid.Col span={9}>
<TextInput
id={"deamonAddress"}
id="deamonAddress"
label={
<FormattedMessage
id={LangKeys.AccountNodeFieldDeamonAddress}
defaultMessage={"Deamon Address"}
defaultMessage="Deamon Address"
/>
}
{...form.getInputProps("deamonAddress")}
@ -72,11 +72,11 @@ export function NodeLocalForm() {
</Grid.Col>
<Grid.Col span={3}>
<TextInput
id={"port"}
id="port"
label={
<FormattedMessage
id={LangKeys.AccountNodeFieldPort}
defaultMessage={"Port"}
defaultMessage="Port"
/>
}
{...form.getInputProps("port")}
@ -94,10 +94,10 @@ function NodeLocalStopDeamon() {
return (
<div className={classes.actions}>
<Button flavor={"neutral"}>
<Button flavor="neutral">
<FormattedMessage
id={LangKeys.AccountNodeStopDeamon}
defaultMessage={"Stop deamon"}
defaultMessage="Stop deamon"
/>
</Button>
</div>

View File

@ -24,22 +24,19 @@ export function NodeRemoteStatus() {
return (
<Stack>
<NodeStatus
title={"node.moneroworldcom:18089"}
title="node.moneroworldcom:18089"
status={NodeStatusType.Active}
/>
<NodeStatus title="node.xmr.pt:18081" status={NodeStatusType.Inactive} />
<NodeStatus
title={"node.xmr.pt:18081"}
status={NodeStatusType.Inactive}
/>
<NodeStatus
title={"node.monero.net:18081"}
title="node.monero.net:18081"
status={NodeStatusType.Active}
/>
<AddNewNodeButton />
<Group position={"right"} mt={"sm"}>
<Button size={"md"}>
<FormattedMessage id={LangKeys.Save} defaultMessage={"Save"} />
<Group position="right" mt="sm">
<Button size="md">
<FormattedMessage id={LangKeys.Save} defaultMessage="Save" />
</Button>
</Group>
</Stack>
@ -51,8 +48,8 @@ function AddNewNodeButton({ ...rest }) {
return (
<Button
variant={"subtle"}
color={"dark"}
variant="subtle"
color="dark"
classNames={{
root: classes.root,
inner: classes.inner,
@ -62,7 +59,7 @@ function AddNewNodeButton({ ...rest }) {
+{" "}
<FormattedMessage
id={LangKeys.AccountSettingsAddNode}
defaultMessage={"Add a new node"}
defaultMessage="Add a new node"
/>
</Button>
);

View File

@ -18,7 +18,6 @@ import { Stack, Box, createStyles } from "@mantine/core";
import { AccountLayout } from "@templates/AccountLayout";
import { LangKeys } from "@constants/lang";
import { NodeSettingsSwitch } from "./NodeSettingsSwitch";
import { WIDTH } from "./_constants";
import { BodyText, Heading } from "@atoms/Typography";
export function AccountNodeSettings() {
@ -27,13 +26,13 @@ export function AccountNodeSettings() {
return (
<AccountLayout>
<Box className={classes.content}>
<Stack spacing={"sm"}>
<Stack spacing="sm">
<Heading stringId={LangKeys.AccountNodeSettingsTitle} order={3}>
Your node settings
</Heading>
<BodyText
stringId={LangKeys.AccountNodeSettingsDesc}
size={"md"}
size="md"
className={classes.paragraph}
>
Using a local node is recommended, but does require loading the
@ -49,7 +48,7 @@ export function AccountNodeSettings() {
const useStyles = createStyles((theme) => ({
content: {
maxWidth: WIDTH,
maxWidth: theme.other.contentWidthMd,
},
paragraph: {
marginBottom: theme.spacing.xl,

View File

@ -28,17 +28,17 @@ export function NodeSettingsSwitch() {
return (
<NodeConnectSwitch
initialTab={"local-node"}
initialTab="local-node"
className={classes.connectSwitch}
>
<NodeConnectSwitch.Method
active={true}
current={true}
tabKey={"local-node"}
tabKey="local-node"
label={
<FormattedMessage
id={LangKeys.AccountNodeSettingsLocal}
defaultMessage={"Local Node"}
defaultMessage="Local Node"
/>
}
icon={<ServerIcon width={32} height={62} />}
@ -47,11 +47,11 @@ export function NodeSettingsSwitch() {
</NodeConnectSwitch.Method>
<NodeConnectSwitch.Method
tabKey={"remote-node"}
tabKey="remote-node"
label={
<FormattedMessage
id={LangKeys.AccountNodeSettingsRemote}
defaultMessage={"Remote Node"}
defaultMessage="Remote Node"
/>
}
icon={<CloudIcon width={58} height={54} />}

View File

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

View File

@ -1,57 +0,0 @@
import { LangKeys } from "@constants/lang";
import Joi from "joi";
import { useIntl } from "react-intl";
import { MIN_PASSWORD_CHARS } from "./_constants";
const getPasswordRegex = () => {
return RegExp(
`^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{${MIN_PASSWORD_CHARS},})`,
"i"
);
};
export const useAccountSecurityFormSchema = () => {
const { formatMessage } = useIntl();
return Joi.object({
password: Joi.string()
.required()
.regex(
getPasswordRegex(),
formatMessage(
{
id: LangKeys.AccountSecurityFieldPasswordFormatMsg,
defaultMessage: `contain atleast ${MIN_PASSWORD_CHARS} characters, one uppercase, one lowercase and one number`,
},
{
minChars: MIN_PASSWORD_CHARS,
}
)
)
.label(
formatMessage({
id: LangKeys.AccountSecurityFieldPassword,
defaultMessage: "Password",
})
),
confirmPassword: Joi.string()
.valid(Joi.ref("password"))
.required()
.options({
messages: {
"any.only": formatMessage({
id: LangKeys.AccountSecurityFieldRepeatPasswordMatchMsg,
defaultMessage: "Password confirmation doesn't match Password.",
}),
},
}),
currentPassword: Joi.string()
.required()
.label(
formatMessage({
id: LangKeys.AccountSecurityFieldCurrentPassword,
defaultMessage: "Current password",
})
),
});
};

View File

@ -14,5 +14,10 @@
// limitations under the License.
// =============================================================================
export * from "./AddPaymentMethod";
export * from "./AddPaymentAccount";
export * from "./PaymentMethods";
export * from "./AccountBackup";
export * from "./NodeSettings";
export * from "./AccountPaymentAccounts";
export * from "./AccountSecurity";
export * from "./AccountWallet";

View File

@ -0,0 +1,65 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Box, Space, Stack } from "@mantine/core";
import { LangKeys } from "@constants/lang/LangKeys";
import { CenteredLayout } from "@templates/CenteredLayout";
import { Heading } from "@atoms/Typography";
import Logo from "@assets/logo.svg";
import { useAccountInfo } from "@src/hooks/storage/useGetAccountInfo";
import { ROUTES } from "@constants/routes";
import { showNotification } from "@mantine/notifications";
export function Home() {
const { data: accountInfo, isSuccess, isError } = useAccountInfo();
const navigate = useNavigate();
useEffect(() => {
if (isSuccess) {
if (!accountInfo) {
setTimeout(() => navigate(ROUTES.Welcome, { replace: true }), 1000);
} else {
setTimeout(() => navigate(ROUTES.Login, { replace: true }), 1000);
}
} else if (isError) {
showNotification({
color: "red",
title: "Unable to load account",
message: "Failed to load account details",
});
}
}, [isSuccess, isError]);
return (
<CenteredLayout>
<Stack align="center" justify="center" sx={{ flex: 1 }}>
<Stack>
<Box component="img" src={Logo} alt="Haveno" />
<Heading
order={2}
stringId={LangKeys.AppHeading2}
sx={{ fontWeight: 500 }}
>
Monero based decentralized exchange
</Heading>
</Stack>
<Space h="lg" />
</Stack>
</CenteredLayout>
);
}

View File

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

View File

@ -0,0 +1,95 @@
// =============================================================================
// 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 Joi from "joi";
import { joiResolver, useForm } from "@mantine/form";
import { useNavigate } from "react-router-dom";
import { Container, Group, Space, Stack } from "@mantine/core";
import { showNotification } from "@mantine/notifications";
import { CenteredLayout } from "@templates/CenteredLayout";
import { BodyText, Heading } from "@atoms/Typography";
import { ROUTES } from "@constants/routes";
import { useLogin } from "@hooks/session/useLogin";
import { Button } from "@atoms/Buttons";
import { TextInput } from "@atoms/TextInput";
import { CONTENT_MAX_WIDTH } from "./_constants";
export function Login() {
const { mutate: login } = useLogin();
const navigate = useNavigate();
const { getInputProps, onSubmit } = useForm<FormValues>({
schema: joiResolver(schema),
initialValues: {
password: "",
},
});
const handleSubmit = (values: FormValues) => {
login(values, {
onSuccess: () => {
navigate(ROUTES.AccountPaymentAccounts, { replace: true });
},
onError: (err) => {
showNotification({
title: "Login failed",
message: err.message,
color: "red",
});
},
});
};
return (
<CenteredLayout showHeader size={CONTENT_MAX_WIDTH}>
<Stack align="center" justify="center" sx={{ flex: 1 }}>
<form onSubmit={onSubmit(handleSubmit)}>
<Stack>
<Container>
<Heading order={1}>Login to Haveno</Heading>
</Container>
<BodyText size="lg">
All your data is stored locally on your machine. Haveno uses
solely a password.
</BodyText>
<Space h="lg" />
<TextInput
id="password"
label="Password"
type="password"
{...getInputProps("password")}
/>
<Space h="lg" />
<Group position="apart">
<Button type="submit">Login</Button>
</Group>
</Stack>
</form>
</Stack>
</CenteredLayout>
);
}
interface FormValues {
password: string;
}
const schema = Joi.object({
password: Joi.string().min(6).required().messages({
"string.min": "Password too short",
"string.empty": "Password can't be empty",
}),
});

View File

@ -14,7 +14,4 @@
// limitations under the License.
// =============================================================================
export const WIDTH = 475;
// The minimum characters that should password field contain.
export const MIN_PASSWORD_CHARS = 8;
export const CONTENT_MAX_WIDTH = 470;

View File

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

View File

@ -21,7 +21,7 @@ import { ConnectionProgress } from "@atoms/ConnectionProgress";
import { Heading } from "@atoms/Typography";
import Logo from "@assets/logo.svg";
export function Home() {
export function ConnectingMonero() {
return (
<CenteredLayout>
<Stack align="center" justify="center" sx={{ flex: 1 }}>

View File

@ -0,0 +1,105 @@
// =============================================================================
// 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, 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 { SetPassword } from "@organisms/SetPassword";
import { SetPrimaryFiat } from "@organisms/SetPrimaryFiat";
import { SelectMoneroNode } from "@organisms/SelectMoneroNode";
import { ReadyToUse } from "@molecules/ReadyToUse";
enum Steps {
CreatePassword = "CreatePassword",
SetFiat = "SetFiat",
SelectNode = "SelectNode",
Completed = "Completed",
}
export function CreateAccount() {
const [step, setStep] = useState<Steps>(Steps.CreatePassword);
const [password, setPassword] = useState("");
const [fiat, setFiat] = useState("");
const navigate = useNavigate();
const { mutate: createAccount } = useCreateAccount();
const handleSetPassword = (value: string) => {
setPassword(value);
setStep(Steps.SetFiat);
};
const handleSetFiat = (value: string) => {
setFiat(value);
setStep(Steps.SelectNode);
};
const handleCreateAccount = (moneroNode: {
url: string;
password: string;
}) => {
createAccount(
{
moneroNode: moneroNode.url,
password,
primaryFiat: fiat,
},
{
onSuccess: () => setStep(Steps.Completed),
}
);
};
return (
<CenteredLayout showHeader>
<Stack align="center" justify="center" sx={{ flex: 1 }}>
<Container size={CONTENT_MAX_WIDTH}>
{step === Steps.CreatePassword && (
<SetPassword
value={password}
onGoBack={() => navigate(ROUTES.Welcome)}
onNext={handleSetPassword}
/>
)}
{step === Steps.SetFiat && (
<SetPrimaryFiat
value={fiat}
onGoBack={() => setStep(Steps.CreatePassword)}
onNext={handleSetFiat}
/>
)}
{step === Steps.SelectNode && (
<SelectMoneroNode
onGoBack={() => setStep(Steps.SetFiat)}
onNext={handleCreateAccount}
/>
)}
{step === Steps.Completed && (
<ReadyToUse
onSubmit={() => navigate(ROUTES.AccountPaymentAccounts)}
/>
)}
</Container>
</Stack>
</CenteredLayout>
);
}

View File

@ -20,6 +20,8 @@ import { CenteredLayout } from "@templates/CenteredLayout";
import { Button } from "@atoms/Buttons";
import { BodyText, Heading } from "@atoms/Typography";
import { CONTENT_MAX_WIDTH } from "./_constants";
import { Link } from "react-router-dom";
import { ROUTES } from "@constants/routes";
export function Welcome() {
return (
@ -40,8 +42,12 @@ export function Welcome() {
</Stack>
<Space h="lg" />
<Group position="left" sx={{ width: CONTENT_MAX_WIDTH }}>
<Button>Setup Account</Button>
<Button flavor="neutral">Upload Backup</Button>
<Button component={Link} to={ROUTES.CreateAccount}>
Setup Account
</Button>
<Button flavor="neutral" component={Link} to={ROUTES.RestoreBackup}>
Upload Backup
</Button>
</Group>
</Stack>
</CenteredLayout>

View File

@ -14,5 +14,5 @@
// limitations under the License.
// =============================================================================
export * from "./Home";
export * from "./Welcome";
export * from "./CreateAccount";

View File

@ -1,64 +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 { FormEvent } from "react";
import { useEffect, useRef } from "react";
import { Link } from "react-router-dom";
import { ROUTES } from "@constants/routes";
export function Page2() {
const txtUserRef = useRef<HTMLInputElement>(null);
const txtPasswdRef = useRef<HTMLInputElement>(null);
const handleSubmit = (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();
if (!txtUserRef.current || !txtPasswdRef.current) {
return;
}
window.electronStore
.storeUserinfo({
username: txtUserRef.current.value,
password: txtPasswdRef.current.value,
})
.then((value) => {
console.log({ value });
});
};
useEffect(() => {
if (txtUserRef.current) {
window.electronStore.storeUserinfo().then((value) => {
if (txtUserRef.current) {
txtUserRef.current.value = value.username;
}
});
}
}, [txtUserRef.current]);
return (
<div>
<h1>Page 2</h1>
<form onSubmit={handleSubmit}>
<input ref={txtUserRef} type="text" placeholder="username" />
<br />
<input ref={txtPasswdRef} type="password" placeholder="password" />
<button type="submit">Let me in</button>
</form>
<Link to={ROUTES.Home}>Go Home</Link>
</div>
);
}

View File

@ -17,43 +17,6 @@
import type { MantineThemeOverride } from "@mantine/core";
export const themeOverride: MantineThemeOverride = {
fontFamily: "Inter, sans-serif",
fontSizes: {
xl: 18,
lg: 16,
md: 14,
sm: 12,
xs: 10,
},
headings: {
fontFamily: "Inter, sans-serif",
fontWeight: 600,
sizes: {
h1: {
fontSize: "2.25rem",
lineHeight: 1.25,
},
h2: {
fontSize: "1.25rem",
lineHeight: 1.25,
},
h3: {
fontSize: "1.125rem",
lineHeight: 1.25,
},
h4: {
fontSize: "0.875rem",
lineHeight: 1.25,
},
h5: {
fontSize: "0.75rem",
lineHeight: 1.25,
},
},
},
other: {
buttonHeight: 48,
},
colors: {
blue: [
"#E7F1FE",
@ -116,4 +79,43 @@ export const themeOverride: MantineThemeOverride = {
"#fff",
],
},
defaultRadius: 10,
fontFamily: "Inter, sans-serif",
fontSizes: {
xl: 18,
lg: 16,
md: 14,
sm: 12,
xs: 10,
},
headings: {
fontFamily: "Inter, sans-serif",
fontWeight: 600,
sizes: {
h1: {
fontSize: "2.25rem",
lineHeight: 1.25,
},
h2: {
fontSize: "1.25rem",
lineHeight: 1.25,
},
h3: {
fontSize: "1.125rem",
lineHeight: 1.25,
},
h4: {
fontSize: "0.875rem",
lineHeight: 1.25,
},
h5: {
fontSize: "0.75rem",
lineHeight: 1.25,
},
},
},
other: {
buttonHeight: 48,
contentWidthMd: "30rem",
},
};

View File

@ -14,4 +14,4 @@
// limitations under the License.
// =============================================================================
export * from "./store";
export * from "../../../main/src/types/store";

View File

@ -1,64 +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 { Schema } from "electron-store";
export enum StoreKeys {
UserInfo = "UserInfo",
Permissions = "Permissions",
}
// TS types for StoreSchema
export interface IStoreSchema {
[StoreKeys.UserInfo]: IUserInfo;
[StoreKeys.Permissions]: Array<IUserPermission>;
}
export interface IUserInfo {
username: string;
password: Buffer;
}
export type UserInfoInputType = Omit<IUserInfo, "password"> & {
password: string;
};
export interface IUserPermission {
name: string;
}
// this schema is used by electron-store
// must mirror IStoreSchema
export const StoreSchema: Schema<IStoreSchema> = {
[StoreKeys.UserInfo]: {
type: "object",
required: [],
properties: {
username: { type: "string" },
},
},
[StoreKeys.Permissions]: {
type: "array",
default: [],
items: {
type: "object",
required: [],
properties: {
name: { type: "string" },
},
},
},
};

Some files were not shown because too many files have changed in this diff Show More