Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to custom-rendered QR codes #220

Merged
merged 2 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
130 changes: 130 additions & 0 deletions components/QRCode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Box, Flex, Image } from '@chakra-ui/react'
import { Fragment, memo } from 'react'
import QRCodeLib from 'qrcode'
import NextImage, { StaticImageData } from 'next/image'
import { isDataURL } from '../helpers/urls'
import { convertToPixels } from '../helpers/sizes'

type QRCodeProps = {
value: string
image?: string | StaticImageData
size?: string
imageSize?: string
}

const QRCode = memo(function QRCode({
value,
image,
size,
imageSize,
}: QRCodeProps) {
const qr = QRCodeLib.create(value, {
errorCorrectionLevel: 'H',
})
const modules1D = qr.modules.data
const modules: boolean[][] = []
const length = Math.sqrt(modules1D.length)

// Check if the module is part of the finder pattern
const isFindingPattern = (x: number, y: number) =>
(x < 8 && (y < 8 || y >= length - 8)) || (x >= length - 8 && y < 8)

// Compute the scale factor so we can determine which modules are behind the image
let isModuleBehindImage: (x: number, y: number) => boolean = () => false
if (image) {
const scaleFactor = length / convertToPixels(size)
const imageViewboxSize = convertToPixels(imageSize) * scaleFactor
const imagePadding = 1
const imageTop = Math.floor((length - imageViewboxSize) / 2 - imagePadding)
const imageBottom = Math.ceil(
imageTop + imageViewboxSize + imagePadding * 2,
)
const imageLeft = Math.floor((length - imageViewboxSize) / 2 - imagePadding)
const imageRight = Math.ceil(
imageLeft + imageViewboxSize + imagePadding * 2,
)
isModuleBehindImage = (x: number, y: number) => {
return (
x >= imageTop && x < imageBottom && y >= imageLeft && y < imageRight
)
}
}

for (let i = 0; i < length; i++) {
modules.push(
[...modules1D.slice(i * length, (i + 1) * length)].map((bit, j) => {
return bit && !isFindingPattern(i, j) && !isModuleBehindImage(i, j)
}),
)
}

// Positions of the finder patterns used for custom rendering
const findingPatternPositions = [
[0, 0],
[0, length - 7],
[length - 7, 0],
]

return (
<Box position="relative" width={size} height={size}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox={`0 0 ${modules.length} ${modules.length}`}
width={size}
height={size}
>
{modules.map((row, y) =>
row.map((cell, x) => (
<circle
key={`${x}-${y}`}
cx={x + 0.5}
cy={y + 0.5}
r={0.4}
fill={cell ? 'black' : 'white'}
/>
)),
)}

{findingPatternPositions.map(([x, y]) => (
<Fragment key={`${x}-${y}`}>
<rect x={x} y={y} width={7} height={7} fill="black" rx={2} ry={2} />

<rect
x={x + 1}
y={y + 1}
width={5}
height={5}
fill="white"
rx={1.5}
ry={1.5}
/>

<rect x={x + 2} y={y + 2} width={3} height={3} fill="black" />
</Fragment>
))}
</svg>
<Flex
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
justifyContent="center"
alignItems="center"
>
{image && (
<Image
as={isDataURL(image as any) ? 'img' : NextImage}
src={image as any}
alt="QR Code"
objectFit="contain"
boxSize={imageSize}
borderRadius="lg"
/>
)}
</Flex>
</Box>
)
})

export default QRCode
4 changes: 2 additions & 2 deletions components/views/ScanConnect.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Box, Stack, Text } from '@chakra-ui/react'
import QRCode from 'react-qr-code'
import { Wallet } from '../../data/wallets'
import { useConfig } from '../../contexts/ConfigContext'
import QRCode from '../QRCode'

interface ScanConnectProps {
wallet: Wallet
Expand All @@ -23,7 +23,7 @@ export default function ScanConnect({ wallet }: ScanConnectProps) {
border="1px"
borderColor="gray.200"
>
<QRCode level="M" value={walletConnectUri} size={300} />
<QRCode value={walletConnectUri} size="300px" />
</Box>
</Stack>
)
Expand Down
4 changes: 2 additions & 2 deletions components/views/ScanInstall.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box, Button, Stack, Text } from '@chakra-ui/react'
import QRCode from 'react-qr-code'
import { Wallet } from '../../data/wallets'
import QRCode from '../QRCode'

interface ScanInstallProps {
onContinue: () => void
Expand All @@ -22,7 +22,7 @@ export default function ScanInstall({ onContinue, wallet }: ScanInstallProps) {
border="1px"
borderColor="gray.200"
>
<QRCode level="M" value={wallet.installLink?.mobile} size={300} />
<QRCode value={wallet.installLink?.mobile} size="300px" />
</Box>

<Button
Expand Down
24 changes: 24 additions & 0 deletions helpers/sizes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export function convertToPixels(
size: string,
context = document.documentElement,
) {
// Create a temporary element
const tempEl = document.createElement('div')
Copy link
Member

Choose a reason for hiding this comment

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

This is expensive. You could reuse a hidden element here if you wanted to optimize more for performance. You could also cache the result in a map by size.

Copy link
Contributor Author

@jribbink jribbink Aug 7, 2024

Choose a reason for hiding this comment

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

The QRCode component is memoized, but yea makes sense to do this indidividual function as well. I'll wrap this in a useMemo 👍


// Set the element's style
tempEl.style.width = size
tempEl.style.position = 'absolute'
tempEl.style.visibility = 'hidden'

// Append the element to the context (usually the document or a specific element)
context.appendChild(tempEl)

// Get the computed width in pixels
const pixels = window.getComputedStyle(tempEl).width

// Remove the temporary element
context.removeChild(tempEl)

// Return the width as a number (parseFloat removes the 'px' unit)
return parseFloat(pixels)
}
Loading
Loading