feat: my wallet

- transaction list
- primary address and qr code
- generate sub address
- send and receive xmr

---

Reviewed-by: @schowdhuri
This commit is contained in:
Ahmed Bouhuolia 2022-05-27 22:34:22 +02:00 committed by GitHub
parent 500be9a7fd
commit b40fe25930
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
101 changed files with 4328 additions and 50 deletions

View File

@ -88,6 +88,7 @@
"@mantine/hooks": "^4.1.2",
"@mantine/modals": "^4.1.2",
"@mantine/notifications": "^4.1.2",
"@tanstack/react-table": "^8.0.0-alpha.87",
"dayjs": "^1.11.0",
"electron-store": "^8.0.1",
"electron-updater": "4.6.5",
@ -98,6 +99,7 @@
"react": "<18.0.0",
"react-dom": "<18.0.0",
"react-intl": "^5.24.8",
"react-qr-code": "^2.0.7",
"react-query": "^3.34.19",
"react-router-dom": "6",
"recoil": "^0.7.0",

View File

@ -0,0 +1,3 @@
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.16623 1.03657L1.26729 5.5307C0.815851 5.94484 0.815851 6.63506 1.26729 7.0492C1.71873 7.46333 2.47113 7.46333 2.92256 7.0492L5.83183 4.39566L5.83183 14.2122C5.83183 14.8104 6.35015 15.2859 7.00223 15.2859C7.65431 15.2859 8.17262 14.8104 8.17262 14.2122L8.17262 4.39566L11.0652 7.0492C11.5166 7.46333 12.269 7.46333 12.7204 7.0492C12.9545 6.83446 13.0716 6.55837 13.0716 6.28228C13.0716 6.00619 12.9545 5.7301 12.7204 5.51536L7.83823 1.03657C7.62087 0.837168 7.31991 0.714461 7.00223 0.714461C6.68455 0.714461 6.38359 0.821829 6.16623 1.03657Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 681 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.83401 14.9637L12.733 10.4695C13.1844 10.0554 13.1844 9.36518 12.733 8.95105C12.2815 8.53691 11.5291 8.53691 11.0777 8.95105L8.16841 11.6046L8.16841 1.78804C8.16841 1.18984 7.65009 0.714355 6.99802 0.714355C6.34594 0.714355 5.82762 1.18984 5.82762 1.78804L5.82762 11.6046L2.93507 8.95105C2.48363 8.53691 1.73124 8.53691 1.2798 8.95105C1.04572 9.16578 0.928677 9.44187 0.928677 9.71796C0.928677 9.99405 1.04572 10.2701 1.2798 10.4849L6.16202 14.9637C6.37938 15.1631 6.68034 15.2858 6.99801 15.2858C7.31569 15.2858 7.61665 15.1784 7.83401 14.9637Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 683 B

View File

@ -0,0 +1,11 @@
<svg id="monero-icon" width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2924_33103)">
<path d="M15.9979 0C7.16414 0 -0.0112325 7.17316 1.32008e-05 15.9979C0.00222827 17.7635 0.283798 19.4618 0.812262 21.0505H5.5992V7.59219L15.9979 17.9897L26.3961 7.59219V21.0507H31.1841C31.7133 19.4622 31.9934 17.7638 31.9966 15.9982C32.0117 7.16422 24.8326 0.00212965 15.9979 0.00212965V0Z" fill="#111111"/>
<path d="M13.6084 20.3801L9.0703 15.8423V24.3109H5.60074L2.32678 24.3115C5.13506 28.9184 10.2103 32 15.9998 32C21.7893 32 26.8648 28.9177 29.6736 24.3108H22.9283V15.8423L18.39 20.3801L15.9993 22.7705L13.6086 20.3801H13.6084Z" fill="#111111"/>
</g>
<defs>
<clipPath id="clip0_2924_33103">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 819 B

View File

