Skip to content
This repository has been archived by the owner on Feb 3, 2025. It is now read-only.

Turn off Qr scanner after pasting from clipboard #203

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 114 additions & 10 deletions frontend/src/components/QrCodeScanner.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
import { SendConfirmParams } from "../routes/SendConfirm"
import { detectPaymentType, objectToSearchParams, PaymentType, toastAnything } from "@util/dumb"
import { Html5Qrcode, Html5QrcodeSupportedFormats } from "html5-qrcode"
import { memo, useEffect, useRef, useState } from "react"
import { memo, useContext, useEffect, useRef, useState } from "react"
import toast from "react-hot-toast"
import ActionButton from "./ActionButton"
import { useNavigate } from "react-router-dom"
import { NodeManager } from "node-manager";
import { NodeManagerContext } from "./GlobalStateProvider"
import { useSearchParams } from "react-router-dom";
import { inputStyle } from "../styles"
import bip21 from "bip21"

type Props = {
autoStart?: boolean
onCodeDetected: (barcodeValue: string) => any
onValidCode: (data: any) => Promise<void>
}

type UnifiedQrOptions =
{
amount?: number;
lightning?: string;
label?: string;
message?: string;
};

export type MutinyBip21 = { address: string, options: UnifiedQrOptions };

const QrCodeDetectorComponent = ({
autoStart = true,
onCodeDetected,
Expand All @@ -16,6 +36,82 @@ const QrCodeDetectorComponent = ({
const [errorMessage, setErrorMessage] = useState("")
const [cameraReady, setCameraReady] = useState<boolean>(false)
const qrCodeRef = useRef<Html5Qrcode | null>(null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the move here is to raise this qrCodeRef up into Send.tsx, rather than lowering the navigate functions into here. that way Send.tsx can call anything it needs for cleanup on the ref.

ideally the QrCodeScanner stays rather dumb and generic (ideally it should handle other scenarios, like imagine it's being used for lnurl auth... it detects a valid qr code and then runs whatever callbacks have been passed to it from its parent.)

const [textFieldDestination, setDestination] = useState("")
const { nodeManager } = useContext(NodeManagerContext);
const [searchParams] = useSearchParams();
const sendAll = searchParams.get("all")
let navigate = useNavigate();

async function navigateForInvoice(invoiceStr: string) {
try {
let invoice = await nodeManager?.decode_invoice(invoiceStr);
console.table(invoice);
if (invoice?.amount_sats && Number(invoice?.amount_sats) > 0) {
const params = objectToSearchParams<SendConfirmParams>({ destination: invoiceStr, amount: invoice?.amount_sats.toString(), description: invoice?.description || undefined })
navigate(`/send/confirm?${params}`)
} else {
const params = objectToSearchParams<SendConfirmParams>({ destination: invoiceStr, description: invoice?.description || undefined })
navigate(`/send/amount?${params}`)
}
} catch (e) {
console.error(e);
toastAnything(e);
}
}

async function navigateForBip21(bip21String: string) {
const { address, options } = bip21.decode(bip21String) as MutinyBip21;
if (options?.lightning) {
await navigateForInvoice(options.lightning)
} else if (options?.amount) {
try {
const amount = NodeManager.convert_btc_to_sats(options.amount ?? 0.0);
if (!amount) {
throw new Error("Failed to convert BTC to sats")
}
if (options.label) {
const params = objectToSearchParams<SendConfirmParams>({ destination: address, amount: amount.toString(), description: options.label })
navigate(`/send/confirm?${params}`)
} else {
const params = objectToSearchParams<SendConfirmParams>({ destination: address, amount: amount.toString() })
navigate(`/send/confirm?${params}`)
}
} catch (e) {
console.error(e)
toastAnything(e);
}
}
}

async function handleContinue(qrRead?: string) {
let destination: string = qrRead || textFieldDestination;
if (!destination) {
toast("You didn't paste anything!");
return
}

let paymentType = detectPaymentType(destination)

if (paymentType === PaymentType.unknown) {
toast("Couldn't parse that one, buddy")
return
}

if (paymentType === PaymentType.invoice) {
await navigateForInvoice(destination)
} else if (paymentType === PaymentType.bip21) {
await navigateForBip21(destination)
} else if (paymentType === PaymentType.onchain) {
if (sendAll === "true") {
const params = objectToSearchParams<SendConfirmParams>({ destination, all: "true" })
navigate(`/send/confirm?${params}`)
} else {
const params = objectToSearchParams<SendConfirmParams>({ destination })
navigate(`/send/amount?${params}`)
}
}
qrCodeRef.current?.stop()
}

useEffect(() => {
if (detecting) {
Expand Down Expand Up @@ -57,17 +153,25 @@ const QrCodeDetectorComponent = ({
}, [detecting, onCodeDetected, onValidCode])

return (
<div>
{errorMessage && <h1 className="text-2xl font-light">{errorMessage}</h1>}
<div>
<div className={cameraReady ? "border-2 border-green" : ""}>
<div id="qrCodeCamera" />
<>
<div className="qrContainer">
{errorMessage && <h1 className="text-2xl font-light">{errorMessage}</h1>}
<div>
<div className={cameraReady ? "border-2 border-green" : ""}>
<div id="qrCodeCamera" />
</div>
{!cameraReady &&
<h1 className="text-2xl font-light">Loading scanner...</h1>
}
</div>
{!cameraReady &&
<h1 className="text-2xl font-light">Loading scanner...</h1>
}
</div>
</div>
<div className="inputContainer">
<input onChange={e => setDestination(e.target.value)} value={textFieldDestination} className={`w-full ${inputStyle({ accent: "green" })}`} type="text" placeholder='Paste invoice, pubkey, or address' />
<ActionButton onClick={() => handleContinue(undefined)}>
Continue
</ActionButton>
</div>
</>
)
}

Expand Down
13 changes: 13 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,16 @@ dd {
*::-webkit-scrollbar {
display: none;
}

.qrContainer {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for one-off styling I prefer inline tailwind when possible

width: 70%;
margin: 1rem auto 0 auto;
}

.inputContainer {
width: 70%;
margin: 0 auto auto auto;
}
.inputContainer > input {
margin: 0 auto 1rem auto
}
8 changes: 1 addition & 7 deletions frontend/src/routes/Send.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { useNavigate } from "react-router";
import Close from "../components/Close";
import PageTitle from "../components/PageTitle";
import ScreenMain from "../components/ScreenMain";
import { inputStyle } from "../styles";
import toast from "react-hot-toast"
import MutinyToaster from "../components/MutinyToaster";
import { detectPaymentType, objectToSearchParams, PaymentType, toastAnything } from "@util/dumb";
Expand All @@ -12,7 +11,6 @@ import bip21 from "bip21"
import { NodeManager } from "node-manager";
import { QrCodeScanner } from "@components/QrCodeScanner";
import { SendConfirmParams } from "./SendConfirm";
import ActionButton from "@components/ActionButton";
import { useSearchParams } from "react-router-dom";

type UnifiedQrOptions =
Expand All @@ -29,7 +27,7 @@ function Send() {
const { nodeManager } = useContext(NodeManagerContext);
let navigate = useNavigate();

const [textFieldDestination, setDestination] = useState("")
const [textFieldDestination] = useState("")

const [searchParams] = useSearchParams();
const sendAll = searchParams.get("all")
Expand Down Expand Up @@ -129,10 +127,6 @@ function Send() {
</header>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like it would help if the component would accept some onClose callback that overrides the default behavior of navigating to root, that way we can pass cleanup logic when necessary

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

referring to the <Close /> component

<ScreenMain>
<QrCodeScanner onValidCode={onValidCode} onCodeDetected={onCodeDetected} />
<input onChange={e => setDestination(e.target.value)} value={textFieldDestination} className={`w-full ${inputStyle({ accent: "green" })}`} type="text" placeholder='Paste invoice, pubkey, or address' />
<ActionButton onClick={() => handleContinue(undefined)}>
Continue
</ActionButton>
</ScreenMain>
<MutinyToaster />
</>
Expand Down