mirror of
https://github.com/haveno-dex/haveno-ui.git
synced 2025-06-07 14:22:41 -04:00
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:
parent
7bcf36d595
commit
a0c7875391
109 changed files with 2276 additions and 573 deletions
2
.env.example
Normal file
2
.env.example
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
VITE_HAVENO_URL=http://127.0.0.1:8080
|
||||||
|
VITE_HAVENO_PASSWORD=daemon-password
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -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
25
.vscode/tasks.json
vendored
Normal 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
8
.vscode/tscwatch.sh
vendored
Executable 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
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,4 +14,5 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
export * from "./ipc";
|
||||||
export * from "./store";
|
export * from "./store";
|
||||||
|
|
27
packages/main/src/types/ipc.ts
Normal file
27
packages/main/src/types/ipc.ts
Normal 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",
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
84
packages/main/src/utils/password.ts
Normal file
84
packages/main/src/utils/password.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -14,4 +14,4 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export * from "./store";
|
export * from "../../../main/src/types";
|
||||||
|
|
|
@ -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" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -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": {
|
||||||
|
|
3
packages/renderer/assets/unknown.svg
Normal file
3
packages/renderer/assets/unknown.svg
Normal 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 |
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
`;
|
|
@ -14,4 +14,5 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export * from "./Buttons";
|
export * from "./Button";
|
||||||
|
export * from "./TextButton";
|
||||||
|
|
35
packages/renderer/src/components/atoms/Link/Link.tsx
Normal file
35
packages/renderer/src/components/atoms/Link/Link.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -14,4 +14,4 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export const WIDTH = 470;
|
export * from "./Link";
|
|
@ -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" />}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -14,4 +14,4 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export * from "./AccountSecurity";
|
export * from "./ProtectedRoute";
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
You’ve 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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";
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
|
@ -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",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
|
@ -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";
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, it’s 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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";
|
|
@ -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",
|
||||||
|
}),
|
||||||
|
});
|
|
@ -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";
|
|
@ -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,
|
||||||
|
}));
|
|
@ -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";
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
20
packages/renderer/src/constants/haveno-daemon.ts
Normal file
20
packages/renderer/src/constants/haveno-daemon.ts
Normal 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";
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
26
packages/renderer/src/constants/query-keys.ts
Normal file
26
packages/renderer/src/constants/query-keys.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// =============================================================================
|
||||||
|
// Copyright 2022 Haveno
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export enum QueryKeys {
|
||||||
|
HavenoVersion = "Haveno.Version",
|
||||||
|
Balances = "Haveno.Balances",
|
||||||
|
PaymentAccounts = "Haveno.PaymentAccounts",
|
||||||
|
|
||||||
|
StorageAccountInfo = "Storage.AccountInfo",
|
||||||
|
StoragePreferences = "Storage.Preferences",
|
||||||
|
|
||||||
|
AuthSession = "AuthSession",
|
||||||
|
}
|
|
@ -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",
|
|
||||||
};
|
};
|
||||||
|
|
26
packages/renderer/src/hooks/haveno/useBalances.ts
Normal file
26
packages/renderer/src/hooks/haveno/useBalances.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// =============================================================================
|
||||||
|
// Copyright 2022 Haveno
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
import { QueryKeys } from "@constants/query-keys";
|
||||||
|
import { useHavenoClient } from "./useHavenoClient";
|
||||||
|
|
||||||
|
export function useBalances() {
|
||||||
|
const client = useHavenoClient();
|
||||||
|
return useQuery(QueryKeys.Balances, async () => {
|
||||||
|
return client.getBalances();
|
||||||
|
});
|
||||||
|
}
|
29
packages/renderer/src/hooks/haveno/useCreateAccount.ts
Normal file
29
packages/renderer/src/hooks/haveno/useCreateAccount.ts
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
35
packages/renderer/src/hooks/haveno/useHavenoClient.ts
Normal file
35
packages/renderer/src/hooks/haveno/useHavenoClient.ts
Normal 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;
|
||||||
|
}
|
26
packages/renderer/src/hooks/haveno/useHavenoVersion.ts
Normal file
26
packages/renderer/src/hooks/haveno/useHavenoVersion.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// =============================================================================
|
||||||
|
// Copyright 2022 Haveno
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
import { 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();
|
||||||
|
});
|
||||||
|
}
|
36
packages/renderer/src/hooks/haveno/usePaymentAccounts.ts
Normal file
36
packages/renderer/src/hooks/haveno/usePaymentAccounts.ts
Normal 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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
29
packages/renderer/src/hooks/haveno/useSetMoneroConnection.ts
Normal file
29
packages/renderer/src/hooks/haveno/useSetMoneroConnection.ts
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
35
packages/renderer/src/hooks/session/useAuth.ts
Normal file
35
packages/renderer/src/hooks/session/useAuth.ts
Normal 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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
34
packages/renderer/src/hooks/session/useLogin.ts
Normal file
34
packages/renderer/src/hooks/session/useLogin.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
48
packages/renderer/src/hooks/storage/useChangePassword.ts
Normal file
48
packages/renderer/src/hooks/storage/useChangePassword.ts
Normal 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);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
44
packages/renderer/src/hooks/storage/useCreateAccount.ts
Normal file
44
packages/renderer/src/hooks/storage/useCreateAccount.ts
Normal 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);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
26
packages/renderer/src/hooks/storage/useGetAccountInfo.ts
Normal file
26
packages/renderer/src/hooks/storage/useGetAccountInfo.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// =============================================================================
|
||||||
|
// Copyright 2022 Haveno
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
import { QueryKeys } from "@constants/query-keys";
|
||||||
|
import type { AccountInfoDto } from "@src/types";
|
||||||
|
|
||||||
|
export function useAccountInfo() {
|
||||||
|
return useQuery<AccountInfoDto, Error>(
|
||||||
|
QueryKeys.StorageAccountInfo,
|
||||||
|
async () => window.electronStore.getAccountInfo()
|
||||||
|
);
|
||||||
|
}
|
25
packages/renderer/src/hooks/storage/useGetPreferences.ts
Normal file
25
packages/renderer/src/hooks/storage/useGetPreferences.ts
Normal 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()
|
||||||
|
);
|
||||||
|
}
|
36
packages/renderer/src/hooks/storage/useSetPrimaryFiat.ts
Normal file
36
packages/renderer/src/hooks/storage/useSetPrimaryFiat.ts
Normal 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);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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. It’s 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. It’s 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,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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} />}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export function PaymentMethods() {
|
||||||
return (
|
return (
|
||||||
<NavbarLayout>
|
<NavbarLayout>
|
||||||
<PaymentMethodList
|
<PaymentMethodList
|
||||||
onAdd={() => navigate(ROUTES.AccountAddPaymentMethod)}
|
onAdd={() => navigate(ROUTES.AccountAddPaymentAccount)}
|
||||||
/>
|
/>
|
||||||
</NavbarLayout>
|
</NavbarLayout>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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",
|
|
||||||
})
|
|
||||||
),
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -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";
|
||||||
|
|
65
packages/renderer/src/pages/Home/Home.tsx
Normal file
65
packages/renderer/src/pages/Home/Home.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
17
packages/renderer/src/pages/Home/index.ts
Normal file
17
packages/renderer/src/pages/Home/index.ts
Normal 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";
|
95
packages/renderer/src/pages/Login/Login.tsx
Normal file
95
packages/renderer/src/pages/Login/Login.tsx
Normal 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",
|
||||||
|
}),
|
||||||
|
});
|
|
@ -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;
|
|
17
packages/renderer/src/pages/Login/index.ts
Normal file
17
packages/renderer/src/pages/Login/index.ts
Normal 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";
|
|
@ -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 }}>
|
105
packages/renderer/src/pages/Onboarding/CreateAccount.tsx
Normal file
105
packages/renderer/src/pages/Onboarding/CreateAccount.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -14,5 +14,5 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export * from "./Home";
|
|
||||||
export * from "./Welcome";
|
export * from "./Welcome";
|
||||||
|
export * from "./CreateAccount";
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,4 +14,4 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export * from "./store";
|
export * from "../../../main/src/types/store";
|
||||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue