-
Notifications
You must be signed in to change notification settings - Fork 231
feat(pair): generate pairing download QR per entrypoint (Block) #20716
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
Open
vbudhram
wants to merge
1
commit into
main
Choose a base branch
from
fxa-13720
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
packages/fxa-settings/src/components/QRCode/index.stories.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| import { Meta } from '@storybook/react'; | ||
| import { withLocalization } from 'fxa-react/lib/storybooks'; | ||
| import QRCode from '.'; | ||
| import firefoxLogo from '../../pages/Pair/Index/firefox-logo-browser.svg'; | ||
|
|
||
| export default { | ||
| title: 'Components/QRCode', | ||
| component: QRCode, | ||
| decorators: [withLocalization], | ||
| } as Meta; | ||
|
|
||
| const value = 'https://app.adjust.com/2uo1qc?campaign=send-tab&creative=demo'; | ||
|
|
||
| export const Default = () => ( | ||
| <QRCode value={value} localizedLabel="Scan to download Firefox for mobile" /> | ||
| ); | ||
|
|
||
| export const WithLogo = () => ( | ||
| <QRCode | ||
| value={value} | ||
| localizedLabel="Scan to download Firefox for mobile" | ||
| logoSrc={firefoxLogo} | ||
| /> | ||
| ); | ||
|
|
||
| export const Loading = () => ( | ||
| <QRCode | ||
| value={value} | ||
| localizedLabel="Scan to download Firefox for mobile" | ||
| logoSrc={firefoxLogo} | ||
| loading | ||
| /> | ||
| ); |
91 changes: 91 additions & 0 deletions
91
packages/fxa-settings/src/components/QRCode/index.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| import React from 'react'; | ||
| import { act, screen } from '@testing-library/react'; | ||
| import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; | ||
| import QRCode from './index'; | ||
|
|
||
| const VALUE = 'https://app.adjust.com/2uo1qc?campaign=send-tab'; | ||
| const LABEL = 'QR code to download Firefox for mobile'; | ||
| const IMAGE_DATA = | ||
| 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; | ||
|
|
||
| describe('QRCode', () => { | ||
| it('renders an accessible image labelled by localizedLabel', () => { | ||
| renderWithLocalizationProvider( | ||
| <QRCode value={VALUE} localizedLabel={LABEL} /> | ||
| ); | ||
| expect(screen.getByRole('img', { name: LABEL })).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders a pre-rendered QR image when imageData is provided', () => { | ||
| renderWithLocalizationProvider( | ||
| <QRCode imageData={IMAGE_DATA} localizedLabel={LABEL} /> | ||
| ); | ||
| expect(screen.getByRole('img', { name: LABEL })).toHaveAttribute( | ||
| 'src', | ||
| IMAGE_DATA | ||
| ); | ||
| }); | ||
|
|
||
| it('does not generate an SVG QR code when imageData is provided', () => { | ||
| const { container } = renderWithLocalizationProvider( | ||
| <QRCode imageData={IMAGE_DATA} localizedLabel={LABEL} /> | ||
| ); | ||
| expect(container.querySelector('svg')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('forwards the provided className to the container', () => { | ||
| const { container } = renderWithLocalizationProvider( | ||
| <QRCode value={VALUE} localizedLabel={LABEL} className="my-10 mx-auto" /> | ||
| ); | ||
| expect(container.firstChild).toHaveClass('my-10', 'mx-auto'); | ||
| }); | ||
|
|
||
| it('overlays the logo when logoSrc is provided', () => { | ||
| const { container } = renderWithLocalizationProvider( | ||
| <QRCode value={VALUE} localizedLabel={LABEL} logoSrc="logo.svg" /> | ||
| ); | ||
| expect(container.querySelector('img')).toHaveAttribute('src', 'logo.svg'); | ||
| }); | ||
|
|
||
| it('renders no logo when logoSrc is omitted', () => { | ||
| const { container } = renderWithLocalizationProvider( | ||
| <QRCode value={VALUE} localizedLabel={LABEL} /> | ||
| ); | ||
| expect(container.querySelector('img')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('shows the loading indicator while loading', () => { | ||
| renderWithLocalizationProvider( | ||
| <QRCode value={VALUE} localizedLabel={LABEL} loading /> | ||
| ); | ||
| expect(screen.getByTestId('qrcode-loading')).toBeInTheDocument(); | ||
| expect(screen.getByTestId('loading-spinner')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('does not show the loading indicator when not loading', () => { | ||
| renderWithLocalizationProvider( | ||
| <QRCode value={VALUE} localizedLabel={LABEL} /> | ||
| ); | ||
| expect(screen.queryByTestId('qrcode-loading')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('shows the loading indicator for loadingDelayMs, then reveals the QR', () => { | ||
| jest.useFakeTimers(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎉 |
||
| try { | ||
| renderWithLocalizationProvider( | ||
| <QRCode value={VALUE} localizedLabel={LABEL} loadingDelayMs={1000} /> | ||
| ); | ||
| expect(screen.getByTestId('qrcode-loading')).toBeInTheDocument(); | ||
| act(() => { | ||
| jest.advanceTimersByTime(1000); | ||
| }); | ||
| expect(screen.queryByTestId('qrcode-loading')).not.toBeInTheDocument(); | ||
| } finally { | ||
| jest.useRealTimers(); | ||
| } | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| import React, { useEffect, useState } from 'react'; | ||
| import classNames from 'classnames'; | ||
| import { QRCodeSVG } from 'qrcode.react'; | ||
| import LoadingSpinner from 'fxa-react/components/LoadingSpinner'; | ||
|
|
||
| // Transparent 1x1 PNG; excavates modules behind the logo so its chip never clips them. | ||
| const TRANSPARENT_PIXEL = | ||
| 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; | ||
|
|
||
| /** Logo footprint as a fraction of the QR size. */ | ||
| const LOGO_RATIO = 0.2; | ||
| /** White padding (px) around the logo, matching the chip's Tailwind `p-3`. */ | ||
| const LOGO_PADDING_PX = 12; | ||
|
|
||
| type QRCodeBaseProps = { | ||
| /** Accessible label for the rendered QR image. */ | ||
| localizedLabel: string; | ||
| /** Rendered size in pixels. */ | ||
| size?: number; | ||
| /** Error correction level; defaults to 'H' so an overlaid logo stays scannable. */ | ||
| level?: 'L' | 'M' | 'Q' | 'H'; | ||
| /** Optional logo to overlay in the center of the QR code (generated QR only). */ | ||
| logoSrc?: string; | ||
| /** Minimum QR version (1-40); higher means more, smaller modules. Defaults to 1 for larger modules. */ | ||
| minVersion?: number; | ||
| /** Show a loading indicator in place of the QR (e.g. while the value is fetched). */ | ||
| loading?: boolean; | ||
| /** Artificial delay (ms) holding the loading state so the animation registers. Omit for none. */ | ||
| loadingDelayMs?: number; | ||
| className?: string; | ||
| }; | ||
|
|
||
| /** | ||
| * Exactly one source is required: either a `value` to encode, or a pre-rendered | ||
| * QR as an image data URI (`imageData`, e.g. base64 PNG/SVG) rendered as-is. | ||
| */ | ||
| export type QRCodeProps = QRCodeBaseProps & | ||
| ({ value: string; imageData?: never } | { imageData: string; value?: never }); | ||
|
|
||
| /** | ||
| * Domain-agnostic QR code. While loading, a centered spinner replaces the QR, | ||
| * which fades in once loading ends. | ||
| */ | ||
| const QRCode = ({ | ||
| value, | ||
| imageData, | ||
| localizedLabel, | ||
| size = 224, | ||
| level = 'H', | ||
| logoSrc, | ||
| minVersion = 1, | ||
| loading = false, | ||
| loadingDelayMs, | ||
| className, | ||
| }: QRCodeProps) => { | ||
| const [delaying, setDelaying] = useState(!!loadingDelayMs); | ||
| useEffect(() => { | ||
| if (!loadingDelayMs) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a delay here so that page feels a bit more "alive", otw the code shows almost instantly. |
||
| setDelaying(false); | ||
| return; | ||
| } | ||
| setDelaying(true); | ||
| const timer = setTimeout(() => setDelaying(false), loadingDelayMs); | ||
| return () => clearTimeout(timer); | ||
| }, [loadingDelayMs]); | ||
|
|
||
| const isLoading = loading || delaying; | ||
| const logoSize = Math.round(size * LOGO_RATIO); | ||
| const excavateSize = logoSize + LOGO_PADDING_PX * 2; | ||
| const imageSettings = logoSrc | ||
| ? { | ||
| src: TRANSPARENT_PIXEL, | ||
| width: excavateSize, | ||
| height: excavateSize, | ||
| excavate: true, | ||
| } | ||
| : undefined; | ||
| return ( | ||
| <div | ||
| className={classNames( | ||
| 'relative w-fit rounded-xl bg-white p-4', | ||
| !isLoading && 'border border-black', | ||
| className | ||
| )} | ||
| > | ||
| {isLoading && ( | ||
| <div | ||
| className="absolute inset-0 flex items-center justify-center rounded-xl bg-white" | ||
| data-testid="qrcode-loading" | ||
| > | ||
| <LoadingSpinner imageClassName="w-10 h-10 animate-spin" /> | ||
| </div> | ||
| )} | ||
| <div | ||
| className={classNames( | ||
| 'relative transition-opacity duration-300', | ||
| isLoading ? 'opacity-0' : 'opacity-100' | ||
| )} | ||
| > | ||
| {imageData ? ( | ||
| <img | ||
| src={imageData} | ||
| alt={localizedLabel} | ||
| width={size} | ||
| height={size} | ||
| className="block" | ||
| /> | ||
| ) : ( | ||
| <QRCodeSVG | ||
| value={value ?? ''} | ||
| size={size} | ||
| level={level} | ||
| minVersion={minVersion} | ||
| imageSettings={imageSettings} | ||
| role="img" | ||
| aria-label={localizedLabel} | ||
| className="block" | ||
| /> | ||
| )} | ||
| {logoSrc && !imageData && ( | ||
| <span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white p-3"> | ||
| <img | ||
| src={logoSrc} | ||
| alt="" | ||
| className="block" | ||
| style={{ width: logoSize, height: logoSize }} | ||
| /> | ||
| </span> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default QRCode; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible there would be another
imgelement on the page here and produce a false-negative? Maybe there's a tag, aria-label or similar that could be used to target the specific logo image?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do see a test above is doing something similar, so probably worth targeting the image the same way here too