diff --git a/package.json b/package.json index a908734..6579807 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@types/lodash": "^4.14.182", "@types/react": "<18.0.0", "@types/react-dom": "<18.0.0", + "@types/qrcode": "^1.4.2", "@typescript-eslint/eslint-plugin": "5.12.1", "@typescript-eslint/parser": "^5.19.0", "@vitejs/plugin-react": "^1.3.0", @@ -96,6 +97,7 @@ "joi": "^17.6.0", "jsonwebtoken": "^8.5.1", "lodash": "^4.17.21", + "qrcode": "^1.5.0", "react": "<18.0.0", "react-dom": "<18.0.0", "react-intl": "^5.24.8", diff --git a/packages/main/src/services/haveno.ts b/packages/main/src/services/haveno.ts index 8d5e754..ba49521 100644 --- a/packages/main/src/services/haveno.ts +++ b/packages/main/src/services/haveno.ts @@ -16,6 +16,9 @@ import fsPromises from "fs/promises"; import { ipcMain, dialog } from "electron"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import QRCode from "qrcode/lib/server"; import type { DownloadBackupInput } from "@src/types"; import { IpcChannels } from "@src/types"; @@ -56,4 +59,33 @@ export function registerHavenoHandlers() { const zipFile = files.filePaths[0]; return new Uint8Array(await fsPromises.readFile(zipFile)); }); + + ipcMain.handle( + IpcChannels.DownloadQRCode, + async (_, code: string): Promise => { + const file = await dialog.showSaveDialog({ + defaultPath: "qr-code", + filters: [ + { + extensions: ["png"], + name: "*", + }, + ], + properties: ["createDirectory", "dontAddToRecent"], + }); + if (!file?.filePath) { + return; + } + QRCode.toFile( + file.filePath, + code, + { + width: 500, + }, + (err: Error) => { + if (err) throw err; + } + ); + } + ); } diff --git a/packages/main/src/types/ipc.ts b/packages/main/src/types/ipc.ts index 2176a60..a3a8317 100644 --- a/packages/main/src/types/ipc.ts +++ b/packages/main/src/types/ipc.ts @@ -27,6 +27,7 @@ export enum IpcChannels { // haveno DownloadBackup = "haveno:downloadBackup", RestoreBackup = "haveno:restoreBackup", + DownloadQRCode = "haveno:downloadQRCode", // others VerifyAuthToken = "verifyAuthToken", diff --git a/packages/preload/src/haveno.ts b/packages/preload/src/haveno.ts index 4433655..e8849d4 100644 --- a/packages/preload/src/haveno.ts +++ b/packages/preload/src/haveno.ts @@ -26,6 +26,9 @@ export const haveno = { getBackupData: async (): Promise => ipcRenderer.invoke(IpcChannels.RestoreBackup), + + downloadQRCode: async (qrCode: string): Promise => + ipcRenderer.invoke(IpcChannels.DownloadQRCode, qrCode), }; exposeInMainWorld("haveno", haveno); diff --git a/packages/renderer/src/components/atoms/DetailItemCard/index.tsx b/packages/renderer/src/components/atoms/DetailItemCard/index.tsx new file mode 100644 index 0000000..42451e1 --- /dev/null +++ b/packages/renderer/src/components/atoms/DetailItemCard/index.tsx @@ -0,0 +1 @@ +export * from "./DetailItemCard"; diff --git a/packages/renderer/src/components/atoms/Tabs/Tabs.tsx b/packages/renderer/src/components/atoms/Tabs/Tabs.tsx index 1e1ca82..ec95b4e 100644 --- a/packages/renderer/src/components/atoms/Tabs/Tabs.tsx +++ b/packages/renderer/src/components/atoms/Tabs/Tabs.tsx @@ -45,14 +45,14 @@ const useStyles = createStyles((theme, _params, getRef) => { paddingRight: 0, textTransform: "uppercase", - "&:first-child": { + "&:first-of-type": { marginLeft: 0, }, "&:last-child": { marginRight: 0, }, [`&.${tabActiveRef}`]: { - borderBottomColor: theme.primaryColor, + borderBottomColor: theme.colors.blue[6], color: theme.colors.dark[9], }, }, diff --git a/packages/renderer/src/components/atoms/Typography/Anchor.tsx b/packages/renderer/src/components/atoms/Typography/Anchor.tsx new file mode 100644 index 0000000..4cb80e8 --- /dev/null +++ b/packages/renderer/src/components/atoms/Typography/Anchor.tsx @@ -0,0 +1,14 @@ +import { Anchor as MAnchor, createStyles } from "@mantine/core"; +import type { AnchorProps as MAnchorProps } from "@mantine/core"; + +export function Anchor(props: MAnchorProps<"a">) { + const { classes, cx } = useStyles(); + + return ; +} + +const useStyles = createStyles((theme) => ({ + anchor: { + color: theme.colors.blue[6], + }, +})); diff --git a/packages/renderer/src/components/atoms/Typography/index.ts b/packages/renderer/src/components/atoms/Typography/index.ts index 3c45e15..54b35c3 100644 --- a/packages/renderer/src/components/atoms/Typography/index.ts +++ b/packages/renderer/src/components/atoms/Typography/index.ts @@ -16,3 +16,4 @@ export * from "./Heading"; export * from "./Text"; +export * from "./Anchor"; diff --git a/packages/renderer/src/components/molecules/AddressCard/AddressCard.tsx b/packages/renderer/src/components/molecules/AddressCard/AddressCard.tsx index d5bedc0..39f01af 100644 --- a/packages/renderer/src/components/molecules/AddressCard/AddressCard.tsx +++ b/packages/renderer/src/components/molecules/AddressCard/AddressCard.tsx @@ -17,20 +17,17 @@ import { FormattedMessage, useIntl } from "react-intl"; import QRCode from "react-qr-code"; import type { OpenConfirmModal } from "@mantine/modals/lib/context"; +import { showNotification } from "@mantine/notifications"; import { useModals } from "@mantine/modals"; import { useClipboard } from "@mantine/hooks"; -import { - Anchor, - Box, - createStyles, - Group, - SimpleGrid, - Skeleton, -} from "@mantine/core"; +import { Box, createStyles, Group, SimpleGrid, Skeleton } from "@mantine/core"; import { DetailItem } from "@atoms/DetailItem"; import { Button } from "@atoms/Buttons"; +import { Anchor } from "@atoms/Typography"; +import { DetailItemCard } from "@atoms/DetailItemCard"; import { LangKeys } from "@constants/lang"; -import { DetailItemCard } from "@atoms/DetailItemCard/DetailItemCard"; +import { useSetDownloadQRCode } from "@hooks/haveno/useSetDownloadQRCode"; +import { Modals } from "@constants/modals"; interface AddressCardProps { label?: string; @@ -49,17 +46,34 @@ export function AddressCard({ }: AddressCardProps) { const modals = useModals(); const { classes } = useStyles(); + const { formatMessage } = useIntl(); + + const { mutateAsync: downloadQRCode } = useSetDownloadQRCode(); const clipboard = useClipboard({ timeout: COPY_TEXT_TIMEOUT }); const handleCopyClick = () => { clipboard.copy(address); }; + const handleQRDownloadClick = (addressCode: string) => { + downloadQRCode(addressCode).then(() => { + showNotification({ + color: "green", + message: formatMessage({ + id: LangKeys.AddressCardQRCodeSavedSuccessNotif, + defaultMessage: "The QR code has been saved successfully.", + }), + }); + modals.closeModal(Modals.QRCodeAddress); + }); + }; const handleQRClick = () => { const modalId = modals.openModal({ + id: Modals.QRCodeAddress, children: ( modals.closeModal(modalId)} /> ), @@ -137,7 +151,7 @@ export function AddressCardSkeleton({ interface AddressCardQRModalContentProps { address: string; - onQRDownloadClick?: () => void; + onQRDownloadClick?: (address: string) => void; onReturnClick?: () => void; } @@ -149,10 +163,16 @@ function AddressCardQRModalContent({ const { classes } = useStyles(); const { formatMessage } = useIntl(); + const handleQRDownloadClick = () => { + onQRDownloadClick && onQRDownloadClick(address); + }; return ( - + -