Skip to content

Commit

Permalink
Merge pull request #451 from arconnectio/arc-455/generate-jwk-qr-code
Browse files Browse the repository at this point in the history
refactor: Generate jwk qr code
  • Loading branch information
nicholaswma authored Aug 5, 2024
2 parents 5371233 + 932fa08 commit f76d6e2
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 25 deletions.
12 changes: 12 additions & 0 deletions assets/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
"message": "Save",
"description": "Save button text"
},
"generate": {
"message": "Generate",
"description": "Generate button text"
},
"add_wallet": {
"message": "Add wallet",
"description": "Add a wallet text"
Expand Down Expand Up @@ -1377,6 +1381,14 @@
"message": "Generate QR code",
"description": "Generate QR code button"
},
"generate_qr_code_title": {
"message": "Enter your password to generate QR Code",
"description": "Generate QR code title"
},
"cannot_generate_qr_code": {
"message": "Cannot generate QR code for your hardware wallet.",
"description": "Cannot generate QR text"
},
"viewblock": {
"message": "Viewblock",
"description": "view block"
Expand Down
12 changes: 12 additions & 0 deletions assets/_locales/zh_CN/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
"message": "节省",
"description": "Save button text"
},
"generate": {
"message": "产生",
"description": "Generate button text"
},
"add_wallet": {
"message": "添加钱包",
"description": "Add a wallet text"
Expand Down Expand Up @@ -1365,6 +1369,14 @@
"message": "生成二维码",
"description": "Generate QR code button"
},
"generate_qr_code_title": {
"message": "输入密码生成二维码",
"description": "Generate QR code title"
},
"cannot_generate_qr_code": {
"message": "无法为您的硬件钱包生成二维码。",
"description": "Cannot generate QR text"
},
"viewblock": {
"message": "Viewblock",
"description": "view block"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"plimit-lit": "^3.0.1",
"pretty-bytes": "^6.0.0",
"qrcode.react": "^3.1.0",
"qrloop": "^1.4.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-fast-marquee": "^1.3.5",
Expand Down
10 changes: 5 additions & 5 deletions src/routes/popup/receive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,26 +115,26 @@ export default function Receive({ walletName, walletAddress }: ReceiveProps) {
);
}

const Wrapper = styled.div`
export const Wrapper = styled.div`
display: flex;
flex-direction: column;
height: calc(100vh - 72px);
`;

const ContentWrapper = styled.div`
export const ContentWrapper = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
`;

const AddressField = styled(ButtonV2)`
export const AddressField = styled(ButtonV2)`
display: flex;
align-items: center;
gap: 5px;
font-weight: 500;
`;

const CopyAction = styled(CopyIcon)`
export const CopyAction = styled(CopyIcon)`
font-size: 1.25rem;
width: 1em;
height: 1em;
Expand All @@ -151,7 +151,7 @@ const CopyAction = styled(CopyIcon)`
}
`;

