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 # Editor-based Rest Client
.idea/httpRequests .idea/httpRequests
/.idea/csv-plugin.xml /.idea/csv-plugin.xml
.env

View file

@ -30,7 +30,7 @@ export default {
return Array.from( return Array.from(
filenames.reduce((set, filename) => { filenames.reduce((set, filename) => {
const pack = filename.replace(pathToPackages, "").split(sep)[0]; 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; return set;
}, new 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/react": "^6.4.22",
"@storybook/testing-library": "^0.0.10", "@storybook/testing-library": "^0.0.10",
"@testing-library/react": "^12", "@testing-library/react": "^12",
"@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.182", "@types/lodash": "^4.14.182",
"@types/react": "<18.0.0", "@types/react": "<18.0.0",
"@types/react-dom": "<18.0.0", "@types/react-dom": "<18.0.0",
@ -54,6 +55,7 @@
"@vitejs/plugin-react": "^1.3.0", "@vitejs/plugin-react": "^1.3.0",
"babel-loader": "^8.2.5", "babel-loader": "^8.2.5",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"dotenv": "^16.0.0",
"electron": "17.1.0", "electron": "17.1.0",
"electron-builder": "22.14.13", "electron-builder": "22.14.13",
"electron-devtools-installer": "3.2.0", "electron-devtools-installer": "3.2.0",
@ -82,7 +84,9 @@
"dayjs": "^1.11.0", "dayjs": "^1.11.0",
"electron-store": "^8.0.1", "electron-store": "^8.0.1",
"electron-updater": "4.6.5", "electron-updater": "4.6.5",
"haveno-ts": "0.0.2",
"joi": "^17.6.0", "joi": "^17.6.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "<18.0.0", "react": "<18.0.0",
"react-dom": "<18.0.0", "react-dom": "<18.0.0",

View file

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

View file

@ -17,44 +17,116 @@
import { ipcMain, safeStorage } from "electron"; import { ipcMain, safeStorage } from "electron";
import Store from "electron-store"; import Store from "electron-store";
import type { import type {
AccountInfoDto,
ChangePasswordInput,
IPreferences,
IStoreSchema, IStoreSchema,
IUserInfo, SetPasswordInput,
UserInfoInputType,
IUserPermission,
} from "@src/types"; } 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 }); const store = new Store<IStoreSchema>({ schema: StoreSchema });
export function registerStoreHandlers() { export function registerStoreHandlers() {
ipcMain.handle("store:userinfo", async (_, payload?: UserInfoInputType) => { ipcMain.handle(IpcChannels.SetPassword, async (_, data: SetPasswordInput) => {
const prevData = store.get(StoreKeys.UserInfo); const encryptedPassword = store.get(StorageKeys.AccountInfo_Password);
// retrieve encrypted data like so: if (encryptedPassword) {
// safeStorage.decryptString(Buffer.from(prevData.password)); throw new Error("[[Can't set password]]");
if (!payload) {
return prevData;
} }
const userInfo: IUserInfo = { const hash = await hashPassword(data.newPassword);
...payload, store.set(
// encrypt sensitive data before storage StorageKeys.AccountInfo_Password,
password: safeStorage.encryptString(payload.password), safeStorage.encryptString(hash)
}; );
store.set(StoreKeys.UserInfo, {
...(prevData ?? {}),
...userInfo,
});
return store.get(StoreKeys.UserInfo);
}); });
ipcMain.handle( ipcMain.handle(
"store:permissions", IpcChannels.ChangePassword,
async (_, permissions?: Array<IUserPermission>) => { async (_, data: ChangePasswordInput): Promise<string> => {
const prevData = store.get(StoreKeys.Permissions); const encryptedPassword = store.get(StorageKeys.AccountInfo_Password);
if (!permissions) { if (!encryptedPassword) {
return prevData; throw new Error("[[No password currently set]]");
} }
store.set(StoreKeys.Permissions, [...(prevData || []), ...permissions]); // verify old password
return store.get(StoreKeys.Permissions); 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. // limitations under the License.
// ============================================================================= // =============================================================================
export * from "./ipc";
export * from "./store"; 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"; import type { Schema } from "electron-store";
export enum StoreKeys { export enum StorageKeys {
UserInfo = "UserInfo", AccountInfo_Password = "accounInfo.password",
Permissions = "Permissions", AccountInfo_PrimaryFiat = "accounInfo.primaryFiat",
Preferences_MoneroNode = "preferences.moneroNode",
} }
// TS types for StoreSchema // TS types for StoreSchema
export interface IStoreSchema { export interface IStoreSchema {
[StoreKeys.UserInfo]: IUserInfo; [StorageKeys.AccountInfo_Password]: IAccountInfo["password"];
[StoreKeys.Permissions]: Array<IUserPermission>; [StorageKeys.AccountInfo_PrimaryFiat]: IAccountInfo["primaryFiat"];
[StorageKeys.Preferences_MoneroNode]: IPreferences["moneroNode"]; // TODO: change to object {url, password}
} }
export interface IUserInfo { export interface IAccountInfo {
username: string;
password: Buffer; password: Buffer;
primaryFiat: string;
} }
export type UserInfoInputType = Omit<IUserInfo, "password"> & { export interface AccountInfoDto extends Omit<IAccountInfo, "password"> {
password: string; passwordHash: string;
}; }
export interface IUserPermission { export interface IPreferences {
name: string; moneroNode: string;
} }
// this schema is used by electron-store // this schema is used by electron-store
// must mirror IStoreSchema // must mirror IStoreSchema
export const StoreSchema: Schema<IStoreSchema> = { export const StoreSchema: Schema<IStoreSchema> = {
[StoreKeys.UserInfo]: { [StorageKeys.AccountInfo_Password]: {
type: "object", type: "string",
required: [],
properties: {
username: { type: "string" },
},
}, },
[StoreKeys.Permissions]: { [StorageKeys.AccountInfo_PrimaryFiat]: {
type: "array", type: "string",
default: [], },
items: { [StorageKeys.Preferences_MoneroNode]: {
type: "object", type: "string",
required: [],
properties: {
name: { 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 { ipcRenderer } from "electron";
import { exposeInMainWorld } from "./exposeInMainWorld"; 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 for types in contracts.d.ts
export const store = { export const store = {
storeUserinfo: async (data?: UserInfoInputType) => setPassword: async (data: SetPasswordInput): Promise<void> =>
ipcRenderer.invoke("store:userinfo", data), 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); exposeInMainWorld("electronStore", store);

View file

@ -14,4 +14,4 @@
// limitations under the License. // 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-unused-vars": "error",
"@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/consistent-type-imports": "error",
"prettier/prettier": "error" "prettier/prettier": "error",
"react/jsx-curly-brace-presence": "error"
}, },
"settings": { "settings": {
"react": { "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 charset="UTF-8" />
<meta <meta
http-equiv="Content-Security-Policy" 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" /> <meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>Haveno</title> <title>Haveno</title>

View file

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

View file

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

View file

@ -43,7 +43,6 @@ export function Button<TComponent = "button">(props: ButtonProps<TComponent>) {
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({
common: { common: {
borderRadius: 10,
fontSize: "0.875rem", fontSize: "0.875rem",
fontWeight: 600, fontWeight: 600,
height: theme.other.buttonHeight, 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. // 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. // limitations under the License.
// ============================================================================= // =============================================================================
export const WIDTH = 470; export * from "./Link";

View file

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

View file

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

View file

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

View file

@ -24,11 +24,11 @@ describe("atoms::NodeStatus", () => {
const { asFragment } = render( const { asFragment } = render(
<AppProviders> <AppProviders>
<NodeStatus <NodeStatus
title={"node.moneroworldcom:18089:active"} title="node.moneroworldcom:18089:active"
status={NodeStatusType.Active} status={NodeStatusType.Active}
/> />
<NodeStatus <NodeStatus
title={"node.moneroworldcom:18089:inactive"} title="node.moneroworldcom:18089:inactive"
status={NodeStatusType.Inactive} status={NodeStatusType.Inactive}
/> />
</AppProviders> </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. // limitations under the License.
// ============================================================================= // =============================================================================
export * from "./AccountSecurity"; export * from "./ProtectedRoute";

View file

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

View file

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

View file

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

View file

@ -114,13 +114,13 @@ export function AddPaymentMethod() {
); );
} }
const schema = Joi.object({ const schema = Joi.object<FormValues>({
currency: Joi.string().required(), currency: Joi.string().required(),
paymentMethod: Joi.string().required(), paymentMethod: Joi.string().required(),
accountNumber: Joi.string().required(), accountNumber: Joi.string().required(),
}); });
const Currencies = SupportedCurrencies.map((curr) => ({ const Currencies = SupportedCurrencies.map((curr) => ({
value: curr.id,
label: curr.name, 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" class="mantine-Group-root mantine-mk7hdv"
> >
<button <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" type="submit"
> >
<div <div

View file

@ -17,66 +17,96 @@
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { Stack, Box, Group } from "@mantine/core"; import { Stack, Box, Group } from "@mantine/core";
import { useForm, joiResolver } from "@mantine/form"; import { useForm, joiResolver } from "@mantine/form";
import { showNotification } from "@mantine/notifications";
import { TextInput } from "@atoms/TextInput"; import { TextInput } from "@atoms/TextInput";
import { LangKeys } from "@constants/lang"; import { LangKeys } from "@constants/lang";
import { useAccountSecurityFormSchema } from "./_hooks";
import { Button } from "@atoms/Buttons"; 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 accountSecurityFormSchema = useAccountSecurityFormSchema();
const { mutate: changePassword } = useChangePassword();
const form = useForm({ const form = useForm<ChangePasswordFormValues>({
initialValues: { initialValues: {
currentPassword: "", currentPassword: "",
password: "", newPassword: "",
confirmPassword: "", confirmPassword: "",
}, },
schema: joiResolver(accountSecurityFormSchema), 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 ( return (
<Box> <Box>
<form onSubmit={form.onSubmit((values) => console.log(values))}> <form onSubmit={form.onSubmit(handleSubmit)}>
<Stack spacing="lg"> <Stack spacing="lg">
<TextInput <TextInput
id={"password"} id="password"
type={"password"} type="password"
required required
label={ label={
<FormattedMessage <FormattedMessage
id={LangKeys.AccountSecurityFieldPassword} id={LangKeys.AccountSecurityFieldPassword}
defaultMessage={"Password"} defaultMessage="Password"
/> />
} }
{...form.getInputProps("password")} {...form.getInputProps("newPassword")}
/> />
<TextInput <TextInput
id={"confirmPassword"} id="confirmPassword"
required required
type={"password"} type="password"
label={ label={
<FormattedMessage <FormattedMessage
id={LangKeys.AccountSecurityFieldRepeatPassword} id={LangKeys.AccountSecurityFieldRepeatPassword}
defaultMessage={"Repeat new password"} defaultMessage="Repeat new password"
/> />
} }
{...form.getInputProps("confirmPassword")} {...form.getInputProps("confirmPassword")}
/> />
<TextInput <TextInput
id={"currentPassword"} id="currentPassword"
type={"password"} type="password"
required required
label={ label={
<FormattedMessage <FormattedMessage
id={LangKeys.AccountSecurityFieldCurrentPassword} id={LangKeys.AccountSecurityFieldCurrentPassword}
defaultMessage={"Current password"} defaultMessage="Current password"
/> />
} }
{...form.getInputProps("currentPassword")} {...form.getInputProps("currentPassword")}
/> />
<Group position="right" mt="md"> <Group position="right" mt="md">
<Button size="md" type={"submit"}> <Button size="md" type="submit">
<FormattedMessage id={LangKeys.Save} defaultMessage={"Save"} /> <FormattedMessage id={LangKeys.Save} defaultMessage="Save" />
</Button> </Button>
</Group> </Group>
</Stack> </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. // limitations under the License.
// ============================================================================= // =============================================================================
import { AccountLayout } from "@templates/AccountLayout"; export interface ChangePasswordFormValues {
currentPassword: string;
export function Account() { newPassword: string;
return ( confirmPassword: string;
<AccountLayout>
<h1>Payment accounts</h1>
</AccountLayout>
);
} }

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, AddPaymentMethodButton,
PaymentMethodCard, PaymentMethodCard,
} from "@molecules/PaymentMethodCard"; } from "@molecules/PaymentMethodCard";
import { usePaymentAccounts } from "@hooks/haveno/usePaymentAccounts";
interface PaymentMethodsProps { interface PaymentMethodsProps {
onAdd: () => void; onAdd: () => void;
} }
export function PaymentMethodList({ onAdd }: PaymentMethodsProps) { export function PaymentMethodList({ onAdd }: PaymentMethodsProps) {
const { data: paymentAccounts, isLoading } = usePaymentAccounts();
return ( return (
<Stack spacing="lg"> <Stack spacing="lg">
<Stack sx={{ maxWidth: "32rem" }}> <Stack sx={{ maxWidth: "32rem" }}>
@ -37,15 +40,15 @@ export function PaymentMethodList({ onAdd }: PaymentMethodsProps) {
</BodyText> </BodyText>
</Stack> </Stack>
<Space h="xl" /> <Space h="xl" />
<Group spacing="xl"> {isLoading && <BodyText>Loading accounts ...</BodyText>}
<PaymentMethodCard {!isLoading && paymentAccounts?.length && (
accountId="1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2" <Group spacing="xl">
currency="BTC" {paymentAccounts.map((account) => (
/> <PaymentMethodCard key={account.getId()} data={account} />
<PaymentMethodCard accountId="tqTFn5Au4m4GFg7x" currency="ETH" /> ))}
<PaymentMethodCard accountId="112233" currency="EUR" /> <AddPaymentMethodButton onClick={onAdd} />
<AddPaymentMethodButton onClick={onAdd} /> </Group>
</Group> )}
</Stack> </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 { interface CenteredLayoutProps {
showHeader?: boolean; showHeader?: boolean;
size?: number;
} }
export const CenteredLayout: FC<CenteredLayoutProps> = (props) => { export const CenteredLayout: FC<CenteredLayoutProps> = (props) => {
const { children, showHeader = false } = props; const { children, showHeader = false, size } = props;
return ( return (
<Stack sx={{ width: "100%" }}> <Stack sx={{ width: "100%" }}>
{showHeader && <HeaderWithLogo />} {showHeader && <HeaderWithLogo />}
<Container p="sm" sx={{ display: "flex", flex: 1 }}> <Container p="sm" size={size} sx={{ display: "flex", flex: 1 }}>
{children} {children}
</Container> </Container>
</Stack> </Stack>

View file

@ -23,18 +23,21 @@ export const SupportedCurrencies = [
{ {
id: "BTC", id: "BTC",
name: "Bitcoin", name: "Bitcoin",
fiat: false,
logo: BtcLogo, logo: BtcLogo,
paymentMethods: [PaymentMethodIds.BLOCK_CHAINS_ID], paymentMethods: [PaymentMethodIds.BLOCK_CHAINS_ID],
}, },
{ {
id: "ETH", id: "ETH",
name: "Ethereum", name: "Ethereum",
fiat: false,
logo: EthLogo, logo: EthLogo,
paymentMethods: [PaymentMethodIds.BLOCK_CHAINS_ID], paymentMethods: [PaymentMethodIds.BLOCK_CHAINS_ID],
}, },
{ {
id: "EUR", id: "EUR",
name: "Euro", name: "Euro",
fiat: true,
logo: EurLogo, logo: EurLogo,
paymentMethods: [ paymentMethods: [
// EUR // EUR
@ -84,6 +87,7 @@ export const SupportedCurrencies = [
{ {
id: "USD", id: "USD",
name: "US Dollar", name: "US Dollar",
fiat: true,
logo: EurLogo, logo: EurLogo,
paymentMethods: [ paymentMethods: [
// US // 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", ConnectingToNetwork = "app.connectingToNetwork",
WelcomeToHaveno = "app.welcomeToHaveno", WelcomeToHaveno = "app.welcomeToHaveno",
Save = "app.save", Save = "app.save",
CreatePassword = "onboarding.createPassword",
AccountTitle = "account.title", AccountTitle = "account.title",
AccountSidebarPaymentAccounts = "account.sidebar.paymentAccounts", AccountSidebarPaymentAccounts = "account.sidebar.paymentAccounts",
AccountSidebarSecurity = "account.sidebar.security", AccountSidebarSecurity = "account.sidebar.security",

View file

@ -45,13 +45,14 @@ const LangPackEN: { [key in LangKeys]: string } = {
[LangKeys.AccountNodeStopDeamon]: "Stop deamon", [LangKeys.AccountNodeStopDeamon]: "Stop deamon",
[LangKeys.AccountSettingsAddNode]: "Add a new node", [LangKeys.AccountSettingsAddNode]: "Add a new node",
[LangKeys.AccountSettingsCurrent]: "Current", [LangKeys.AccountSettingsCurrent]: "Current",
[LangKeys.AccountSecurityFieldPassword]: "Password", [LangKeys.AccountSecurityFieldPassword]: "Update account password",
[LangKeys.AccountSecurityFieldRepeatPassword]: "Repeat new password", [LangKeys.AccountSecurityFieldRepeatPassword]: "Repeat new password",
[LangKeys.AccountSecurityFieldCurrentPassword]: "Current password", [LangKeys.AccountSecurityFieldCurrentPassword]: "Current password",
[LangKeys.AccountSecurityFieldPasswordFormatMsg]: [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]: [LangKeys.AccountSecurityFieldRepeatPasswordMatchMsg]:
"Password confirmation doesn't match Password.", "Passwords don't match",
[LangKeys.CreatePassword]: "Create password",
}; };
export default LangPackEN; 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.", "contener al menos {minChars} caracteres, una mayúscula, una minúscula y un número.",
[LangKeys.AccountSecurityFieldRepeatPasswordMatchMsg]: [LangKeys.AccountSecurityFieldRepeatPasswordMatchMsg]:
"La confirmación de la contraseña no coincide con la contraseña.", "La confirmación de la contraseña no coincide con la contraseña.",
[LangKeys.CreatePassword]: "Crear contraseña",
}; };
export default LangPackES; 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 = { export const ROUTES = {
Home: "/", Home: "",
HomeAlias: "/",
Login: "/login",
Welcome: "/onboarding/welcome", Welcome: "/onboarding/welcome",
CreateAccount: "/onboarding/create-account",
RestoreBackup: "/onboarding/restore-backup", RestoreBackup: "/onboarding/restore-backup",
SetupAccount: "/onboarding/setup",
Wallet: "/wallet",
// Account routes. // Account routes
Account: "/account",
AccountPaymentAccounts: "/account/payment-accounts", AccountPaymentAccounts: "/account/payment-accounts",
AccountAddPaymentAccount: "/account/payment-accounts/add",
AccountNodeSettings: "/account/node-settings", AccountNodeSettings: "/account/node-settings",
AccountBackup: "/account/backup", AccountBackup: "/account/backup",
AccountWallet: "/account/wallet", AccountWallet: "/account/wallet",
AccountSecurity: "/account/security", 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. // 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"; import { AccountLayout } from "@templates/AccountLayout";
export function AccountPaymentAccounts() { export function AccountPaymentAccounts() {
const navigate = useNavigate();
return ( return (
<AccountLayout> <AccountLayout>
<h1>Payment accounts</h1> <PaymentMethodList
onAdd={() => navigate(ROUTES.AccountAddPaymentAccount)}
/>
</AccountLayout> </AccountLayout>
); );
} }

View file

@ -17,24 +17,8 @@
import { LangKeys } from "@constants/lang"; import { LangKeys } from "@constants/lang";
import { Stack, Box, createStyles, Group } from "@mantine/core"; import { Stack, Box, createStyles, Group } from "@mantine/core";
import { AccountLayout } from "@templates/AccountLayout"; import { AccountLayout } from "@templates/AccountLayout";
import { AccountSecurityForm } from "./AccountSecurityForm";
import { Heading, BodyText } from "@atoms/Typography"; import { Heading, BodyText } from "@atoms/Typography";
import { WIDTH } from "./_constants"; import { ChangePassword } from "@organisms/ChangePassword";
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>
);
}
export function AccountSecurity() { export function AccountSecurity() {
const { classes } = useStyles(); const { classes } = useStyles();
@ -43,16 +27,26 @@ export function AccountSecurity() {
<AccountLayout> <AccountLayout>
<Box className={classes.content}> <Box className={classes.content}>
<Stack spacing="lg"> <Stack spacing="lg">
<AccountSecurityHeader /> <Group spacing="sm">
<AccountSecurityForm /> <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> </Stack>
</Box> </Box>
</AccountLayout> </AccountLayout>
); );
} }
const useStyles = createStyles(() => ({ const useStyles = createStyles((theme) => ({
content: { content: {
maxWidth: WIDTH, maxWidth: theme.other.contentWidthMd,
}, },
})); }));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,7 +25,7 @@ export function PaymentMethods() {
return ( return (
<NavbarLayout> <NavbarLayout>
<PaymentMethodList <PaymentMethodList
onAdd={() => navigate(ROUTES.AccountAddPaymentMethod)} onAdd={() => navigate(ROUTES.AccountAddPaymentAccount)}
/> />
</NavbarLayout> </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. // limitations under the License.
// ============================================================================= // =============================================================================
export * from "./AddPaymentMethod"; export * from "./AddPaymentAccount";
export * from "./PaymentMethods"; 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. // limitations under the License.
// ============================================================================= // =============================================================================
export const WIDTH = 475; export const CONTENT_MAX_WIDTH = 470;
// The minimum characters that should password field contain.
export const MIN_PASSWORD_CHARS = 8;

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 { Heading } from "@atoms/Typography";
import Logo from "@assets/logo.svg"; import Logo from "@assets/logo.svg";
export function Home() { export function ConnectingMonero() {
return ( return (
<CenteredLayout> <CenteredLayout>
<Stack align="center" justify="center" sx={{ flex: 1 }}> <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 { Button } from "@atoms/Buttons";
import { BodyText, Heading } from "@atoms/Typography"; import { BodyText, Heading } from "@atoms/Typography";
import { CONTENT_MAX_WIDTH } from "./_constants"; import { CONTENT_MAX_WIDTH } from "./_constants";
import { Link } from "react-router-dom";
import { ROUTES } from "@constants/routes";
export function Welcome() { export function Welcome() {
return ( return (
@ -40,8 +42,12 @@ export function Welcome() {
</Stack> </Stack>
<Space h="lg" /> <Space h="lg" />
<Group position="left" sx={{ width: CONTENT_MAX_WIDTH }}> <Group position="left" sx={{ width: CONTENT_MAX_WIDTH }}>
<Button>Setup Account</Button> <Button component={Link} to={ROUTES.CreateAccount}>
<Button flavor="neutral">Upload Backup</Button> Setup Account
</Button>
<Button flavor="neutral" component={Link} to={ROUTES.RestoreBackup}>
Upload Backup
</Button>
</Group> </Group>
</Stack> </Stack>
</CenteredLayout> </CenteredLayout>

View file

@ -14,5 +14,5 @@
// limitations under the License. // limitations under the License.
// ============================================================================= // =============================================================================
export * from "./Home";
export * from "./Welcome"; 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"; import type { MantineThemeOverride } from "@mantine/core";
export const themeOverride: MantineThemeOverride = { 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: { colors: {
blue: [ blue: [
"#E7F1FE", "#E7F1FE",
@ -116,4 +79,43 @@ export const themeOverride: MantineThemeOverride = {
"#fff", "#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. // 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