diff --git a/.changeset/eager-walls-tease.md b/.changeset/eager-walls-tease.md new file mode 100644 index 000000000..73dd5a393 --- /dev/null +++ b/.changeset/eager-walls-tease.md @@ -0,0 +1,13 @@ +--- +'@reown/appkit-react-native': patch +'@reown/appkit-ui-react-native': patch +'@reown/appkit-bitcoin-react-native': patch +'@reown/appkit-coinbase-react-native': patch +'@reown/appkit-common-react-native': patch +'@reown/appkit-core-react-native': patch +'@reown/appkit-ethers-react-native': patch +'@reown/appkit-solana-react-native': patch +'@reown/appkit-wagmi-react-native': patch +--- + +chore: round logo from qr code + added borderWidth 0 to card component diff --git a/packages/appkit/src/modal/w3m-modal/styles.ts b/packages/appkit/src/modal/w3m-modal/styles.ts index f7109eb1a..041a7131d 100644 --- a/packages/appkit/src/modal/w3m-modal/styles.ts +++ b/packages/appkit/src/modal/w3m-modal/styles.ts @@ -3,6 +3,7 @@ import { StyleSheet } from 'react-native'; export default StyleSheet.create({ card: { borderBottomLeftRadius: 0, - borderBottomRightRadius: 0 + borderBottomRightRadius: 0, + borderWidth: 0 } }); diff --git a/packages/appkit/src/partials/w3m-connecting-qrcode/index.tsx b/packages/appkit/src/partials/w3m-connecting-qrcode/index.tsx index c82eb4389..b2ef67a57 100644 --- a/packages/appkit/src/partials/w3m-connecting-qrcode/index.tsx +++ b/packages/appkit/src/partials/w3m-connecting-qrcode/index.tsx @@ -20,6 +20,9 @@ import styles from './styles'; import { ReownButton } from './components/ReownButton'; import { useWindowDimensions } from 'react-native'; +const LOGO_SIZE = 60; +const LOGO_BORDER_RADIUS = 10; + export function ConnectingQrCode() { const { height, width } = useWindowDimensions(); const windowSize = Math.min(height, width); @@ -65,7 +68,13 @@ export function ConnectingQrCode() { flexDirection={isPortrait ? 'column' : 'row'} padding={['xl', 'xl', 'xs', 'xl']} > - + Scan this QR code with your phone {showCopy ? ( diff --git a/packages/ui/src/assets/svg/WalletConnect.tsx b/packages/ui/src/assets/svg/WalletConnect.tsx index d8884b3b2..d3be9f9f6 100644 --- a/packages/ui/src/assets/svg/WalletConnect.tsx +++ b/packages/ui/src/assets/svg/WalletConnect.tsx @@ -1,14 +1,13 @@ import Svg, { ClipPath, Defs, G, Path, type SvgProps } from 'react-native-svg'; const SvgWalletConnect = (props: SvgProps) => ( - + ); diff --git a/packages/ui/src/composites/wui-qr-code/index.tsx b/packages/ui/src/composites/wui-qr-code/index.tsx index dd598dc7b..ccf0374ef 100644 --- a/packages/ui/src/composites/wui-qr-code/index.tsx +++ b/packages/ui/src/composites/wui-qr-code/index.tsx @@ -1,6 +1,6 @@ import { memo, useMemo } from 'react'; import { View, type StyleProp, type ViewStyle } from 'react-native'; -import Svg from 'react-native-svg'; +import Svg, { Circle, Line, Rect } from 'react-native-svg'; import { Icon } from '../../components/wui-icon'; import { Image } from '../../components/wui-image'; import { Shimmer } from '../../components/wui-shimmer'; @@ -17,17 +17,32 @@ export interface QrCodeProps { testID?: string; arenaClear?: boolean; style?: StyleProp; + logoSize?: number; + logoBorderRadius?: number; } -export function QrCode_({ size, uri, imageSrc, testID, arenaClear, icon, style }: QrCodeProps) { +export function QrCode_({ + size, + uri, + imageSrc, + testID, + arenaClear, + icon, + style, + logoSize, + logoBorderRadius +}: QrCodeProps) { const Theme = LightTheme; const containerPadding = Spacing.l; const qrSize = size - containerPadding * 2; - const logoSize = arenaClear ? 0 : qrSize / 4; + const _logoSize = arenaClear ? 0 : logoSize ?? qrSize / 4; - const dots = useMemo( - () => (uri ? QRCodeUtil.generate(uri, qrSize, logoSize) : []), - [uri, qrSize, logoSize] + const dotColor = Theme['inverse-000']; + const edgeColor = Theme['inverse-100']; + + const qrData = useMemo( + () => (uri ? QRCodeUtil.generate(uri, qrSize, _logoSize, logoBorderRadius) : null), + [uri, qrSize, _logoSize, logoBorderRadius] ); const logoTemplate = () => { @@ -40,8 +55,12 @@ export function QrCode_({ size, uri, imageSrc, testID, arenaClear, icon, style } ); @@ -51,14 +70,18 @@ export function QrCode_({ size, uri, imageSrc, testID, arenaClear, icon, style } ); }; - return uri ? ( + if (!uri || !qrData) { + return ; + } + + return ( - {dots} + {/* Render rectangles */} + {qrData.rects.map(rect => ( + + ))} + + {/* Render circles */} + {qrData.circles.map(circle => ( + + ))} + + {/* Render lines */} + {qrData.lines.map(line => ( + + ))} {logoTemplate()} - ) : ( - ); } @@ -81,6 +139,7 @@ export const QrCode = memo(QrCode_, (prevProps, nextProps) => { return ( prevProps.size === nextProps.size && prevProps.uri === nextProps.uri && - prevProps.style === nextProps.style + prevProps.style === nextProps.style && + prevProps.logoBorderRadius === nextProps.logoBorderRadius ); }); diff --git a/packages/ui/src/composites/wui-qr-code/styles.ts b/packages/ui/src/composites/wui-qr-code/styles.ts index 9503ff74b..7b3c0b1bc 100644 --- a/packages/ui/src/composites/wui-qr-code/styles.ts +++ b/packages/ui/src/composites/wui-qr-code/styles.ts @@ -8,8 +8,5 @@ export default StyleSheet.create({ borderRadius: BorderRadius.l, padding: Spacing.l, alignSelf: 'center' - }, - icon: { - position: 'absolute' } }); diff --git a/packages/ui/src/utils/QRCodeUtil.tsx b/packages/ui/src/utils/QRCodeUtil.tsx index 3bb6f01eb..26e2800bf 100644 --- a/packages/ui/src/utils/QRCodeUtil.tsx +++ b/packages/ui/src/utils/QRCodeUtil.tsx @@ -1,15 +1,32 @@ -import type { ReactNode } from 'react'; -import { Line, Rect, Circle } from 'react-native-svg'; import QRCode from 'qrcode'; -import { LightTheme } from '../utils/ThemeUtil'; - -type CoordinateMapping = [number, number[]]; const CONNECTING_ERROR_MARGIN = 0.1; const CIRCLE_SIZE_MODIFIER = 2.5; const QRCODE_MATRIX_MARGIN = 7; +const LOGO_PADDING = 25; + +export interface QRData { + rects: { + x: number; + y: number; + size: number; + fillType: 'dot' | 'edge'; + }[]; + circles: { + cx: number; + cy: number; + r: number; + }[]; + lines: { + x1: number; + x2: number; + y1: number; + y2: number; + strokeWidth: number; + }[]; +} -function isAdjecentDots(cy: number, otherCy: number, cellSize: number) { +function isAdjacentDots(cy: number, otherCy: number, cellSize: number) { if (cy === otherCy) { return false; } @@ -18,7 +35,10 @@ function isAdjecentDots(cy: number, otherCy: number, cellSize: number) { return diff <= cellSize + CONNECTING_ERROR_MARGIN; } -function getMatrix(value: string, errorCorrectionLevel: QRCode.QRCodeErrorCorrectionLevel) { +function getMatrix( + value: string, + errorCorrectionLevel: QRCode.QRCodeErrorCorrectionLevel +): boolean[][] { const arr = Array.prototype.slice.call( QRCode.create(value, { errorCorrectionLevel }).modules.data, 0 @@ -32,154 +52,257 @@ function getMatrix(value: string, errorCorrectionLevel: QRCode.QRCodeErrorCorrec ); } -export const QRCodeUtil = { - generate(uri: string, size: number, logoSize: number) { - const dotColor = LightTheme['inverse-000']; - const edgeColor = LightTheme['inverse-100']; - const dots: ReactNode[] = []; - const matrix = getMatrix(uri, 'Q'); - const cellSize = size / matrix.length; - const qrList = [ - { x: 0, y: 0 }, - { x: 1, y: 0 }, - { x: 0, y: 1 } - ]; - - qrList.forEach(({ x, y }) => { - const x1 = (matrix.length - QRCODE_MATRIX_MARGIN) * cellSize * x; - const y1 = (matrix.length - QRCODE_MATRIX_MARGIN) * cellSize * y; - const borderRadius = 0.32; - for (let i = 0; i < qrList.length; i += 1) { - const dotSize = cellSize * (QRCODE_MATRIX_MARGIN - i * 2); - dots.push( - - ); +function processQRMatrix( + matrix: boolean[][], + size: number, + logoSize: number, + logoBorderRadius?: number +): QRData { + const matrixLength = matrix.length; + const cellSize = size / matrixLength; + const halfCellSize = cellSize / 2; + const strokeWidth = cellSize / (CIRCLE_SIZE_MODIFIER / 2); + const circleRadius = cellSize / CIRCLE_SIZE_MODIFIER; + + const rects: QRData['rects'] = []; + const circles: QRData['circles'] = []; + const lines: QRData['lines'] = []; + + // Generate corner rectangles - optimized with direct indexing + const qrList = [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 0, y: 1 } + ]; + const baseOffset = (matrixLength - QRCODE_MATRIX_MARGIN) * cellSize; + + for (let qrIdx = 0; qrIdx < 3; qrIdx++) { + const qr = qrList[qrIdx]!; + const x1 = baseOffset * qr.x; + const y1 = baseOffset * qr.y; + + for (let i = 0; i < 3; i++) { + const dotSize = cellSize * (QRCODE_MATRIX_MARGIN - i * 2); + rects.push({ + x: x1 + cellSize * i, + y: y1 + cellSize * i, + size: dotSize, + fillType: i % 2 === 0 ? 'dot' : 'edge' + }); + } + } + + const circleCoords: [number, number][] = []; + + // Determine if using circular or rounded rectangle hole + const isCircular = logoBorderRadius === undefined; + const effectiveBorderRadius = logoBorderRadius ?? (logoSize + LOGO_PADDING) / 2; + + // Calculate circle coordinates - optimized with configurable hole shape + for (let i = 0; i < matrixLength; i++) { + const row = matrix[i]!; + const rowLength = row.length; + + for (let j = 0; j < rowLength; j++) { + if (!row[j]) continue; + + // Skip corners check + if ( + (i < QRCODE_MATRIX_MARGIN && j < QRCODE_MATRIX_MARGIN) || + (i > matrixLength - (QRCODE_MATRIX_MARGIN + 1) && j < QRCODE_MATRIX_MARGIN) || + (i < QRCODE_MATRIX_MARGIN && j > matrixLength - (QRCODE_MATRIX_MARGIN + 1)) + ) { + continue; } - }); - - const clearArenaSize = Math.floor((logoSize + 25) / cellSize); - const matrixMiddleStart = matrix.length / 2 - clearArenaSize / 2; - const matrixMiddleEnd = matrix.length / 2 + clearArenaSize / 2 - 1; - const circles: [number, number][] = []; - - // Getting coordinates for each of the QR code dots - matrix.forEach((row: QRCode.QRCode[], i: number) => { - row.forEach((_, j: number) => { - if (matrix[i][j]) { - if ( - !( - (i < QRCODE_MATRIX_MARGIN && j < QRCODE_MATRIX_MARGIN) || - (i > matrix.length - (QRCODE_MATRIX_MARGIN + 1) && j < QRCODE_MATRIX_MARGIN) || - (i < QRCODE_MATRIX_MARGIN && j > matrix.length - (QRCODE_MATRIX_MARGIN + 1)) - ) - ) { - if ( - !( - i > matrixMiddleStart && - i < matrixMiddleEnd && - j > matrixMiddleStart && - j < matrixMiddleEnd - ) - ) { - const cx = i * cellSize + cellSize / 2; - const cy = j * cellSize + cellSize / 2; - circles.push([cx, cy]); - } - } + + // Calculate pixel coordinates first + const cx = i * cellSize + halfCellSize; + const cy = j * cellSize + halfCellSize; + + // Skip hole calculation if logoSize is 0 (arenaClear) + if (logoSize === 0) { + circleCoords.push([cx, cy]); + continue; + } + + // Calculate distance from center in pixel space + const centerX = size / 2; + const centerY = size / 2; + + let isOutsideLogoArea = false; + + if (isCircular) { + // Circular hole + const dx = cx - centerX; + const dy = cy - centerY; + const distanceFromCenter = Math.sqrt(dx * dx + dy * dy); + const pixelRadius = (logoSize + LOGO_PADDING) / 2; + isOutsideLogoArea = distanceFromCenter >= pixelRadius; + } else { + // Rounded rectangle hole + const halfLogoArea = (logoSize + LOGO_PADDING) / 2; + const dx = Math.abs(cx - centerX); + const dy = Math.abs(cy - centerY); + + // Check if point is outside the rounded rectangle + if (dx > halfLogoArea || dy > halfLogoArea) { + isOutsideLogoArea = true; + } else if ( + dx > halfLogoArea - effectiveBorderRadius && + dy > halfLogoArea - effectiveBorderRadius + ) { + // Check corner radius + const cornerDx = dx - (halfLogoArea - effectiveBorderRadius); + const cornerDy = dy - (halfLogoArea - effectiveBorderRadius); + const cornerDistance = Math.sqrt(cornerDx * cornerDx + cornerDy * cornerDy); + isOutsideLogoArea = cornerDistance >= effectiveBorderRadius; + } else { + isOutsideLogoArea = false; } + } + + if (isOutsideLogoArea) { + circleCoords.push([cx, cy]); + } + } + } + + // Build circlesToConnect - optimized loop + const circlesToConnect: Record = {}; + for (let k = 0; k < circleCoords.length; k++) { + const [cx, cy] = circleCoords[k]!; + const existing = circlesToConnect[cx]; + if (existing) { + existing.push(cy); + } else { + circlesToConnect[cx] = [cy]; + } + } + + // Process circles and lines - optimized to avoid Object.entries + for (const cxKey in circlesToConnect) { + const cx = Number(cxKey); + const cys = circlesToConnect[cxKey]!; + + if (cys.length === 1) { + const firstCy = cys[0]; + if (firstCy === undefined) continue; + + // Single dot, add as circle + circles.push({ + cx, + cy: firstCy, + r: circleRadius }); - }); + continue; + } - // Cx to multiple cys - const circlesToConnect: Record = {}; + // Sort once for line grouping + cys.sort((a, b) => a - b); - // Mapping all dots cicles on the same x axis - circles.forEach(([cx, cy]) => { - if (circlesToConnect[cx]) { - circlesToConnect[cx]?.push(cy); - } else { - circlesToConnect[cx] = [cy]; + // Track which dots are connected and which are lonely + const isConnected = new Array(cys.length).fill(false); + + // Find all adjacent pairs + for (let i = 0; i < cys.length - 1; i++) { + const currentCy = cys[i]; + const nextCy = cys[i + 1]; + if ( + currentCy !== undefined && + nextCy !== undefined && + isAdjacentDots(currentCy, nextCy, cellSize) + ) { + isConnected[i] = true; + isConnected[i + 1] = true; } - }); - - // Drawing lonely dots - Object.entries(circlesToConnect) - // Only get dots that have neighbors - .map(([cx, cys]) => { - const newCys = cys.filter(cy => - cys.every(otherCy => !isAdjecentDots(cy, otherCy, cellSize)) - ); - - return [Number(cx), newCys] as CoordinateMapping; - }) - .forEach(([cx, cys]) => { - cys.forEach(cy => { - dots.push( - - ); + } + + // Add lonely dots as circles and build line groups + let groupStart = -1; + let groupEnd = -1; + + for (let i = 0; i < cys.length; i++) { + const cy = cys[i]; + if (cy === undefined) continue; + + if (!isConnected[i]) { + // Lonely dot - add as circle + circles.push({ + cx, + cy, + r: circleRadius }); - }); - // Drawing lines for dots that are close to each other - Object.entries(circlesToConnect) - // Only get dots that have more than one dot on the x axis - .filter(([_, cys]) => cys.length > 1) - // Removing dots with no neighbors - .map(([cx, cys]) => { - const newCys = cys.filter(cy => cys.some(otherCy => isAdjecentDots(cy, otherCy, cellSize))); - - return [Number(cx), newCys] as CoordinateMapping; - }) - // Get the coordinates of the first and last dot of a line - .map(([cx, cys]) => { - cys.sort((a, b) => (a < b ? -1 : 1)); - const groups: number[][] = []; - - for (const cy of cys) { - const group = groups.find(item => - item.some(otherCy => isAdjecentDots(cy, otherCy, cellSize)) - ); - if (group) { - group.push(cy); + // Finish any ongoing line group + if (groupStart !== -1 && groupEnd !== -1 && groupStart !== groupEnd) { + lines.push({ + x1: cx, + x2: cx, + y1: groupStart, + y2: groupEnd, + strokeWidth + }); + } + groupStart = -1; + groupEnd = -1; + } else { + // Part of a line group + if (groupStart === -1) { + groupStart = cy; + groupEnd = cy; + } else { + // Check if adjacent to previous + const prevCy = cys[i - 1]; + if (i > 0 && prevCy !== undefined && isAdjacentDots(cy, prevCy, cellSize)) { + groupEnd = cy; } else { - groups.push([cy]); + // Gap in the group, finish previous line + if (groupStart !== groupEnd) { + lines.push({ + x1: cx, + x2: cx, + y1: groupStart, + y2: groupEnd, + strokeWidth + }); + } + groupStart = cy; + groupEnd = cy; } } + } + } - return [cx, groups.map(item => [item[0], item[item.length - 1]])] as [number, number[][]]; - }) - .forEach(([cx, groups]) => { - groups.forEach(([y1, y2]) => { - dots.push( - - ); - }); + // Don't forget the last group + if (groupStart !== -1 && groupEnd !== -1 && groupStart !== groupEnd) { + lines.push({ + x1: cx, + x2: cx, + y1: groupStart, + y2: groupEnd, + strokeWidth }); + } + } - return dots; + return { rects, circles, lines }; +} + +export function generateQRData( + uri: string, + size: number, + logoSize: number, + logoBorderRadius?: number +): QRData { + if (!uri || size <= 0) { + throw new Error('Invalid QR code parameters'); } + + const matrix = getMatrix(uri, 'Q'); + + return processQRMatrix(matrix, size, logoSize, logoBorderRadius); +} + +export const QRCodeUtil = { + generate: generateQRData };