@ -29,6 +29,7 @@ import {
AddPaymentAccount,
PaymentMethods,
} from "@pages/Account";
import { MyWallet } from "@pages/MyWallet";
export function AppRoutes() {
return (
@ -37,6 +38,14 @@ export function AppRoutes() {
<Route path={ROUTES.Login} element={<Login />} />
<Route path={ROUTES.Welcome} element={<Welcome />} />
<Route path={ROUTES.CreateAccount} element={<CreateAccount />} />
<Route
path={ROUTES.MyWallet}
element={
<ProtectedRoute>
<MyWallet />
</ProtectedRoute>
}
/>
<Route
path={ROUTES.PaymentAccounts}
element={

View File

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

View File

@ -0,0 +1,46 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { Stack } from "@mantine/core";
import { CircleIcon } from "./CircleIcon";
import { ReactComponent as ArrowNorth } from "@assets/arrow-north.svg";
import { ReactComponent as ArrowWest } from "@assets/arrow-west.svg";
export default {
title: "atoms/CircleIcon",
component: CircleIcon,
} as ComponentMeta<typeof CircleIcon>;
const Template: ComponentStory<typeof CircleIcon> = ({ ...props }) => {
return (
<Stack>
<CircleIcon {...props}>
<ArrowNorth />
</CircleIcon>
<CircleIcon {...props}>
<ArrowWest />
</CircleIcon>
</Stack>
);
};
export const Default = Template.bind({});
Default.args = {
color: "#75B377",
};

View File

@ -0,0 +1,31 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it } from "vitest";
import { render } from "@testing-library/react";
import { CircleIcon } from "./CircleIcon";
import { ReactComponent as ArrowNorth } from "@assets/arrow-north.svg";
describe("atoms::CircleIcon", () => {
it("renders without exploding", () => {
const { asFragment } = render(
<CircleIcon color="#0B65DA">
<ArrowNorth />
</CircleIcon>
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,65 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { DefaultProps } from "@mantine/core";
import { Box, createStyles } from "@mantine/core";
export interface CircleIconProps extends DefaultProps {
color?: string;
size?: string;
children: React.ReactNode;
}
type CircleIconStyle = Pick<CircleIconProps, "color" | "size">;
export function CircleIcon({
children,
classNames,
className,
color,
size,
...otherProps
}: CircleIconProps) {
const { classes, cx } = useStyles(
{ color, size },
{
name: "CircleIcon",
classNames,
}
);
return (
<Box className={cx(classes.root, className)} {...otherProps}>
{children}
</Box>
);
}
const useStyles = createStyles((theme, { color, size }: CircleIconStyle) => ({
root: {
borderRadius: "50%",
backgroundColor: "rgba(0, 0, 0, 0.05)",
color: color || theme.colors.gray[9],
display: "flex",
height: size || 34,
lineHeight: 1,
width: size || 34,
svg: {
margin: "auto",
},
},
}));

View File

@ -0,0 +1,22 @@
// Vitest Snapshot v1
exports[`atoms::CircleIcon > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-CircleIcon-root mantine-hg5ly7"
>
<svg
fill="none"
height="1em"
viewBox="0 0 14 16"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.16623 1.03657L1.26729 5.5307C0.815851 5.94484 0.815851 6.63506 1.26729 7.0492C1.71873 7.46333 2.47113 7.46333 2.92256 7.0492L5.83183 4.39566L5.83183 14.2122C5.83183 14.8104 6.35015 15.2859 7.00223 15.2859C7.65431 15.2859 8.17262 14.8104 8.17262 14.2122L8.17262 4.39566L11.0652 7.0492C11.5166 7.46333 12.269 7.46333 12.7204 7.0492C12.9545 6.83446 13.0716 6.55837 13.0716 6.28228C13.0716 6.00619 12.9545 5.7301 12.7204 5.51536L7.83823 1.03657C7.62087 0.837168 7.31991 0.714461 7.00223 0.714461C6.68455 0.714461 6.38359 0.821829 6.16623 1.03657Z"
fill="currentColor"
/>
</svg>
</div>
</DocumentFragment>
`;

View File

@ -0,0 +1,40 @@
// =============================================================================
// 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 } from "@mantine/core";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { DetailItem } from "./DetailItem";
export default {
title: "atoms/DetailItem",
component: DetailItem,
} as ComponentMeta<typeof DetailItem>;
const Template: ComponentStory<typeof DetailItem> = ({ ...props }) => {
return (
<Stack spacing="xl">
<DetailItem {...props} />
</Stack>
);
};
export const Default = Template.bind({});
Default.args = {
label: "RECEIPIENT ADDRESS",
children: "a1b848fdf7fb77f1dae266331d23c522db267ced63566a6e35800421c988d9f1",
textAlign: "left",
};

View File

@ -0,0 +1,43 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import { DetailItem } from "./DetailItem";
describe("atoms::DetailItem", () => {
it("renders without exploding", () => {
const { asFragment, unmount } = render(
<DetailItem label="Label">Content</DetailItem>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("renders detail label.", () => {
const { unmount } = render(<DetailItem label="Label">Content</DetailItem>);
expect(screen.queryByText("Label")).toBeInTheDocument();
unmount();
});
it("renders detail content.", () => {
const { unmount } = render(<DetailItem>Content here ...</DetailItem>);
expect(screen.queryByText("Content here ...")).toBeInTheDocument();
unmount();
});
});

View File

@ -0,0 +1,67 @@
// =============================================================================
// 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 { DefaultProps } from "@mantine/core";
import { Stack, createStyles, Text } from "@mantine/core";
import { BodyText } from "@atoms/Typography";
export interface DetailItemProps extends DefaultProps {
label?: string;
textAlign?: "left" | "right";
children: React.ReactNode | string;
}
type DetailItemStyleProps = Pick<DetailItemProps, "textAlign">;
export function DetailItem({
label,
children,
classNames,
className,
textAlign,
...other
}: DetailItemProps) {
const { classes, cx } = useStyles(
{ textAlign },
{ name: "DetailItem", classNames }
);
return (
<Stack spacing={0} className={cx(classes.root, className)} {...other}>
{label && <Text className={classes.label}>{label}</Text>}
<BodyText heavy className={classes.content}>
{children}
</BodyText>
</Stack>
);
}
const useStyles = createStyles(
(theme, { textAlign }: DetailItemStyleProps) => ({
root: {
textAlign: textAlign || undefined,
},
label: {
color: theme.colors.gray[6],
fontSize: theme.fontSizes.sm,
fontWeight: 600,
letterSpacing: "0.075rem",
textTransform: "uppercase",
},
content: {
fontWeight: 500,
},
})
);

View File

@ -0,0 +1,20 @@
// Vitest Snapshot v1
exports[`atoms::DetailItem > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-Stack-root mantine-DetailItem-root mantine-fpp2be"
>
<div
class="mantine-Text-root mantine-DetailItem-label mantine-1mkytp"
>
Label
</div>
<div
class="mantine-Text-root mantine-DetailItem-content mantine-17mq3ni"
>
Content
</div>
</div>
</DocumentFragment>
`;

View File

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

View File

@ -0,0 +1,40 @@
// =============================================================================
// 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 } from "@mantine/core";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { DetailItemCard } from "./DetailItemCard";
export default {
title: "atoms/DetailItemCard",
component: DetailItemCard,
} as ComponentMeta<typeof DetailItemCard>;
const Template: ComponentStory<typeof DetailItemCard> = (props) => {
return (
<Stack>
<DetailItemCard {...props} />
</Stack>
);
};
export const Default = Template.bind({});
Default.args = {
label: "Primary Label",
children: "Content here",
primary: false,
};

View File

@ -0,0 +1,62 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { createStyles, Group } from "@mantine/core";
import type { DetailItemProps } from "@atoms/DetailItem";
import { DetailItem } from "@atoms/DetailItem";
export interface DetailItemCardProps extends DetailItemProps {
primary?: boolean;
}
interface DetailItemCardStyleProps {
primary?: boolean;
}
const useStyles = createStyles(
(theme, { primary }: DetailItemCardStyleProps) => ({
root: {
background: primary ? theme.colors.gray[2] : theme.white,
borderRadius: theme.radius.md,
border: `1px solid ${
primary ? theme.colors.gray[2] : theme.colors.gray[3]
}`,
paddingTop: theme.spacing.sm,
paddingBottom: theme.spacing.sm,
paddingLeft: theme.spacing.md,
paddingRight: theme.spacing.md,
},
detailRoot: {
width: "100%",
},
})
);
export function DetailItemCard({
label,
primary,
children,
}: DetailItemCardProps) {
const { classes } = useStyles({ primary });
return (
<Group className={classes.root}>
<DetailItem label={label} classNames={{ root: classes.detailRoot }}>
{children}
</DetailItem>
</Group>
);
}

View File

@ -0,0 +1,57 @@
// =============================================================================
// 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 { useState } from "react";
import { Group, Text } from "@mantine/core";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { Modal } from "./Modal";
import { Button } from "@atoms/Buttons";
export default {
title: "atoms/Modal",
component: Modal,
} as ComponentMeta<typeof Modal>;
const Template: ComponentStory<typeof Modal> = ({ ...props }) => {
const [opened, setOpened] = useState(false);
return (
<>
<Modal
{...props}
opened={opened}
onClose={() => setOpened(false)}
title="Funds are sent!"
>
<Text color="gray">
Youve sent 1.15 XMR to:
44tgg3TkQ2jGRDabB5cjbNWDF7PKDBKqw2bsjgRRCQSThiE15ePWk6kJFH7YWnPKR88JQB8WwDX34TwfYnhWVeT1J1rC6b7
</Text>
<Group mt="lg">
<Button>Go to Wallet</Button>
</Group>
</Modal>
<Group position="center">
<Button onClick={() => setOpened(true)}>Open Modal</Button>
</Group>
</>
);
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,55 @@
// =============================================================================
// 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 { ModalProps as MModalProps } from "@mantine/core";
import { createStyles, Modal as MModal } from "@mantine/core";
import type { ModalsProviderProps } from "@mantine/modals";
import { ModalsProvider as MModalsProvider } from "@mantine/modals";
const commonModalProps = {
overlayOpacity: 0.25,
padding: 25,
};
export function Modal(props: MModalProps) {
const style = useStyles();
return <MModal classNames={style.classes} {...commonModalProps} {...props} />;
}
export function ModalsProvider({ ...props }: ModalsProviderProps) {
const style = useStyles();
return (
<MModalsProvider
modalProps={{
classNames: style.classes,
...commonModalProps,
}}
{...props}
/>
);
}
const useStyles = createStyles((theme) => ({
modal: {
borderRadius: theme.radius.xl / 1.6,
},
title: {
fontSize: theme.fontSizes.xl,
fontWeight: 600,
},
}));

View File

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

View File

@ -9,7 +9,7 @@ exports[`atoms::Sync Status > renders the fully synced status 1`] = `
class="mantine-Group-child mantine-16wvh2m"
/>
<div
class="mantine-Text-root mantine-Group-child mantine-1mga3um"
class="mantine-Text-root mantine-Group-child mantine-6ffw9j"
>
Fully Synced
</div>
@ -26,7 +26,7 @@ exports[`atoms::Sync Status > renders the not synced status by default 1`] = `
class="mantine-Group-child mantine-1mmj0t1"
/>
<div
class="mantine-Text-root mantine-Group-child mantine-1mga3um"
class="mantine-Text-root mantine-Group-child mantine-6ffw9j"
>
Not Synced
</div>
@ -43,7 +43,7 @@ exports[`atoms::Sync Status > renders the sync in progress status 1`] = `
class="mantine-Group-child mantine-6c6x3l"
/>
<div
class="mantine-Text-root mantine-Group-child mantine-1mga3um"
class="mantine-Text-root mantine-Group-child mantine-6ffw9j"
>
Syncing
</div>

View File

@ -0,0 +1,37 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { Tabs } from "./Tabs";
export default {
title: "atoms/Tabs",
component: Tabs,
} as ComponentMeta<typeof Tabs>;
const Template: ComponentStory<typeof Tabs> = () => {
return (
<Tabs>
<Tabs.Tab label="Gallery">Gallery tab content</Tabs.Tab>
<Tabs.Tab label="Messages">Messages tab content</Tabs.Tab>
<Tabs.Tab label="Settings">Settings tab content</Tabs.Tab>
</Tabs>
);
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,60 @@
// =============================================================================
// 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 { TabsProps as MTabsProps } from "@mantine/core";
import { Tabs as MTabs, Tab as MTab, createStyles } from "@mantine/core";
export function Tabs(props: MTabsProps) {
const style = useStyles();
return <MTabs classNames={style.classes} variant="unstyled" {...props} />;
}
Tabs.Tab = MTab;
const useStyles = createStyles((theme, _params, getRef) => {
const tabActiveRef = getRef("tabActive");
return {
tabsList: {
borderBottom: `2px solid ${theme.colors.gray[3]}`,
},
tabControl: {
borderBottom: "2px solid transparent",
color: theme.colors.gray[6],
fontWeight: 700,
height: 32,
letterSpacing: 0.8,
marginBottom: -2,
marginLeft: theme.spacing.md,
marginRight: theme.spacing.md,
paddingLeft: 0,
paddingRight: 0,
textTransform: "uppercase",
"&:first-child": {
marginLeft: 0,
},
"&:last-child": {
marginRight: 0,
},
[`&.${tabActiveRef}`]: {
borderBottomColor: theme.primaryColor,
color: theme.colors.dark[9],
},
},
};
});

View File

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

View File

@ -36,11 +36,14 @@ export function BodyText<TComponent = "p">(props: BodyTextProps<TComponent>) {
return (
<MText
{...rest}
className={cx(className, {
[classes.body]: !size || size === "md",
[classes.bodyLg]: size === "lg",
[classes.bodyHeavy]: Boolean(heavy),
})}
className={cx(
{
[classes.body]: !size || size === "md",
[classes.bodyLg]: size === "lg",
[classes.bodyHeavy]: Boolean(heavy),
},
className
)}
size={size}
>
{stringId ? (

View File

@ -0,0 +1,50 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { Stack } from "@mantine/core";
import { AddressCard } from "./AddressCard";
export default {
title: "molecules/AddressCard",
component: AddressCard,
} as ComponentMeta<typeof AddressCard>;
const Template: ComponentStory<typeof AddressCard> = (args) => {
return (
<Stack>
<AddressCard
{...args}
primary={true}
qrModalProps={{ target: "#root" }}
/>
<AddressCard
{...args}
primary={false}
qrModalProps={{ target: "#root" }}
/>
</Stack>
);
};
export const Default = Template.bind({});
Default.args = {
label: "Primary Address",
primary: true,
address:
"44tgg3TkQ2jGRDabB5cjbNWDF7PKDBKqw2bsjgRRCQSThiE15ePWk6kJFH7YWnPKR88JQB8WwDX34TwfYnhWVeT1J1rC6b7",
};

View File

@ -0,0 +1,52 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import { AddressCard } from "./AddressCard";
import { AppProviders } from "@atoms/AppProviders";
describe("molecules::AddressCard", () => {
it("renders without exploding", () => {
const { asFragment, unmount } = render(
<AppProviders>
<AddressCard address="Address here" label="Address label" />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("renderes the address hash code.", () => {
const { unmount } = render(
<AppProviders>
<AddressCard address="ASDASDQWEKOOQLMWEM" />
</AppProviders>
);
expect(screen.queryByText("ASDASDQWEKOOQLMWEM")).toMatchSnapshot();
unmount();
});
it("renderes the address label.", () => {
const { unmount } = render(
<AppProviders>
<AddressCard label="Primary Address" address="ASDASDQWEKOOQLMWEM" />
</AppProviders>
);
expect(screen.queryByText("Primary Address")).toMatchSnapshot();
unmount();
});
});

View File

@ -0,0 +1,207 @@
// =============================================================================
// 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 { FormattedMessage, useIntl } from "react-intl";
import QRCode from "react-qr-code";
import type { OpenConfirmModal } from "@mantine/modals/lib/context";
import { useModals } from "@mantine/modals";
import { useClipboard } from "@mantine/hooks";
import {
Anchor,
Box,
createStyles,
Group,
SimpleGrid,
Skeleton,
} from "@mantine/core";
import { DetailItem } from "@atoms/DetailItem";
import { Button } from "@atoms/Buttons";
import { LangKeys } from "@constants/lang";
import { DetailItemCard } from "@atoms/DetailItemCard/DetailItemCard";
interface AddressCardProps {
label?: string;
address: string;
primary?: boolean;
qrModalProps?: OpenConfirmModal;
}
const COPY_TEXT_TIMEOUT = 500;
export function AddressCard({
label,
address,
primary = false,
qrModalProps,
}: AddressCardProps) {
const modals = useModals();
const { classes } = useStyles();
const clipboard = useClipboard({ timeout: COPY_TEXT_TIMEOUT });
const handleCopyClick = () => {
clipboard.copy(address);
};
const handleQRClick = () => {
const modalId = modals.openModal({
children: (
<AddressCardQRModalContent
address={address}
onReturnClick={() => modals.closeModal(modalId)}
/>
),
labels: { confirm: "Confirm", cancel: "Cancel" },
size: "lg",
withCloseButton: false,
...qrModalProps,
});
};
return (
<DetailItemCard label={label} primary={primary}>
<Group noWrap className={classes.contentGroup}>
<Box className={classes.address}>{address}</Box>
<Group noWrap className={classes.addressBtns}>
<Anchor onClick={handleCopyClick} underline>
{!clipboard.copied ? (
<FormattedMessage
id={LangKeys.AddressCardCopyBtn}
defaultMessage="Copy"
/>
) : (
<FormattedMessage
id={LangKeys.AddressCardCopiedBtn}
defaultMessage="Copied"
/>
)}
</Anchor>
<Anchor onClick={handleQRClick} underline>
<FormattedMessage
id={LangKeys.AddressCardQRBtn}
defaultMessage="QR"
/>
</Anchor>
</Group>
</Group>
</DetailItemCard>
);
}
type AddressCardSkeletonProps = Pick<AddressCardProps, "label" | "primary">;
export function AddressCardSkeleton({
label,
primary,
}: AddressCardSkeletonProps) {
const { classes } = useStyles();
return (
<DetailItemCard label={label} primary={primary}>
<Box className={classes.address}>
<Skeleton
width="80%"
height={8}
mt="xs"
sx={(theme) => ({
"&:before": {
backgroundColor: primary
? theme.colors.gray[1]
: theme.colors.gray[0],
},
"&:after": {
backgroundColor: primary
? theme.colors.gray[4]
: theme.colors.gray[3],
},
})}
/>
</Box>
</DetailItemCard>
);
}
interface AddressCardQRModalContentProps {
address: string;
onQRDownloadClick?: () => void;
onReturnClick?: () => void;
}
function AddressCardQRModalContent({
address,
onQRDownloadClick,
onReturnClick,
}: AddressCardQRModalContentProps) {
const { classes } = useStyles();
const { formatMessage } = useIntl();
return (
<Box>
<DetailItem
classNames={{ content: classes.qrModalAddress }}
label={formatMessage({
id: LangKeys.MyWalletQRModalPrimaryAddress,
defaultMessage: "Primary Address",
})}
>
{address}
</DetailItem>
<Box className={classes.qrRoot}>
<QRCode value={address} size={370} />
</Box>
<SimpleGrid cols={2} mt="xl">
<Button flavor="neutral" onClick={onReturnClick}>
<FormattedMessage
id={LangKeys.MyWalletQRModalReturnBtn}
defaultMessage="Return"
/>
</Button>
<Button onClick={onQRDownloadClick}>
<FormattedMessage
id={LangKeys.MyWalletQRModalDownloadQRBtn}
defaultMessage="Download QR"
/>
</Button>
</SimpleGrid>
</Box>
);
}
const useStyles = createStyles((theme) => ({
contentGroup: {
minWidth: "100%",
},
address: {
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
width: "100%",
},
addressBtns: {
marginLeft: "auto",
},
qrRoot: {
marginTop: theme.spacing.xl,
textAlign: "center",
},
qrModalAddress: {
fontSize: theme.fontSizes.lg,
},
}));

View File

@ -0,0 +1,40 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { Stack } from "@mantine/core";
import { AddressCardSkeleton } from "./AddressCard";
export default {
title: "molecules/AddressCardSkeleton",
component: AddressCardSkeleton,
} as ComponentMeta<typeof AddressCardSkeleton>;
const Template: ComponentStory<typeof AddressCardSkeleton> = (args) => {
return (
<Stack>
<AddressCardSkeleton {...args} primary={false} />
<AddressCardSkeleton {...args} primary={true} />
</Stack>
);
};
export const Default = Template.bind({});
Default.args = {
label: "Primary Address",
primary: true,
};

View File

@ -0,0 +1,62 @@
// Vitest Snapshot v1
exports[`molecules::AddressCard > renderes the address hash code. 1`] = `
<div
class="mantine-Group-child mantine-2w2ufd"
>
ASDASDQWEKOOQLMWEM
</div>
`;
exports[`molecules::AddressCard > renderes the address label. 1`] = `
<div
class="mantine-Text-root mantine-DetailItem-label mantine-10gsq91"
>
Primary Address
</div>
`;
exports[`molecules::AddressCard > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-Group-root mantine-chiw4t"
>
<div
class="mantine-Stack-root mantine-DetailItem-root mantine-Group-child mantine-1a4fzbd"
>
<div
class="mantine-Text-root mantine-DetailItem-label mantine-10gsq91"
>
Address label
</div>
<div
class="mantine-Text-root mantine-DetailItem-content mantine-oat2gy"
>
<div
class="mantine-Group-root mantine-1i932k4"
>
<div
class="mantine-Group-child mantine-2w2ufd"
>
Address here
</div>
<div
class="mantine-Group-root mantine-Group-child mantine-ze53qg"
>
<a
class="mantine-Text-root mantine-Anchor-root mantine-Group-child mantine-16zvpw1"
>
Copy
</a>
<a
class="mantine-Text-root mantine-Anchor-root mantine-Group-child mantine-16zvpw1"
>
QR
</a>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

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

View File

@ -0,0 +1,123 @@
// =============================================================================
// 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 { createTable } from "@tanstack/react-table";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { Table } from "./Table";
export default {
title: "atoms/Table",
component: Table,
} as ComponentMeta<typeof Table>;
const Template: ComponentStory<typeof Table> = () => {
return (
<Table
table={table}
data={data}
columns={columns}
rowSubComponent={() => "asdasd"}
/>
);
};
export const Default = Template.bind({});
Default.args = {};
interface Person {
firstName: string;
lastName: string;
age: number;
visits: number;
status: string;
progress: number;
}
const table = createTable().setRowType<Person>();
const columns = [
table.createGroup({
header: "Name",
footer: (props) => props.column.id,
columns: [
table.createDataColumn("firstName", {
cell: (info) => info.getValue(),
footer: (props) => props.column.id,
}),
table.createDataColumn((row) => row.lastName, {
id: "lastName",
cell: (info) => info.getValue(),
header: () => <span>Last Name</span>,
footer: (props) => props.column.id,
}),
],
}),
table.createGroup({
header: "Info",
footer: (props) => props.column.id,
columns: [
table.createDataColumn("age", {
header: () => "Age",
footer: (props) => props.column.id,
}),
table.createGroup({
header: "More Info",
columns: [
table.createDataColumn("visits", {
header: () => <span>Visits</span>,
footer: (props) => props.column.id,
}),
table.createDataColumn("status", {
header: "Status",
footer: (props) => props.column.id,
}),
table.createDataColumn("progress", {
header: "Profile Progress",
footer: (props) => props.column.id,
}),
],
}),
],
}),
];
const data: Array<Person> = [
{
firstName: "tanner",
lastName: "linsley",
age: 24,
visits: 100,
status: "In Relationship",
progress: 50,
},
{
firstName: "tandy",
lastName: "miller",
age: 40,
visits: 40,
status: "Single",
progress: 80,
},
{
firstName: "joe",
lastName: "dirte",
age: 45,
visits: 20,
status: "Complicated",
progress: 10,
},
];

View File

@ -0,0 +1,99 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import { createTable } from "@tanstack/react-table";
import { Table } from "./Table";
describe("molecules::Table", () => {
it("renders without exploding.", () => {
const { asFragment, unmount } = render(
<Table table={table} data={data} columns={columns} />
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("renders all columns.", () => {
const { unmount } = render(
<Table table={table} data={data} columns={columns} />
);
expect(screen.queryByText("Name")).toBeInTheDocument();
expect(screen.queryByText("First Name")).toBeInTheDocument();
expect(screen.queryByText("Last Name")).toBeInTheDocument();
unmount();
});
it("shouldn't render columns if `showHeader` was true.", () => {
const { unmount } = render(
<Table table={table} data={data} columns={columns} showHeader={false} />
);
expect(screen.queryByText("Name")).not.toBeInTheDocument();
expect(screen.queryByText("First Name")).not.toBeInTheDocument();
expect(screen.queryByText("Last Name")).not.toBeInTheDocument();
unmount();
});
it("renders all columns.", () => {
const { unmount } = render(
<Table table={table} data={data} columns={columns} />
);
expect(screen.queryByText("Ahmed")).toBeInTheDocument();
expect(screen.queryByText("Subir")).toBeInTheDocument();
expect(screen.queryByText("In Relationship")).not.toBeInTheDocument();
unmount();
});
});
type Person = {
firstName: string;
lastName: string;
age: number;
visits: number;
status: string;
progress: number;
};
const table = createTable().setRowType<Person>();
const data: Array<Person> = [
{
firstName: "Ahmed",
lastName: "Subir",
age: 24,
visits: 100,
status: "In Relationship",
progress: 50,
},
];
const columns = [
table.createGroup({
header: "Name",
footer: (props) => props.column.id,
columns: [
table.createDataColumn("firstName", {
header: "First Name",
footer: (props) => props.column.id,
}),
table.createDataColumn("lastName", {
id: "lastName",
header: "Last Name",
footer: (props) => props.column.id,
}),
],
}),
];

View File

@ -0,0 +1,51 @@
// =============================================================================
// 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 {
useTableInstance,
getCoreRowModel,
getExpandedRowModel,
} from "@tanstack/react-table";
import { Table as MTable } from "@mantine/core";
import type { TableProps } from "./_types";
import { TableProvider } from "./use-table-context";
import { TableHeader } from "./TableHeader";
import { TableBody } from "./TableBody";
export function Table(props: TableProps) {
const { table, columns, data, tableWrap } = props;
const tableInstance = useTableInstance(table, {
data,
columns,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
});
return (
<MTable {...tableWrap}>
<TableProvider value={{ table: tableInstance, props }}>
<TableHeader />
<TableBody />
</TableProvider>
</MTable>
);
}
Table.defaultProps = {
showHeader: true,
showFooter: true,
};

View File

@ -0,0 +1,58 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { Fragment } from "react";
import { useTableContext } from "./use-table-context";
export function TableBody() {
const {
table,
props: { rowSubComponent },
} = useTableContext();
return (
<tbody>
{table.getRowModel().rows.map((row) => (
<Fragment key={row.id}>
<tr
key={row.id}
onClick={() => {
row.toggleExpanded();
}}
>
{row.getVisibleCells().map((cell) => (
<td
key={cell.id}
style={{
width: cell.column.getSize(),
}}
>
{cell.renderCell()}
</td>
))}
</tr>
{row.getIsExpanded() && rowSubComponent ? (
<tr key={`${row.id}-subcomponent`}>
<td colSpan={row.getVisibleCells()?.length}>
{rowSubComponent({ row })}
</td>
</tr>
) : null}
</Fragment>
))}
</tbody>
);
}

View File

@ -0,0 +1,35 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useTableContext } from "./use-table-context";
export function TableFooter() {
const { table } = useTableContext();
return (
<tfoot>
{table.getFooterGroups().map((footerGroup) => (
<tr key={footerGroup.id}>
{footerGroup.headers.map((header) => (
<th key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder ? null : header.renderFooter()}
</th>
))}
</tr>
))}
</tfoot>
);
}

View File

@ -0,0 +1,48 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useTableContext } from "./use-table-context";
export function TableHeader() {
const {
table,
props: { showHeader },
} = useTableContext();
if (!showHeader) {
return null;
}
return (
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
colSpan={header.colSpan}
style={{
width: header.getSize(),
}}
>
{header.isPlaceholder ? null : header.renderHeader()}
</th>
))}
</tr>
))}
</thead>
);
}

View File

@ -0,0 +1,48 @@
// Vitest Snapshot v1
exports[`molecules::Table > renders without exploding. 1`] = `
<DocumentFragment>
<table
class="mantine-Table-root mantine-1uvqbeb"
>
<thead>
<tr>
<th
colspan="2"
style="width: 300px;"
>
Name
</th>
</tr>
<tr>
<th
colspan="1"
style="width: 150px;"
>
First Name
</th>
<th
colspan="1"
style="width: 150px;"
>
Last Name
</th>
</tr>
</thead>
<tbody>
<tr>
<td
style="width: 150px;"
>
Ahmed
</td>
<td
style="width: 150px;"
>
Subir
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;

View File

@ -0,0 +1,32 @@
// =============================================================================
// 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.
// =============================================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ColumnDef, Row } from "@tanstack/react-table";
import type { TableProps as MTableProps } from "@mantine/core";
export interface TableProps {
columns: Array<ColumnDef<any>>;
table: any;
data: Array<any>;
showHeader?: boolean;
showFooter?: boolean;
rowSubComponent?: ({ row }: { row: Row<any> }) => React.ReactNode;
tableWrap?: MTableProps;
}

View File

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

View File

@ -0,0 +1,43 @@
// =============================================================================
// 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.
// =============================================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createContext, useContext } from "react";
import type { TableInstance } from "@tanstack/react-table";
import type { TableProps } from "../_types";
interface TableContextValue {
table: TableInstance<any>;
props: TableProps;
}
interface TableProviderProps {
value: TableContextValue;
children: React.ReactNode;
}
export const TableContext = createContext<TableContextValue>(
{} as TableContextValue
);
export function TableProvider({ children, value }: TableProviderProps) {
return (
<TableContext.Provider value={value}>{children}</TableContext.Provider>
);
}
export const useTableContext = () =>
useContext<TableContextValue>(TableContext);

View File

@ -21,14 +21,14 @@ import { WalletBalance } from ".";
describe("molecules::WalletBalance", () => {
beforeAll(() => {
vi.mock("@hooks/haveno/useHavenoClient", () => ({
useHavenoClient: () => ({
getBalances: async () => {
return {
getLockedBalance: () => 12,
getReservedTradeBalance: () => 14,
getBalance: () => 15,
};
vi.mock("@hooks/haveno/useBalances", () => ({
useBalances: () => ({
isLoading: false,
isSuccess: true,
data: {
lockedBalance: 12,
reservedTradeBalance: 14,
balance: 15,
},
}),
}));
@ -53,16 +53,17 @@ describe("molecules::WalletBalance", () => {
});
it("renders loading state", () => {
const { asFragment } = render(
const { asFragment, unmount } = render(
<AppProviders>
<WalletBalance />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("renders after loading data", async () => {
const { asFragment, queryByText } = render(
const { asFragment, queryByText, unmount } = render(
<AppProviders>
<WalletBalance />
</AppProviders>
@ -71,5 +72,6 @@ describe("molecules::WalletBalance", () => {
await waitForElementToBeRemoved(() => queryByText("Loading..."));
}
expect(asFragment()).toMatchSnapshot();
unmount();
});
});

View File

@ -34,18 +34,15 @@ import { BodyText } from "@atoms/Typography";
export function WalletBalance() {
const [isOpen, setOpen] = useState(false);
const { classes } = useStyles({ isOpen });
const { data: availableBalances, isLoading: isLoadingBalance } =
useBalances();
const { data: balances, isLoading: isLoadingBalance } = useBalances();
const { data: accountInfo, isLoading: isLoadingAccountInfo } =
useAccountInfo();
const { data: price } = usePrice(accountInfo?.primaryFiat);
const totalBalance = useMemo(() => {
return (
Number(availableBalances?.getLockedBalance() || 0) +
Number(availableBalances?.getReservedTradeBalance() || 0)
);
}, [availableBalances]);
return balances?.balance || 0 + (balances?.reservedTradeBalance || 0);
}, [balances]);
const fiatBalance = useMemo(() => {
if (!totalBalance || !price || !accountInfo?.primaryFiat) {
@ -76,9 +73,7 @@ export function WalletBalance() {
<Stack spacing={4}>
<Group>
<Text className={classes.xmr}>
<Currency
value={Number(availableBalances?.getBalance() ?? 0)}
/>
<Currency value={Number(balances?.balance || 0)} />
</Text>
<ArrowDown className={classes.toggleIcon} />
</Group>
@ -102,19 +97,13 @@ export function WalletBalance() {
<Stack spacing={4}>
<Text className={classes.balanceLabel}>Reserved</Text>
<Text className={classes.balanceValue}>
<Currency
value={Number(
availableBalances?.getReservedTradeBalance() ?? 0
)}
/>
<Currency value={balances?.reservedTradeBalance || 0} />
</Text>
</Stack>
<Stack spacing={4}>
<Text className={classes.balanceLabel}>Locked</Text>
<Text className={classes.balanceValue}>
<Currency
value={Number(availableBalances?.getLockedBalance() ?? 0)}
/>
<Currency value={balances?.lockedBalance || 0} />
</Text>
</Stack>
</Stack>

View File

@ -81,7 +81,7 @@ exports[`molecules::WalletBalance > renders after loading data 1`] = `
<div
class="mantine-Text-root mantine-1pfxwhx"
>
(USD 7,800.00)
(USD 4,500.00)
</div>
</div>
<div
@ -106,7 +106,7 @@ exports[`molecules::WalletBalance > renders after loading data 1`] = `
<div
class="mantine-Text-root mantine-14d5cdm"
>
26.00
15.00
</div>
</div>
<div
@ -197,12 +197,92 @@ exports[`molecules::WalletBalance > renders loading state 1`] = `
</div>
</div>
<div
class="mantine-Stack-root mantine-lfk3cq"
class="mantine-Stack-root mantine-1kb6t4k"
>
<div
class="mantine-Text-root mantine-1152338"
class="mantine-Group-root mantine-6y1794"
>
Loading...
<div
class="mantine-Text-root mantine-Group-child mantine-q5labh"
>
15.00
</div>
<svg
class="mantine-Group-child mantine-qg5oag"
fill="none"
height="1em"
viewBox="0 0 7 4"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M2.91916 3.69319C3.23963 4.04927 3.76166 4.04657 4.07971 3.69319L6.82329 0.644746C7.14377 0.288661 7.01636 0 6.53811 0H0.460749C-0.0172283 0 -0.14248 0.291357 0.17557 0.644746L2.91916 3.69319Z"
fill="#111111"
fill-rule="evenodd"
/>
</svg>
</div>
<div
class="mantine-Text-root mantine-1pfxwhx"
>
(USD 4,500.00)
</div>
</div>
<div
aria-hidden="true"
class="mantine-1avyp1d"
style="box-sizing: border-box; display: none; height: 0px; overflow: hidden;"
>
<div
style="opacity: 0; transition: opacity 200ms ease;"
>
<div
class="mantine-Stack-root mantine-lfk3cq"
>
<div
class="mantine-Stack-root mantine-1kb6t4k"
>
<div
class="mantine-Text-root mantine-kaqdcf"
>
Total
</div>
<div
class="mantine-Text-root mantine-14d5cdm"
>
15.00
</div>
</div>
<div
class="mantine-Stack-root mantine-1kb6t4k"
>
<div
class="mantine-Text-root mantine-kaqdcf"
>
Reserved
</div>
<div
class="mantine-Text-root mantine-14d5cdm"
>
14.00
</div>
</div>
<div
class="mantine-Stack-root mantine-1kb6t4k"
>
<div
class="mantine-Text-root mantine-kaqdcf"
>
Locked
</div>
<div
class="mantine-Text-root mantine-14d5cdm"
>
12.00
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,90 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { WalletTransactions } from "./WalletTransactions";
import type { TWalletTransaction } from "./_types";
import { WalletTransactionType } from "./_types";
export default {
title: "organisms/WalletTransactions",
component: WalletTransactions,
} as ComponentMeta<typeof WalletTransactions>;
const Template: ComponentStory<typeof WalletTransactions> = () => {
return <WalletTransactions data={data} />;
};
export const Default = Template.bind({});
Default.args = {};
const data = [
{
type: WalletTransactionType.Sent,
timestamp: Date.now(),
amount: 17.275849365201,
foreignAmount: 4108.5,
amountCurrency: "XMR",
foreignAmountCurrency: "EUR",
transactionId:
"a1b848fdf7fb77f1dae266331d23c522db267ced63566a6e35800421c988d9f1",
incomingAddresses: [
"7631e90afdb723b1a798b39bfc5ec942i5d0e155dfa993f536827c7f9699740a",
],
destinationAddresses: [
"7631e90afdb723b1a798b39bfc5ec942i5d0e155dfa993f536827c7f9699740a",
],
height: 2482937,
fee: 0.000005096,
},
{
type: WalletTransactionType.Received,
timestamp: Date.now(),
amount: 17.275849365201,
foreignAmount: 4108.5,
amountCurrency: "XMR",
foreignAmountCurrency: "EUR",
transactionId:
"a1b848fdf7fb77f1dae266331d23c522db267ced63566a6e35800421c988d9f1",
incomingAddresses: [
"7631e90afdb723b1a798b39bfc5ec942i5d0e155dfa993f536827c7f9699740a",
],
destinationAddresses: [
"7631e90afdb723b1a798b39bfc5ec942i5d0e155dfa993f536827c7f9699740a",
],
height: 2482937,
fee: 0.000005096,
},
{
type: WalletTransactionType.Sent,
timestamp: Date.now(),
amount: 17.275849365201,
foreignAmount: 4108.5,
amountCurrency: "XMR",
foreignAmountCurrency: "EUR",
transactionId:
"a1b848fdf7fb77f1dae266331d23c522db267ced63566a6e35800421c988d9f1",
incomingAddresses: [
"7631e90afdb723b1a798b39bfc5ec942i5d0e155dfa993f536827c7f9699740a",
],
destinationAddresses: [
"7631e90afdb723b1a798b39bfc5ec942i5d0e155dfa993f536827c7f9699740a",
],
height: 2482937,
fee: 0.000005096,
},
] as TWalletTransaction[];

View File

@ -0,0 +1,77 @@
// =============================================================================
// 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 { createTable } from "@tanstack/react-table";
import { Table } from "@molecules/Table";
import { createStyles } from "@mantine/core";
import {
WalletTransactionnSignCell,
WalletTransactionAmountCell,
} from "./WalletTransactionsCells";
import { WalletTransactionRowExpanded } from "./WalletTransactionsRowExpanded";
import type { TWalletTransaction } from "./_types";
const table = createTable().setRowType<TWalletTransaction>();
const columns = [
table.createDataColumn("type", {
id: "type",
header: "Type",
cell: ({ row }) => <WalletTransactionnSignCell row={row.original} />,
}),
table.createDataColumn("amount", {
id: "transaction",
header: "Amount",
cell: ({ row }) => <WalletTransactionAmountCell row={row.original} />,
size: 400,
}),
];
interface WalletTransactionsProps {
data: Array<TWalletTransaction>;
}
export function WalletTransactions({ data }: WalletTransactionsProps) {
const { classes } = useStyles();
return (
<Table
table={table}
columns={columns}
data={data}
showHeader={false}
tableWrap={{
verticalSpacing: "md",
className: classes.root,
}}
rowSubComponent={WalletTransactionRowExpanded}
/>
);
}
const useStyles = createStyles(() => ({
root: {
"tbody tr td:first-child": {
paddingLeft: 0,
},
"tbody tr td:last-child": {
paddingRight: 0,
},
"tbody tr": {
cursor: "pointer",
},
},
}));

View File

@ -0,0 +1,98 @@
// =============================================================================
// 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 { FormattedDate, FormattedTime, useIntl } from "react-intl";
import { Box, Group, Stack, Text, useMantineTheme } from "@mantine/core";
import { ReactComponent as ArrowNorth } from "@assets/arrow-north.svg";
import { ReactComponent as ArrowWest } from "@assets/arrow-west.svg";
import { LangKeys } from "@constants/lang";
import { Currency } from "@atoms/Currency";
import { CircleIcon } from "@atoms/CircleIcon/CircleIcon";
import type { TWalletTransaction } from "./_types";
import { WalletTransactionType } from "./_types";
export function WalletTransactionnSignCell({
row,
}: {
row?: TWalletTransaction;
}) {
const { formatMessage } = useIntl();
const theme = useMantineTheme();
return (
<Group>
<CircleIcon>
{WalletTransactionType.Sent === row?.type ? (
<ArrowNorth color={theme.colors.blue[5]} width={18} height={18} />
) : (
<ArrowWest color={theme.colors.green[4]} width={18} height={18} />
)}
</CircleIcon>
<Box>
<Text weight="bold">
{row?.type === WalletTransactionType.Sent
? formatMessage({
id: LangKeys.WalletDetailSent,
defaultMessage: "Sent",
})
: formatMessage({
id: LangKeys.WalletDetailReceived,
defaultMessage: "Received",
})}
</Text>
<Group spacing="xs">
<Text size="sm" color="gray">
<FormattedTime value={row?.timestamp} />
</Text>
<Text size="sm" color="gray">
<FormattedDate
value={row?.timestamp}
year="numeric"
month="long"
day="2-digit"
/>
</Text>
</Group>
</Box>
</Group>
);
}
export function WalletTransactionAmountCell({
row,
}: {
row?: TWalletTransaction;
}) {
return (
<Stack spacing={0} sx={{ textAlign: "right" }}>
<Text weight="bold">
<Currency value={row?.amount || 0} currencyCode={row?.amountCurrency} />
</Text>
{row?.foreignAmount && (
<Text size="sm" color="gray">
<Currency
value={row?.foreignAmount}
currencyCode={row?.foreignAmountCurrency}
/>
</Text>
)}
</Stack>
);
}

View File

@ -0,0 +1,131 @@
// =============================================================================
// 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 { useIntl } from "react-intl";
import { Box, createStyles, Grid } from "@mantine/core";
import type { Row } from "@tanstack/react-table";
import { DetailItem } from "@atoms/DetailItem";
import { LangKeys } from "@constants/lang";
import { Currency } from "@atoms/Currency";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function WalletTransactionRowExpanded({ row }: { row: Row<any> }) {
const { formatMessage } = useIntl();
const { classes } = useRowExpanded();
return (
<Box className={classes.root}>
<Grid>
<Grid.Col span={9}>
<DetailItem
label={formatMessage({
id: LangKeys.WalletDetailTransactionId,
defaultMessage: "Transaction ID",
})}
classNames={{
label: classes.label,
content: classes.detailContent,
}}
mb="lg"
>
{row.original.transactionId}
</DetailItem>
{row.original?.destinationAddresses?.map((address: string) => (
<DetailItem
key={address}
label={formatMessage({
id: LangKeys.WalletDetailDestinationAddress,
defaultMessage: "Transaction Key",
})}
classNames={{
label: classes.label,
content: classes.detailContent,
}}
mb="lg"
>
{address}
</DetailItem>
))}
{row.original?.incomingAddresses?.map((address: string) => (
<DetailItem
key={address}
label={formatMessage({
id: LangKeys.WalletDetailIncomingAddress,
defaultMessage: "Incoming Address",
})}
classNames={{
label: classes.label,
content: classes.detailContent,
}}
mb="lg"
>
{address}
</DetailItem>
))}
</Grid.Col>
<Grid.Col span={3}>
<DetailItem
label={formatMessage({
id: LangKeys.WalletDetailFee,
defaultMessage: "Fee",
})}
ml="auto"
textAlign="right"
classNames={{
label: classes.label,
content: classes.detailContent,
}}
mb="lg"
>
<Currency value={row.original.fee} />
</DetailItem>
<DetailItem
label={formatMessage({
id: LangKeys.WalletDetailHeight,
defaultMessage: "Height",
})}
ml="auto"
textAlign="right"
classNames={{
label: classes.label,
content: classes.detailContent,
}}
mb="lg"
>
<Currency value={row.original.height} />
</DetailItem>
</Grid.Col>
</Grid>
</Box>
);
}
const useRowExpanded = createStyles((theme) => ({
root: {
paddingLeft: 50,
paddingTop: theme.spacing.sm,
paddingBottom: theme.spacing.sm,
},
label: {
fontSize: 10,
},
detailContent: {
wordBreak: "break-all",
},
}));

View File

@ -0,0 +1,39 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
export enum WalletTransactionType {
Sent = "Sent",
Received = "Received",
}
export interface TWalletTransaction {
type: WalletTransactionType;
timestamp: number | Date;
amount: number;
amountCurrency: string;
foreignAmount?: number;
foreignAmountCurrency?: string;
transactionId: string;
destinationAddresses?: Array<string>;
incomingAddresses?: Array<string>;
fee: number;
height: number;
}

View File

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

View File

@ -0,0 +1,44 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { MoneroBalance } from "./MoneroBalance";
export default {
title: "molecules/MoneroBalance",
component: MoneroBalance,
} as ComponentMeta<typeof MoneroBalance>;
const Template: ComponentStory<typeof MoneroBalance> = () => {
return (
<MoneroBalance>
<MoneroBalance.Detail label="AVAILABLE BALANCE">
14.048212174412
</MoneroBalance.Detail>
<MoneroBalance.Detail label="AVAILABLE BALANCE">
14.048212174412
</MoneroBalance.Detail>
<MoneroBalance.Detail label="AVAILABLE BALANCE">
14.048212174412
</MoneroBalance.Detail>
</MoneroBalance>
);
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,57 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import { MoneroBalance } from "./MoneroBalance";
describe("organisms::MoneroBalance", () => {
it("renders without exploding.", () => {
const { asFragment, unmount } = render(
<MoneroBalance>
<MoneroBalance.Detail label="Balance">10.20</MoneroBalance.Detail>
</MoneroBalance>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("renders children content.", () => {
const { unmount } = render(<MoneroBalance>Content here...</MoneroBalance>);
expect(screen.queryByText("Content here...")).toBeInTheDocument();
unmount();
});
it("renders monero balance detail", () => {
const { unmount } = render(
<MoneroBalance>
<MoneroBalance.Detail label="Balance label">10.20</MoneroBalance.Detail>
</MoneroBalance>
);
expect(screen.queryByText("Balance label")).toBeInTheDocument();
expect(screen.queryByText("10.20")).toBeInTheDocument();
unmount();
});
it("display Monero logo", () => {
const { unmount, container } = render(
<MoneroBalance>Content here...</MoneroBalance>
);
expect(container.querySelector("svg#monero-icon")).toBeInTheDocument();
unmount();
});
});

View File

@ -0,0 +1,65 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { Group, createStyles } from "@mantine/core";
import { ReactComponent as MoneroIcon } from "@assets/monero.svg";
import { DetailItem } from "@atoms/DetailItem";
interface MoneroBalanceProps {
children: React.ReactNode;
}
export function MoneroBalance({ children }: MoneroBalanceProps) {
const { classes } = useStyles();
return (
<Group className={classes.root} spacing={0}>
<MoneroIcon className={classes.moneroIcon} />
<Group className={classes.content}>{children}</Group>
</Group>
);
}
interface MoneroBalanceDetail {
label: string;
children: React.ReactNode;
}
MoneroBalance.Detail = MoneroBalanceDetail;
function MoneroBalanceDetail({ ...props }: MoneroBalanceDetail) {
return <DetailItem {...props} />;
}
const useStyles = createStyles((theme) => ({
root: {
backgroundColor: theme.white,
border: `1px solid ${theme.colors.gray[3]}`,
borderRadius: theme.radius.md,
paddingTop: theme.spacing.md,
paddingBottom: theme.spacing.md,
paddingLeft: theme.spacing.md,
paddingRight: theme.spacing.md,
},
moneroIcon: {
height: 32,
marginLeft: theme.spacing.xs,
marginRight: theme.spacing.xl,
width: 32,
},
content: {
gap: theme.spacing.xl * 1.5,
},
}));

View File

@ -0,0 +1,61 @@
// Vitest Snapshot v1
exports[`organisms::MoneroBalance > renders without exploding. 1`] = `
<DocumentFragment>
<div
class="mantine-Group-root mantine-3iq866"
>
<svg
class="mantine-Group-child mantine-154defe"
fill="none"
height="1em"
id="monero-icon"
viewBox="0 0 32 32"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#clip0_2924_33103)"
>
<path
d="M15.9979 0C7.16414 0 -0.0112325 7.17316 1.32008e-05 15.9979C0.00222827 17.7635 0.283798 19.4618 0.812262 21.0505H5.5992V7.59219L15.9979 17.9897L26.3961 7.59219V21.0507H31.1841C31.7133 19.4622 31.9934 17.7638 31.9966 15.9982C32.0117 7.16422 24.8326 0.00212965 15.9979 0.00212965V0Z"
fill="#111111"
/>
<path
d="M13.6084 20.3801L9.0703 15.8423V24.3109H5.60074L2.32678 24.3115C5.13506 28.9184 10.2103 32 15.9998 32C21.7893 32 26.8648 28.9177 29.6736 24.3108H22.9283V15.8423L18.39 20.3801L15.9993 22.7705L13.6086 20.3801H13.6084Z"
fill="#111111"
/>
</g>
<defs>
<clippath
id="clip0_2924_33103"
>
<rect
fill="white"
height="32"
width="32"
/>
</clippath>
</defs>
</svg>
<div
class="mantine-Group-root mantine-Group-child mantine-1gdejmd"
>
<div
class="mantine-Stack-root mantine-DetailItem-root mantine-Group-child mantine-2d5onn"
>
<div
class="mantine-Text-root mantine-DetailItem-label mantine-1mkytp"
>
Balance
</div>
<div
class="mantine-Text-root mantine-DetailItem-content mantine-17mq3ni"
>
10.20
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

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

View File

@ -0,0 +1,83 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { FC } from "react";
import { useIntl } from "react-intl";
import { LangKeys } from "@constants/lang";
import { useBalances } from "@hooks/haveno/useBalances";
import { MoneroBalance } from "@organisms/MoneroBalance";
import { MyWalletMeneroBalanceSkeleton } from "./MyWalletMeneroBalanceSkeleton";
import { Currency } from "@atoms/Currency";
export function MyWalletMoneroBalanceContent() {
const { formatMessage } = useIntl();
const { isLoading: isBalancesLoading, data: balanceInfo } = useBalances();
if (isBalancesLoading || !balanceInfo) {
return <>Loading ...</>;
}
return (
<MoneroBalance>
<MoneroBalance.Detail
label={formatMessage({
id: LangKeys.MyWalletMoneroAvaliableBalance,
defaultMessage: "Avaliable Balance",
})}
data-testid="avaliable-balance"
>
<Currency value={balanceInfo.balance} />
</MoneroBalance.Detail>
<MoneroBalance.Detail
label={formatMessage({
id: LangKeys.MyWalletMoneroReserveredFunds,
defaultMessage: "Reservered Funds",
})}
data-testid="reserverd-funds"
>
<Currency value={balanceInfo.reservedOfferBalance || 0} />
</MoneroBalance.Detail>
<MoneroBalance.Detail
label={formatMessage({
id: LangKeys.MyWalletMoneroLockedFunds,
defaultMessage: "Locked Funds",
})}
data-testid="locked-funds"
>
<Currency value={balanceInfo.lockedBalance || 0} />
</MoneroBalance.Detail>
</MoneroBalance>
);
}
export function MyWalletMoneroBalance() {
return (
<MyWalletMoneroBalanceBoot>
<MyWalletMoneroBalanceContent />
</MyWalletMoneroBalanceBoot>
);
}
export const MyWalletMoneroBalanceBoot: FC = ({ children }): JSX.Element => {
const { isLoading: isBalancesLoading } = useBalances();
return isBalancesLoading ? (
<MyWalletMeneroBalanceSkeleton />
) : (
<>{children}</>
);
};

View File

@ -0,0 +1,55 @@
// =============================================================================
// 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 { useIntl } from "react-intl";
import { Skeleton } from "@mantine/core";
import { LangKeys } from "@constants/lang";
import { MoneroBalance } from "@organisms/MoneroBalance";
export function MyWalletMeneroBalanceSkeleton() {
const { formatMessage } = useIntl();
return (
<MoneroBalance>
<MoneroBalance.Detail
label={formatMessage({
id: LangKeys.MyWalletMoneroAvaliableBalance,
defaultMessage: "Avaliable Balance",
})}
>
<Skeleton height={8} radius="xl" mt={6} />
</MoneroBalance.Detail>
<MoneroBalance.Detail
label={formatMessage({
id: LangKeys.MyWalletMoneroReserveredFunds,
defaultMessage: "Reservered Funds",
})}
>
<Skeleton height={8} radius="xl" mt={6} />
</MoneroBalance.Detail>
<MoneroBalance.Detail
label={formatMessage({
id: LangKeys.MyWalletMoneroLockedFunds,
defaultMessage: "Locked Funds",
})}
>
<Skeleton height={8} radius="xl" mt={6} />
</MoneroBalance.Detail>
</MoneroBalance>
);
}

View File

@ -0,0 +1,31 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { MyWalletMoneroBalance } from "./MyWalletMeneroBalance";
export default {
title: "organisms/MyWalletMoneroBalance",
component: MyWalletMoneroBalance,
} as ComponentMeta<typeof MyWalletMoneroBalance>;
const Template: ComponentStory<typeof MyWalletMoneroBalance> = () => {
return <MyWalletMoneroBalance />;
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,85 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it, vi, beforeAll } from "vitest";
import { render, screen } from "@testing-library/react";
import { MyWalletMoneroBalance } from "./MyWalletMeneroBalance";
import { AppProviders } from "@atoms/AppProviders";
describe("organisms::MyWalletMoneroBalance", () => {
beforeAll(() => {
vi.mock("@hooks/haveno/useBalances", () => ({
useBalances: () => ({
isLoading: false,
isSuccess: true,
data: {
balance: MoneroBalance.balance,
lockedBalance: MoneroBalance.lockedBalance,
reservedOfferBalance: MoneroBalance.reserverdBalance,
reservedTradeBalance: MoneroBalance.reserverdBalance,
unlockedBalance: MoneroBalance.unlockedBalance,
},
}),
}));
});
it("renders without exploding", () => {
const { asFragment, unmount } = render(
<AppProviders>
<MyWalletMoneroBalance />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("contains avaliable balance, reservered funds and locked funds details", () => {
const { unmount } = render(
<AppProviders>
<MyWalletMoneroBalance />
</AppProviders>
);
expect(screen.queryByTestId("avaliable-balance")).toBeInTheDocument();
expect(screen.queryByTestId("locked-funds")).toBeInTheDocument();
expect(screen.queryByTestId("reserverd-funds")).toBeInTheDocument();
unmount();
});
it("contains avaliable balance, reservered funds and locked funds details", () => {
const { unmount } = render(
<AppProviders>
<MyWalletMoneroBalance />
</AppProviders>
);
expect(screen.queryByTestId("avaliable-balance")).toHaveTextContent(
"835,120.34017"
);
expect(screen.queryByTestId("reserverd-funds")).toHaveTextContent(
"74,610.1236"
);
expect(screen.queryByTestId("locked-funds")).toHaveTextContent(
"90,371.161239"
);
unmount();
});
});
const MoneroBalance = {
balance: 835120.34017,
reserverdBalance: 74610.1236,
lockedBalance: 90371.161239,
unlockedBalance: 0,
};

View File

@ -0,0 +1,31 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { MyWalletMeneroBalanceSkeleton } from "./MyWalletMeneroBalanceSkeleton";
export default {
title: "organisms/MyWalletMeneroBalanceSkeleton",
component: MyWalletMeneroBalanceSkeleton,
} as ComponentMeta<typeof MyWalletMeneroBalanceSkeleton>;
const Template: ComponentStory<typeof MyWalletMeneroBalanceSkeleton> = () => {
return <MyWalletMeneroBalanceSkeleton />;
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,31 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it } from "vitest";
import { render } from "@testing-library/react";
import { MyWalletMeneroBalanceSkeleton } from "./MyWalletMeneroBalanceSkeleton";
import { AppProviders } from "@atoms/AppProviders";
describe("organisms::MyWalletMeneroBalanceSkeleton", () => {
it("renders without exploding", () => {
const { asFragment } = render(
<AppProviders>
<MyWalletMeneroBalanceSkeleton />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,92 @@
// Vitest Snapshot v1
exports[`organisms::MyWalletMoneroBalance > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-Group-root mantine-3iq866"
>
<svg
class="mantine-Group-child mantine-154defe"
fill="none"
height="1em"
id="monero-icon"
viewBox="0 0 32 32"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#clip0_2924_33103)"
>
<path
d="M15.9979 0C7.16414 0 -0.0112325 7.17316 1.32008e-05 15.9979C0.00222827 17.7635 0.283798 19.4618 0.812262 21.0505H5.5992V7.59219L15.9979 17.9897L26.3961 7.59219V21.0507H31.1841C31.7133 19.4622 31.9934 17.7638 31.9966 15.9982C32.0117 7.16422 24.8326 0.00212965 15.9979 0.00212965V0Z"
fill="#111111"
/>
<path
d="M13.6084 20.3801L9.0703 15.8423V24.3109H5.60074L2.32678 24.3115C5.13506 28.9184 10.2103 32 15.9998 32C21.7893 32 26.8648 28.9177 29.6736 24.3108H22.9283V15.8423L18.39 20.3801L15.9993 22.7705L13.6086 20.3801H13.6084Z"
fill="#111111"
/>
</g>
<defs>
<clippath
id="clip0_2924_33103"
>
<rect
fill="white"
height="32"
width="32"
/>
</clippath>
</defs>
</svg>
<div
class="mantine-Group-root mantine-Group-child mantine-1gdejmd"
>
<div
class="mantine-Stack-root mantine-DetailItem-root mantine-Group-child mantine-2d5onn"
data-testid="avaliable-balance"
>
<div
class="mantine-Text-root mantine-DetailItem-label mantine-10gsq91"
>
Avaliable Balance
</div>
<div
class="mantine-Text-root mantine-DetailItem-content mantine-oat2gy"
>
835,120.34017
</div>
</div>
<div
class="mantine-Stack-root mantine-DetailItem-root mantine-Group-child mantine-2d5onn"
data-testid="reserverd-funds"
>
<div
class="mantine-Text-root mantine-DetailItem-label mantine-10gsq91"
>
Reservered Funds
</div>
<div
class="mantine-Text-root mantine-DetailItem-content mantine-oat2gy"
>
74,610.1236
</div>
</div>
<div
class="mantine-Stack-root mantine-DetailItem-root mantine-Group-child mantine-2d5onn"
data-testid="locked-funds"
>
<div
class="mantine-Text-root mantine-DetailItem-label mantine-10gsq91"
>
Locked Funds
</div>
<div
class="mantine-Text-root mantine-DetailItem-content mantine-oat2gy"
>
90,371.161239
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

@ -0,0 +1,95 @@
// Vitest Snapshot v1
exports[`organisms::MyWalletMeneroBalanceSkeleton > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-Group-root mantine-3iq866"
>
<svg
class="mantine-Group-child mantine-154defe"
fill="none"
height="1em"
id="monero-icon"
viewBox="0 0 32 32"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#clip0_2924_33103)"
>
<path
d="M15.9979 0C7.16414 0 -0.0112325 7.17316 1.32008e-05 15.9979C0.00222827 17.7635 0.283798 19.4618 0.812262 21.0505H5.5992V7.59219L15.9979 17.9897L26.3961 7.59219V21.0507H31.1841C31.7133 19.4622 31.9934 17.7638 31.9966 15.9982C32.0117 7.16422 24.8326 0.00212965 15.9979 0.00212965V0Z"
fill="#111111"
/>
<path
d="M13.6084 20.3801L9.0703 15.8423V24.3109H5.60074L2.32678 24.3115C5.13506 28.9184 10.2103 32 15.9998 32C21.7893 32 26.8648 28.9177 29.6736 24.3108H22.9283V15.8423L18.39 20.3801L15.9993 22.7705L13.6086 20.3801H13.6084Z"
fill="#111111"
/>
</g>
<defs>
<clippath
id="clip0_2924_33103"
>
<rect
fill="white"
height="32"
width="32"
/>
</clippath>
</defs>
</svg>
<div
class="mantine-Group-root mantine-Group-child mantine-1gdejmd"
>
<div
class="mantine-Stack-root mantine-DetailItem-root mantine-Group-child mantine-2d5onn"
>
<div
class="mantine-Text-root mantine-DetailItem-label mantine-10gsq91"
>
Avaliable Balance
</div>
<div
class="mantine-Text-root mantine-DetailItem-content mantine-oat2gy"
>
<div
class="mantine-Skeleton-root mantine-Skeleton-visible mantine-12edv24"
/>
</div>
</div>
<div
class="mantine-Stack-root mantine-DetailItem-root mantine-Group-child mantine-2d5onn"
>
<div
class="mantine-Text-root mantine-DetailItem-label mantine-10gsq91"
>
Reservered Funds
</div>
<div
class="mantine-Text-root mantine-DetailItem-content mantine-oat2gy"
>
<div
class="mantine-Skeleton-root mantine-Skeleton-visible mantine-12edv24"
/>
</div>
</div>
<div
class="mantine-Stack-root mantine-DetailItem-root mantine-Group-child mantine-2d5onn"
>
<div
class="mantine-Text-root mantine-DetailItem-label mantine-10gsq91"
>
Locked Funds
</div>
<div
class="mantine-Text-root mantine-DetailItem-content mantine-oat2gy"
>
<div
class="mantine-Skeleton-root mantine-Skeleton-visible mantine-12edv24"
/>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

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

View File

@ -0,0 +1,52 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it, vi, beforeAll } from "vitest";
import { render, screen } from "@testing-library/react";
import { AppProviders } from "@atoms/AppProviders";
import { MyWalletPrimaryAddress } from "./MyWalletPrimaryAddress";
describe("organisms::MyWalletPrimaryAddress", () => {
beforeAll(() => {
vi.mock("@hooks/haveno/useXmrPrimaryAddress", () => ({
useXmrPrimaryAddress: () => ({
isLoading: false,
isSuccess: true,
data: "HAVENOADDRESS",
}),
}));
});
it("renders without exploding", () => {
const { asFragment, unmount } = render(
<AppProviders>
<MyWalletPrimaryAddress />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("renders xmr primary address from the daemon.", () => {
const { unmount } = render(
<AppProviders>
<MyWalletPrimaryAddress />
</AppProviders>
);
expect(screen.queryByText("HAVENOADDRESS")).toMatchSnapshot();
unmount();
});
});

View File

@ -0,0 +1,65 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useIntl } from "react-intl";
import {
AddressCard,
AddressCardSkeleton,
} from "@molecules/AddressCard/AddressCard";
import { useXmrPrimaryAddress } from "@hooks/haveno/useXmrPrimaryAddress";
import { LangKeys } from "@constants/lang";
export function MyWalletPrimaryAddressContent() {
const { isLoading, data: xmrAddress } = useXmrPrimaryAddress();
const { formatMessage } = useIntl();
if (isLoading || !xmrAddress) return <>Loading...</>;
return (
<AddressCard
label={formatMessage({
id: LangKeys.MyWalletBalancePrimaryAddress,
defaultMessage: "Primary Address",
})}
address={xmrAddress}
primary={true}
/>
);
}
export function MyWalletPrimaryAddressSkeleton() {
const { formatMessage } = useIntl();
return (
<AddressCardSkeleton
label={formatMessage({
id: LangKeys.MyWalletBalancePrimaryAddress,
defaultMessage: "Primary Address",
})}
primary={true}
/>
);
}
export function MyWalletPrimaryAddress() {
const { isLoading } = useXmrPrimaryAddress();
return isLoading ? (
<MyWalletPrimaryAddressSkeleton />
) : (
<MyWalletPrimaryAddressContent />
);
}

View File

@ -0,0 +1,54 @@
// Vitest Snapshot v1
exports[`organisms::MyWalletPrimaryAddress > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-Group-root mantine-sfnj26"
>
<div
class="mantine-Stack-root mantine-DetailItem-root mantine-Group-child mantine-1a4fzbd"
>
<div
class="mantine-Text-root mantine-DetailItem-label mantine-10gsq91"
>
Primary Address
</div>
<div
class="mantine-Text-root mantine-DetailItem-content mantine-oat2gy"
>
<div
class="mantine-Group-root mantine-1i932k4"
>
<div
class="mantine-Group-child mantine-2w2ufd"
>
HAVENOADDRESS
</div>
<div
class="mantine-Group-root mantine-Group-child mantine-ze53qg"
>
<a
class="mantine-Text-root mantine-Anchor-root mantine-Group-child mantine-16zvpw1"
>
Copy
</a>
<a
class="mantine-Text-root mantine-Anchor-root mantine-Group-child mantine-16zvpw1"
>
QR
</a>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;
exports[`organisms::MyWalletPrimaryAddress > renders xmr primary address from the daemon. 1`] = `
<div
class="mantine-Group-child mantine-2w2ufd"
>
HAVENOADDRESS
</div>
`;

View File

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

View File

@ -0,0 +1,30 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { MyWalletReceive } from "./MyWalletReceive";
export default {
title: "organisms/MyWalletReceive",
component: MyWalletReceive,
} as ComponentMeta<typeof MyWalletReceive>;
const Template: ComponentStory<typeof MyWalletReceive> = () => {
return <MyWalletReceive />;
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,84 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import { Box, Button, Group, Stack, Text } from "@mantine/core";
import { LangKeys } from "@constants/lang";
import { Heading } from "@atoms/Typography";
import { AddressCard } from "@molecules/AddressCard/AddressCard";
import { useSetXmrNewSubaddress } from "@hooks/haveno/useSetXmrNewSubaddress";
import { getActiveReceiveAddresses, saveReceiveAddresss } from "./_utils";
export function MyWalletReceive() {
const { mutateAsync: setXmrNewSubaddress, isLoading: isSetXmrLoading } =
useSetXmrNewSubaddress();
const [addresses, setAddresses] = useState<Array<string>>(
getActiveReceiveAddresses() || []
);
const handleGenerateAddressBtn = () => {
setXmrNewSubaddress().then((newSubaddress: string) => {
saveReceiveAddresss(newSubaddress);
const cachedSubaddress = getActiveReceiveAddresses();
setAddresses(cachedSubaddress);
});
};
return (
<Box>
<Heading
order={4}
stringId={LangKeys.MyWalletReceiveTitle}
mb="sm"
mt="lg"
>
Your Address
</Heading>
{addresses.length === 0 && (
<Text mt="xs" color="gray">
<FormattedMessage
id={LangKeys.MyWalletReceiveNoAddressesMsg}
defaultMessage={`You don't have generated address, please generate one.`}
/>
</Text>
)}
<Stack>
{addresses.map((address) => (
<AddressCard key={address} address={address} />
))}
</Stack>
<Group position="right" mt="md">
<Button
variant="subtle"
color="dark"
size="md"
type="submit"
loading={isSetXmrLoading}
onClick={handleGenerateAddressBtn}
>
+{" "}
<FormattedMessage
id={LangKeys.MyWalletGenerateAddressBtn}
defaultMessage="Generate a new sub address"
/>
</Button>
</Group>
</Box>
);
}

View File

@ -0,0 +1,35 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { isEmpty } from "lodash";
const LOCAL_STORAGE_KEY = "mywallet-received-addresses";
export const getActiveReceiveAddresses = (): Array<string> => {
const storedAddressesString = sessionStorage.getItem(LOCAL_STORAGE_KEY);
const storedAddresses = JSON.parse(storedAddressesString || "[]");
return !isEmpty(storedAddresses) ? storedAddresses : [];
};
export const saveReceiveAddresss = (address: string) => {
const activeAddress = getActiveReceiveAddresses();
sessionStorage.setItem(
LOCAL_STORAGE_KEY,
JSON.stringify([...activeAddress, address])
);
};

View File

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

View File

@ -0,0 +1,30 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { MyWalletSendForm } from "./MyWalletSendForm";
export default {
title: "organisms/MyWalletSendForm",
component: MyWalletSendForm,
} as ComponentMeta<typeof MyWalletSendForm>;
const Template: ComponentStory<typeof MyWalletSendForm> = () => {
return <MyWalletSendForm />;
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,137 @@
// =============================================================================
// 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 { joiResolver, useForm } from "@mantine/form";
import { Group, SimpleGrid, Stack, Text } from "@mantine/core";
import { FormattedMessage, useIntl } from "react-intl";
import { useModals } from "@mantine/modals";
import { TextInput } from "@atoms/TextInput";
import type { MyWalletSendFormValues } from "./_hooks";
import { useMyWalletSendFormValidation } from "./_hooks";
import { LangKeys } from "@constants/lang";
import { Button } from "@atoms/Buttons";
import { useSetXmrSend } from "@hooks/haveno/useSetXmrSend";
export function MyWalletSendForm() {
const { formatMessage } = useIntl();
const modals = useModals();
const { mutateAsync: setXmrSend, isLoading: isXmrSendLoading } =
useSetXmrSend();
const validation = useMyWalletSendFormValidation();
const form = useForm<MyWalletSendFormValues>({
initialValues: {
amount: "",
address: "",
paymentId: "",
},
validate: joiResolver(validation),
});
const handleFormSubmit = (values: MyWalletSendFormValues) => {
setXmrSend(values).then((hash: string) => {
const modalId = modals.openModal({
title: formatMessage({
id: LangKeys.MyWalletSendSuccessModalTitle,
defaultMessage: "Fund are sent!",
}),
children: (
<>
<Text color="gray">
<FormattedMessage
id={LangKeys.MyWalletSendSuccessModalMsg}
defaultMessage="Youve sent {amount} XMR to: {hash}"
values={{
amount: values.amount,
hash,
}}
/>
</Text>
<Button onClick={() => modals.closeModal(modalId)} mt="md">
<FormattedMessage
id={LangKeys.MyWalletSendBackToWallet}
defaultMessage="Back to Wallet"
/>
</Button>
</>
),
});
form.reset();
});
};
return (
<form onSubmit={form.onSubmit(handleFormSubmit)}>
<Stack spacing="xl">
<SimpleGrid cols={2}>
<TextInput
id="amount"
type="number"
label={
<FormattedMessage
id={LangKeys.MyWalletSendFieldAmount}
defaultMessage="Amount"
/>
}
rightSection={
<Text mr="xl" weight={500} color="gray">
XMR
</Text>
}
{...form.getInputProps("amount")}
/>
</SimpleGrid>
<TextInput
id="address"
label={
<FormattedMessage
id={LangKeys.MyWalletSendFieldAddress}
defaultMessage="Address"
/>
}
placeholder={formatMessage({
id: LangKeys.MyWalletSendFieldAddressPlaceholder,
defaultMessage: "Paste in address here...",
})}
{...form.getInputProps("address")}
/>
<TextInput
id="paymentId"
label={
<FormattedMessage
id={LangKeys.MyWalletSendFieldPaymentId}
defaultMessage="Payment ID"
/>
}
placeholder={formatMessage({
id: LangKeys.MyWalletSendFieldPaymentIdPlaceholder,
defaultMessage: "Paste in address here...",
})}
{...form.getInputProps("paymentId")}
/>
</Stack>
<Group position="right" mt="xl">
<Button size="md" type="submit" loading={isXmrSendLoading}>
<FormattedMessage id={LangKeys.Save} defaultMessage="Save" />
</Button>
</Group>
</form>
);
}

View File

@ -0,0 +1,31 @@
// =============================================================================
// 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 * as Joi from "joi";
export interface MyWalletSendFormValues {
amount: string;
address: string;
paymentId: string;
}
export function useMyWalletSendFormValidation() {
return Joi.object<MyWalletSendFormValues>({
amount: Joi.number().required(),
address: Joi.string().required(),
paymentId: Joi.string().optional().empty(""),
});
}

View File

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

View File

@ -0,0 +1,82 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it, vi, beforeAll } from "vitest";
import { render, screen } from "@testing-library/react";
import { AppProviders } from "@atoms/AppProviders";
import { MyWalletTransactions } from "./MyWalletTransactions";
describe("organisms::MyWalletMoneroBalance", () => {
beforeAll(() => {
vi.mock("@hooks/haveno/useXmrTxs", () => ({
useXmrTxs: () => ({
isLoading: false,
isSuccess: true,
data: [
{
timestamp: 1653593643913,
height: "1.334423",
fee: "3.33",
isConfirmed: true,
isLocked: false,
hash: "HASHADDRESS",
incomingTransfersList: [
{
amount: "10000",
accountIndex: 1,
subaddressIndex: 1,
address: "INCOMINGADDRESS",
numSuggestedConfirmations: 100,
},
],
outgoingTransfer: {},
metadata: "",
},
],
}),
}));
});
it("renders without exploding.", () => {
const { asFragment, unmount } = render(
<AppProviders>
<MyWalletTransactions />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("renders date detail correctly.", () => {
const { unmount } = render(
<AppProviders>
<MyWalletTransactions />
</AppProviders>
);
expect(screen.queryByText("May 26, 2022")).toBeInTheDocument();
unmount();
});
it("renders time detail correctly.", () => {
const { unmount } = render(
<AppProviders>
<MyWalletTransactions />
</AppProviders>
);
expect(screen.queryByText("7:34 PM")).toBeInTheDocument();
unmount();
});
});

View File

@ -0,0 +1,50 @@
// =============================================================================
// 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 { FC } from "react";
import { useMemo } from "react";
import { Group, Loader } from "@mantine/core";
import { useXmrTxs } from "@hooks/haveno/useXmrTxs";
import { WalletTransactions } from "@molecules/WalletTransactions";
import { transfromXmrTxs } from "./_utils";
export function MyWalletTransactionsTable() {
const { data: xmrTxs } = useXmrTxs();
const transactions = useMemo(() => transfromXmrTxs(xmrTxs || []), [xmrTxs]);
return xmrTxs ? <WalletTransactions data={transactions} /> : null;
}
const MyWalletTransactionsBoot: FC = ({ children }) => {
const { isLoading: isXmrTxsLoading } = useXmrTxs();
return isXmrTxsLoading ? (
<Group position="center" pt="lg" pb="lg">
<Loader color="gray" size="sm" />
</Group>
) : (
<>{children}</>
);
};
export function MyWalletTransactions() {
return (
<MyWalletTransactionsBoot>
<MyWalletTransactionsTable />
</MyWalletTransactionsBoot>
);
}

View File

@ -0,0 +1,75 @@
// Vitest Snapshot v1
exports[`organisms::MyWalletMoneroBalance > renders without exploding. 1`] = `
<DocumentFragment>
<table
class="mantine-Table-root mantine-16jrhzw"
>
<tbody>
<tr>
<td
style="width: 150px;"
>
<div
class="mantine-Group-root mantine-6y1794"
>
<div
class="mantine-CircleIcon-root mantine-Group-child mantine-lh1mzd"
>
<svg
color="#75B377"
fill="none"
height="18"
viewBox="0 0 14 16"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.83401 14.9637L12.733 10.4695C13.1844 10.0554 13.1844 9.36518 12.733 8.95105C12.2815 8.53691 11.5291 8.53691 11.0777 8.95105L8.16841 11.6046L8.16841 1.78804C8.16841 1.18984 7.65009 0.714355 6.99802 0.714355C6.34594 0.714355 5.82762 1.18984 5.82762 1.78804L5.82762 11.6046L2.93507 8.95105C2.48363 8.53691 1.73124 8.53691 1.2798 8.95105C1.04572 9.16578 0.928677 9.44187 0.928677 9.71796C0.928677 9.99405 1.04572 10.2701 1.2798 10.4849L6.16202 14.9637C6.37938 15.1631 6.68034 15.2858 6.99801 15.2858C7.31569 15.2858 7.61665 15.1784 7.83401 14.9637Z"
fill="currentColor"
/>
</svg>
</div>
<div
class="mantine-Group-child mantine-1oprzqz"
>
<div
class="mantine-Text-root mantine-3v9dod"
>
Received
</div>
<div
class="mantine-Group-root mantine-bhsibn"
>
<div
class="mantine-Text-root mantine-Group-child mantine-e2rvui"
>
7:34 PM
</div>
<div
class="mantine-Text-root mantine-Group-child mantine-e2rvui"
>
May 26, 2022
</div>
</div>
</div>
</div>
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Stack-root mantine-1yjxaby"
>
<div
class="mantine-Text-root mantine-3v9dod"
>
XMR 10,000.00
</div>
</div>
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;

View File

@ -0,0 +1,49 @@
// =============================================================================
// 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 { isEmpty } from "lodash";
import type { XmrTx } from "haveno-ts";
import type { TWalletTransaction } from "@molecules/WalletTransactions/_types";
import { WalletTransactionType } from "@molecules/WalletTransactions/_types";
export const transfromXmrTxs = (
xmrTxs: Array<XmrTx.AsObject>
): Array<TWalletTransaction> => {
return xmrTxs.map(transfromXmrTx);
};
const transfromXmrTx = (xmrTx: XmrTx.AsObject): TWalletTransaction => ({
type: isEmpty(xmrTx.incomingTransfersList)
? WalletTransactionType.Sent
: WalletTransactionType.Received,
fee: parseFloat(xmrTx.fee),
height: xmrTx.height,
amount: !isEmpty(xmrTx.outgoingTransfer)
? parseFloat(xmrTx.outgoingTransfer?.amount || "")
: parseFloat(xmrTx.incomingTransfersList[0]?.amount),
amountCurrency: "XMR",
transactionId: xmrTx.hash,
timestamp: xmrTx.timestamp,
incomingAddresses: xmrTx.incomingTransfersList?.map((addr) => addr.address),
destinationAddresses: xmrTx.outgoingTransfer?.destinationsList?.map(
(addr) => addr.address
),
});

View File

@ -32,4 +32,5 @@ const Template: ComponentStory<typeof Sidebar> = () => {
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -274,7 +274,7 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
class="mantine-Group-child mantine-1mmj0t1"
/>
<div
class="mantine-Text-root mantine-Group-child mantine-1mga3um"
class="mantine-Text-root mantine-Group-child mantine-6ffw9j"
>
Not Synced
</div>

View File

@ -53,6 +53,42 @@ export enum LangKeys {
AccountWalletTitle = "account.wallet.title",
AccountWalletDesc = "account.wallet.desc",
AccountWalletPassword = "account.wallet.field.password",
AccountCardCopyBtn = "accountCard.copyButton",
AccountCardCopiedBtn = "accountCard.copiedButton",
AccountCardQRBtn = "accountCard.qrButton",
AddressCardCopyBtn = "accountCard.copyButton",
AddressCardCopiedBtn = "accountCard.copiedButton",
AddressCardQRBtn = "accountCard.qrButton",
MyWalletSendFieldAmount = "myWallet.send.amountField",
MyWalletSendFieldPaymentId = "myWallet.send.paymentIdField",
MyWalletSendFieldPaymentIdPlaceholder = "myWallet.send.paymentIdFieldPlaceholder",
MyWalletSendFieldAddress = "myWallet.send.addressField",
MyWalletSendFieldAddressPlaceholder = "myWallet.send.addressFieldPlaceholder",
MyWalletReceiveTitle = "myWallet.receive.receiveTitle",
MyWalletMoneroAvaliableBalance = "myWallet.monero.avaliableBalance",
MyWalletMoneroReserveredFunds = "myWallet.monero.reserveredFunds",
MyWalletMoneroLockedFunds = "myWallet.monero.lockedFunds",
MyWalletTabTransactions = "myWallet.transactionsTab",
MyWalletTabSend = "myWallet.sendTab",
MyWalletTabReceive = "myWallet.receive",
MyWalletGenerateAddressBtn = "myWallet.receive.generateAddrBtn",
MyWalletReceiveNoAddressesMsg = "myWallet.receive.noAddressesMsg",
MyWalletSendSuccessNotif = "myWallet.send.successNotification",
WalletDetailSent = "myWallet.detail.sent",
WalletDetailReceived = "myWallet.detail.received",
WalletDetailTransactionId = "myWallet.detail.transactionId",
WalletDetailFee = "myWallet.detail.fee",
WalletDetailDestinationAddress = "myWallet.detail.destinationAddress",
WalletDetailIncomingAddress = "myWallet.detail.incomingAddress",
WalletDetailHeight = "myWallet.detail.height",
WalletDetailReceiptAddress = "myWallet.detail.receiptAddress",
MyWalletBalancePrimaryAddress = "myWallet.balance.primaryAddress",
MyWalletSendBackToWallet = "myWallet.sendForm.backToWalletBtn",
MyWalletSendSuccessModalTitle = "myWallet.sendSuccessModal.title",
MyWalletSendSuccessModalMsg = "myWallet.sendSuccessModal.message",
MyWalletQRModalPrimaryAddress = "myWallet.qrModal.primaryAddress",
MyWalletQRModalReturnBtn = "myWallet.qrModal.returnBtn",
MyWalletQRModalDownloadQRBtn = "myWallet.qrModal.downloadBtn",
AccountBackupDownloadTitle = "account.backup.download.title",
AccountBackupDownloadDesc = "account.backup.download.desc",
AccountBackupDownloadBtn = "account.backup.download.btn",

View File

@ -58,10 +58,49 @@ const LangPackEN: { [key in LangKeys]: string } = {
[LangKeys.AccountSecurityFieldRepeatPasswordMatchMsg]:
"Passwords don't match",
[LangKeys.CreatePassword]: "Create password",
[LangKeys.AccountWalletTitle]: "Your wallet details",
[LangKeys.AccountWalletDesc]:
"The Haveno wallet is permanently connected to your account. Solely saving your seed phrase is not enough to recover your account, you need to download a backup of your account, which you can download via the backup section.",
[LangKeys.AccountWalletPassword]: "Password",
[LangKeys.AccountCardCopyBtn]: "Copy",
[LangKeys.AccountCardCopiedBtn]: "Copied",
[LangKeys.AccountCardQRBtn]: "QR",
[LangKeys.AddressCardCopyBtn]: "Copy",
[LangKeys.AddressCardCopiedBtn]: "Copied",
[LangKeys.AddressCardQRBtn]: "QR",
[LangKeys.MyWalletSendFieldAmount]: "Amount",
[LangKeys.MyWalletSendFieldAddress]: "Address",
[LangKeys.MyWalletSendFieldAddressPlaceholder]: "Paste in address here...",
[LangKeys.MyWalletSendFieldPaymentId]: "Payment ID",
[LangKeys.MyWalletSendFieldPaymentIdPlaceholder]: "Type",
[LangKeys.MyWalletMoneroAvaliableBalance]: "Avaliable Balance",
[LangKeys.MyWalletMoneroReserveredFunds]: "Reservered Funds",
[LangKeys.MyWalletMoneroLockedFunds]: "Locked Funds",
[LangKeys.MyWalletTabTransactions]: "Transactions",
[LangKeys.MyWalletTabSend]: "Send",
[LangKeys.MyWalletTabReceive]: "Receive",
[LangKeys.MyWalletGenerateAddressBtn]: "Generate a new sub address",
[LangKeys.MyWalletReceiveNoAddressesMsg]:
"You don't have generated address, please generate one.",
[LangKeys.MyWalletSendSuccessNotif]:
"The XMR transaction has been sent successfully.",
[LangKeys.WalletDetailSent]: "Sent",
[LangKeys.WalletDetailReceived]: "Received",
[LangKeys.WalletDetailTransactionId]: "Transaction ID",
[LangKeys.WalletDetailFee]: "Fee",
[LangKeys.WalletDetailDestinationAddress]: "Destination Address",
[LangKeys.WalletDetailIncomingAddress]: "Incoming Address",
[LangKeys.WalletDetailHeight]: "Height",
[LangKeys.WalletDetailReceiptAddress]: "Receipt Address",
[LangKeys.MyWalletBalancePrimaryAddress]: "Primary Address",
[LangKeys.MyWalletSendBackToWallet]: "Back to Wallet",
[LangKeys.MyWalletSendSuccessModalTitle]: "Fund are sent!",
[LangKeys.MyWalletSendSuccessModalMsg]: "",
[LangKeys.MyWalletReceiveTitle]: "Your Address",
[LangKeys.MyWalletQRModalPrimaryAddress]: "Primary Address",
[LangKeys.MyWalletQRModalReturnBtn]: "Return",
[LangKeys.MyWalletQRModalDownloadQRBtn]: "Download QR",
[LangKeys.AccountBackupDownloadTitle]: "Download your backup file",
[LangKeys.AccountBackupDownloadDesc]:
"To be able to restore your Haveno account you need to create a backup file of your account. Keep it somewhere safe.",

View File

@ -63,6 +63,44 @@ const LangPackES: { [key in LangKeys]: string } = {
[LangKeys.AccountWalletDesc]:
"La billetera Haveno está permanentemente conectada a su cuenta. Solo guardar su frase inicial no es suficiente para recuperar su cuenta, necesita descargar una copia de seguridad de su cuenta, que puede descargar a través de la sección de copia de seguridad.",
[LangKeys.AccountWalletPassword]: "contraseña",
[LangKeys.AccountCardCopyBtn]: "Dupdo",
[LangKeys.AccountCardCopiedBtn]: "Copiada",
[LangKeys.AccountCardQRBtn]: "QR",
[LangKeys.AddressCardCopyBtn]: "Dupdo",
[LangKeys.AddressCardCopiedBtn]: "Copiada",
[LangKeys.AddressCardQRBtn]: "QR",
[LangKeys.MyWalletSendFieldAmount]: "Monto",
[LangKeys.MyWalletSendFieldAddress]: "Dirección",
[LangKeys.MyWalletSendFieldAddressPlaceholder]: "Pegue la dirección aquí...",
[LangKeys.MyWalletSendFieldPaymentId]: "ID de pago",
[LangKeys.MyWalletSendFieldPaymentIdPlaceholder]: "Tipo",
[LangKeys.MyWalletMoneroAvaliableBalance]: "Saldo disponible",
[LangKeys.MyWalletMoneroReserveredFunds]: "Fondos Reservados",
[LangKeys.MyWalletMoneroLockedFunds]: "Fondos bloqueados",
[LangKeys.MyWalletTabTransactions]: "Transactions",
[LangKeys.MyWalletTabSend]: "Enviar",
[LangKeys.MyWalletTabReceive]: "Recibir",
[LangKeys.MyWalletGenerateAddressBtn]: "Generar una nueva dirección",
[LangKeys.MyWalletReceiveNoAddressesMsg]:
"No ha generado una dirección, por favor genere una.",
[LangKeys.MyWalletSendSuccessNotif]:
"La transacción XMR se ha enviado con éxito.",
[LangKeys.WalletDetailSent]: "Sent",
[LangKeys.WalletDetailReceived]: "Received",
[LangKeys.WalletDetailTransactionId]: "ID de transacción",
[LangKeys.WalletDetailFee]: "Tarifa",
[LangKeys.WalletDetailDestinationAddress]: "Dirección de destino",
[LangKeys.WalletDetailIncomingAddress]: "Dirección entrante",
[LangKeys.WalletDetailHeight]: "Altura",
[LangKeys.WalletDetailReceiptAddress]: "Dirección de recibo",
[LangKeys.MyWalletBalancePrimaryAddress]: "Dirección primaria",
[LangKeys.MyWalletSendBackToWallet]: "Volver a Monedero",
[LangKeys.MyWalletSendSuccessModalTitle]: "¡Se envían fondos!",
[LangKeys.MyWalletSendSuccessModalMsg]: "Youve sent {amount} XMR to: {hash}",
[LangKeys.MyWalletReceiveTitle]: "Su dirección",
[LangKeys.MyWalletQRModalPrimaryAddress]: "Dirección primaria",
[LangKeys.MyWalletQRModalReturnBtn]: "Devolver",
[LangKeys.MyWalletQRModalDownloadQRBtn]: "Descargar código QR",
[LangKeys.AccountBackupDownloadTitle]:
"Descarga tu archivo de copia de seguridad",
[LangKeys.AccountBackupDownloadDesc]:

View File

@ -27,6 +27,8 @@ export enum QueryKeys {
PrimaryAddress = "Haveno.PrimaryAddress",
SyncStatus = "Haveno.SyncStatus",
XmrSeed = "Haveno.XmrSeed",
XmrPrimaryAddress = "Haveno.XmrPrimaryAddress",
XmrTxs = "Haveno.XmrTransactions",
// Storage
StorageAccountInfo = "Storage.AccountInfo",

View File

@ -21,6 +21,7 @@ export const ROUTES = {
Welcome: "/onboarding/welcome",
CreateAccount: "/onboarding/create-account",
RestoreBackup: "/onboarding/restore-backup",
MyWallet: "/my-wallet",
// Account routes
PaymentAccounts: "/account/payment-accounts",

View File

@ -15,13 +15,30 @@
// =============================================================================
import { useQuery } from "react-query";
import type { XmrBalanceInfo } from "haveno-ts";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient";
interface BalanceInfo {
balance: number;
unlockedBalance: number;
lockedBalance: number;
reservedOfferBalance: number;
reservedTradeBalance: number;
}
export function useBalances() {
const client = useHavenoClient();
return useQuery<XmrBalanceInfo, Error>(QueryKeys.Balances, async () =>
client.getBalances()
);
return useQuery<BalanceInfo, Error>(QueryKeys.Balances, async () => {
const xmrBalances = await client.getBalances();
const balances = xmrBalances.toObject();
return {
balance: parseFloat(balances.balance),
unlockedBalance: parseFloat(balances.unlockedBalance),
lockedBalance: parseFloat(balances.lockedBalance),
reservedOfferBalance: parseFloat(balances.reservedOfferBalance),
reservedTradeBalance: parseFloat(balances.reservedTradeBalance),
};
});
}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// =============================================================================
// Copyright 2022 Haveno
//

View File

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

View File

@ -0,0 +1,58 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useMutation, useQueryClient } from "react-query";
import { XmrDestination } from "haveno-ts";
import { showNotification } from "@mantine/notifications";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient";
interface SetXmrSendVariables {
address: string;
amount: string;
paymentId?: string;
}
export function useSetXmrSend() {
const client = useHavenoClient();
const queryClient = useQueryClient();
return useMutation(
async (variables: SetXmrSendVariables) => {
const xmrDest = new XmrDestination()
.setAddress(variables.address)
.setAmount(variables.amount);
const tx = await client.createXmrTx([xmrDest]);
return client.relayXmrTx(tx.getMetadata());
},
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.XmrTxs);
queryClient.invalidateQueries(QueryKeys.Balances);
},
onError: (err: Error) => {
console.dir(err);
showNotification({
color: "red",
message: err.message || "",
title: "Something went wrong",
});
},
}
);
}

View File

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

View File

@ -0,0 +1,30 @@
// =============================================================================
// 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 { XmrTx } from "haveno-ts";
import { useQuery } from "react-query";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient";
export const useXmrTxs = () => {
const client = useHavenoClient();
return useQuery<Array<XmrTx.AsObject>>(QueryKeys.XmrTxs, async () => {
const txs = await client.getXmrTxs();
return txs?.map((tx) => tx.toObject());
});
};

View File

@ -71,7 +71,7 @@ export function StartStopDaemon() {
}),
});
})
.catch((err) => {
.catch((err: Error) => {
console.dir(err);
showNotification({
color: "red",

View File

@ -0,0 +1,37 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { Stack } from "@mantine/core";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { MyWallet } from "./MyWallet";
export default {
title: "pages/MyWallet",
component: MyWallet,
} as ComponentMeta<typeof MyWallet>;
const Template: ComponentStory<typeof MyWallet> = () => {
return (
<Stack>
<MyWallet />
</Stack>
);
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,80 @@
// =============================================================================
// 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 { useIntl } from "react-intl";
import { Container, createStyles, Stack } from "@mantine/core";
import { Tabs } from "@atoms/Tabs";
import { MyWalletMoneroBalance } from "@organisms/MyWalletMoneroBalance";
import { MyWalletPrimaryAddress } from "@organisms/MyWalletPrimaryAddress";
import { MyWalletSendForm } from "@organisms/MyWalletSendForm";
import { MyWalletReceive } from "@organisms/MyWalletReceive";
import { NavbarLayout } from "@templates/NavbarLayout";
import { LangKeys } from "@constants/lang";
import { MyWalletTransactions } from "@organisms/MyWalletTransactions/MyWalletTransactions";
export function MyWallet() {
const { classes } = useStyles();
const { formatMessage } = useIntl();
return (
<NavbarLayout>
<Container size="md" mt="xl" className={classes.container}>
<Stack spacing="lg">
<MyWalletMoneroBalance />
<MyWalletPrimaryAddress />
</Stack>
<Tabs className={classes.tabsRoot} pb="xl">
<Tabs.Tab
label={formatMessage({
id: LangKeys.MyWalletTabTransactions,
defaultMessage: "Transactions",
})}
>
<MyWalletTransactions />
</Tabs.Tab>
<Tabs.Tab
label={formatMessage({
id: LangKeys.MyWalletTabSend,
defaultMessage: "Send",
})}
>
<MyWalletSendForm />
</Tabs.Tab>
<Tabs.Tab
label={formatMessage({
id: LangKeys.MyWalletTabReceive,
defaultMessage: "Receive",
})}
>
<MyWalletReceive />
</Tabs.Tab>
</Tabs>
</Container>
</NavbarLayout>
);
}
const useStyles = createStyles(() => ({
tabsRoot: {
marginTop: "3rem",
},
container: {
width: "100%",
},
}));

View File

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

View File

@ -33,7 +33,8 @@
"types/**/*.d.ts",
"../../types/**/*.d.ts",
"../preload/contracts.d.ts",
"../../tests/setup-tests.ts"
"../../tests/setup-tests.ts",
"../../tests/global-setup.ts"
],
"exclude": ["**/*.spec.ts", "**/*.test.ts"]
}

View File

@ -25,6 +25,7 @@ import viteConfig from "./vite.config";
*/
const config = mergeConfig(viteConfig, {
test: {
globalSetup: ["./tests/global-setup.ts"],
setupFiles: ["../../tests/setup-tests.ts"],
environment: "jsdom",
include: ["./src/**/*.{test,spec}.{ts,tsx}"],

19
tests/global-setup.ts Normal file
View File

@ -0,0 +1,19 @@
// =============================================================================
// 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 default function () {
process.env.TZ = "UTC";
}

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