const QRCodeWrapper = styled.div`
export const QRCodeWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
Expand Down
244 changes: 224 additions & 20 deletions src/routes/popup/settings/wallets/[address]/qr.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,230 @@
import { useStorage } from "@plasmohq/storage/hook";
import { useMemo } from "react";
import Receive from "~routes/popup/receive";
import { ExtensionStorage } from "~utils/storage";
import type { StoredWallet } from "~wallets";
import {
useToasts,
Section,
TooltipV2,
useInput,
ButtonV2,
InputV2,
Spacer,
Text
} from "@arconnect/components";
import { CheckIcon, CopyIcon } from "@iconicicons/react";
import copy from "copy-to-clipboard";
import { QRCodeSVG } from "qrcode.react";
import {
useEffect,
useRef,
useState,
type Key,
type MouseEventHandler
} from "react";
import { useLocation } from "wouter";
import HeadV2 from "~components/popup/HeadV2";
import { WarningIcon } from "~components/popup/Token";
import browser from "webextension-polyfill";
import { Degraded, WarningWrapper } from "~routes/popup/send";
import { formatAddress } from "~utils/format";
import { getKeyfile, type DecryptedWallet } from "~wallets";
import { freeDecryptedWallet } from "~wallets/encryption";
import {
AddressField,
ContentWrapper,
CopyAction,
QRCodeWrapper,
Wrapper
} from "~routes/popup/receive";
import { dataToFrames } from "qrloop";
import { checkPassword } from "~wallets/auth";

export default function GenerateQR({ address }: { address: string }) {
// wallets
const [wallets] = useStorage<StoredWallet[]>(
{
key: "wallets",
instance: ExtensionStorage
},
[]
);
const [wallet, setWallet] = useState<DecryptedWallet>(null);
const [copied, setCopied] = useState(false);
const [loading, setLoading] = useState(false);
const [frames, setFrames] = useState<string[]>([]);

// this wallet
const wallet = useMemo(
() => wallets?.find((w) => w.address === address),
[wallets, address]
);
const [, setLocation] = useLocation();
const { setToast } = useToasts();
const passwordInput = useInput();

const isHardware = wallet?.type === "hardware";

const copyAddress: MouseEventHandler = (e) => {
e.stopPropagation();
copy(address);
setCopied(true);
setTimeout(() => setCopied(false), 1000);
setToast({
type: "success",
duration: 2000,
content: `${formatAddress(address, 3)} ${browser.i18n.getMessage(
"copied_address_2"
)}`
});
};

if (!wallet) return <></>;
async function generateQr() {
try {
setLoading(true);
const isPasswordCorrect = await checkPassword(passwordInput.state);
if (isPasswordCorrect) {
const wallet = await getKeyfile(address);
setWallet(wallet);
} else {
passwordInput.setStatus("error");
setToast({
type: "error",
content: browser.i18n.getMessage("invalidPassword"),
duration: 2200
});
}
} catch {
} finally {
setLoading(false);
}
}

return <Receive walletName={wallet.nickname} walletAddress={address} />;
useEffect(() => {
if ((wallet as any)?.keyfile) {
setFrames(dataToFrames(JSON.stringify((wallet as any)?.keyfile)));
freeDecryptedWallet((wallet as any).keyfile);
}
}, [wallet]);

useEffect(() => {
return () => setFrames([]);
}, []);

return (
<Wrapper>
<div>
<HeadV2
title={
wallet
? wallet?.nickname ?? "Account"
: browser.i18n.getMessage("generate_qr_code")
}
back={() => {
if (address) {
setLocation(`/quick-settings/wallets/${address}`);
} else {
setLocation("/");
}
}}
/>
</div>
{wallet ? (
<div>
{isHardware ? (
<Degraded
style={{
justifyContent: "center",
alignItems: "center"
}}
>
<WarningWrapper>
<WarningIcon color="#fff" />
</WarningWrapper>
<div>
<span>
{browser.i18n.getMessage("cannot_generate_qr_code")}
</span>
</div>
</Degraded>
) : (
<ContentWrapper>
<Section style={{ padding: "8px 15px 0 15px" }}>
<QRCodeWrapper>
<QRCodeLoop frames={frames} fps={5} size={275} />
</QRCodeWrapper>
</Section>
<Section style={{ padding: "8px 15px 0 15px" }}>
<AddressField fullWidth onClick={copyAddress}>
{formatAddress(address ?? "", 6)}
<TooltipV2
content={browser.i18n.getMessage("copy_address")}
position="bottom"
>
<CopyAction as={copied ? CheckIcon : CopyIcon} />
</TooltipV2>
</AddressField>
</Section>
</ContentWrapper>
)}
</div>
) : (
<Section style={{ paddingLeft: "1rem", paddingRight: "1rem" }}>
<Text style={{ fontSize: "0.98rem" }}>
{browser.i18n.getMessage("generate_qr_code_title")}
</Text>
<InputV2
small
type="password"
placeholder={browser.i18n.getMessage("password")}
{...passwordInput.bindings}
fullWidth
onKeyDown={(e) => {
if (e.key !== "Enter") return;
generateQr();
}}
/>
<Spacer y={1} />
<ButtonV2 fullWidth onClick={generateQr} loading={loading}>
{browser.i18n.getMessage("generate")}
</ButtonV2>
</Section>
)}
</Wrapper>
);
}

const QRCodeLoop = ({
frames,
size,
fps
}: {
frames: string[];
size: number;
fps: number;
}) => {
const [frame, setFrame] = useState(0);
const rafRef = useRef(null);

useEffect(() => {
const nextFrame = (frame: number, frames: string[]) => {
frame = (frame + 1) % frames.length;
return frame;
};

let lastT: number;
const loop = (t: number) => {
rafRef.current = requestAnimationFrame(loop);
if (!lastT) lastT = t;
if ((t - lastT) * fps < 1000) return;
lastT = t;
setFrame((prevFrame) => nextFrame(prevFrame, frames));
};
rafRef.current = requestAnimationFrame(loop);

return () => {
cancelAnimationFrame(rafRef.current);
};
}, [frames, fps]);

return (
<div style={{ position: "relative", width: size, height: size }}>
{frames.map((chunk: any, i: Key) => (
<div
key={i}
style={{ position: "absolute", opacity: i === frame ? 1 : 0 }}
>
<QRCodeSVG
fgColor="#fff"
bgColor="transparent"
size={size}
value={chunk}
/>
</div>
))}
</div>
);
};
Loading

0 comments on commit f76d6e2

Please sign in to comment.