diff --git a/.changeset/pos-dev-console-open-in-pos.md b/.changeset/pos-dev-console-open-in-pos.md new file mode 100644 index 00000000000..79eda609f83 --- /dev/null +++ b/.changeset/pos-dev-console-open-in-pos.md @@ -0,0 +1,6 @@ +--- +'@shopify/ui-extensions-dev-console-app': patch +--- + +Add an "Open in Shopify POS" call-to-action in the mobile QR code modal for POS extensions. + diff --git a/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/QRCodeModal.module.scss b/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/QRCodeModal.module.scss index f0f3e41bc53..d81ed0fae73 100644 --- a/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/QRCodeModal.module.scss +++ b/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/QRCodeModal.module.scss @@ -28,6 +28,7 @@ .UrlCta { display: flex; + flex-wrap: wrap; gap: 0.8rem; align-items: center; } diff --git a/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/QRCodeModal.test.tsx b/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/QRCodeModal.test.tsx index d5f925a0af7..d29386d5735 100644 --- a/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/QRCodeModal.test.tsx +++ b/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/QRCodeModal.test.tsx @@ -6,12 +6,17 @@ import {mockApp, mockExtension} from '@shopify/ui-extensions-server-kit/testing' import {render, withProviders} from '@shopify/ui-extensions-test-utils' import {mockI18n} from 'tests/mock-i18n' import {DefaultProviders} from 'tests/DefaultProviders' +import {ExternalIcon} from '@shopify/polaris-icons' import {Modal} from '@/components/Modal' +import {IconButton} from '@/components/IconButton' vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null) vi.mock('@/components/Modal', () => ({Modal: (props: any) => props.children})) +let isMobile = false +vi.mock('@/utilities/device', () => ({isMobileDevice: () => isMobile})) + mockI18n(en) describe('QRCodeModal', () => { @@ -24,6 +29,10 @@ describe('QRCodeModal', () => { }, } + beforeEach(() => { + isMobile = false + }) + test('Renders closed if code is undefined', async () => { const app = mockApp() const store = 'example.com' @@ -65,6 +74,64 @@ describe('QRCodeModal', () => { }) }) + test('renders Open in Shopify POS CTA for pos on mobile', async () => { + isMobile = true + + const app = mockApp() + const store = 'example.com' + const extension = mockExtension() + const container = render( + , + withProviders(DefaultProviders), + { + state: {app, store, extensions: [extension]}, + }, + ) + + expect(container).toContainReactComponent(IconButton, { + source: ExternalIcon, + accessibilityLabel: en.qrcode.openPos, + }) + }) + + test('does not render Open in Shopify POS CTA for pos on desktop', async () => { + isMobile = false + + const app = mockApp() + const store = 'example.com' + const extension = mockExtension() + const container = render( + , + withProviders(DefaultProviders), + { + state: {app, store, extensions: [extension]}, + }, + ) + + expect(container).not.toContainReactComponent(IconButton, { + source: ExternalIcon, + }) + }) + + test('does not render Open in Shopify POS CTA for non-pos types on mobile', async () => { + isMobile = true + + const app = mockApp() + const store = 'example.com' + const extension = mockExtension() + const container = render( + , + withProviders(DefaultProviders), + { + state: {app, store, extensions: [extension]}, + }, + ) + + expect(container).not.toContainReactComponent(IconButton, { + source: ExternalIcon, + }) + }) + test('renders QRCode for app home', async () => { const app = mockApp() const store = 'example.com' diff --git a/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/QRCodeModal.tsx b/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/QRCodeModal.tsx index 007e062e9b2..af1ef3f3718 100644 --- a/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/QRCodeModal.tsx +++ b/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/QRCodeModal.tsx @@ -7,9 +7,10 @@ import copyToClipboard from 'copy-to-clipboard' import QRCode from 'qrcode.react' import {toast} from 'react-toastify' import {Surface} from '@shopify/ui-extensions-server-kit' -import {ClipboardIcon} from '@shopify/polaris-icons' +import {ClipboardIcon, ExternalIcon} from '@shopify/polaris-icons' import {Modal, ModalProps} from '@/components/Modal' import {IconButton} from '@/components/IconButton' +import {isMobileDevice} from '@/utilities/device' interface Code { url: string @@ -42,6 +43,8 @@ function QRCodeContent({url, type}: Code) { const {store, app} = useApp() + const shouldShowOpenInPOSCTA = type === 'point_of_sale' && isMobileDevice() + const qrCodeURL = useMemo(() => { // The Websocket hasn't loaded data yet. // Shouldn't happen since you can't open modal without data, @@ -64,12 +67,17 @@ function QRCodeContent({url, type}: Code) { return `https://${store}/admin/extensions-dev/mobile?url=${url}` }, [url, app, app?.mobileUrl]) - const onButtonClick = useCallback(() => { + const onCopyClick = useCallback(() => { if (qrCodeURL && copyToClipboard(qrCodeURL)) { toast(i18n.translate('qrcode.copied'), {toastId: `copy-qrcode-${qrCodeURL}`}) } }, [qrCodeURL]) + const onOpenInPOSClick = useCallback(() => { + if (!qrCodeURL) return + window.location.assign(qrCodeURL) + }, [qrCodeURL]) + if (!qrCodeURL) { return null } @@ -84,14 +92,25 @@ function QRCodeContent({url, type}: Code) { {i18n.translate('right.one')} - {i18n.translate('right.two')}{' '} + {i18n.translate('right.two')} + {shouldShowOpenInPOSCTA && ( + + {i18n.translate('right.three')} + + + )} ) diff --git a/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/translations/en.json b/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/translations/en.json index 113d32e5e8c..67cf46a224a 100644 --- a/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/translations/en.json +++ b/packages/ui-extensions-dev-console/src/sections/Extensions/components/QRCodeModal/translations/en.json @@ -2,11 +2,13 @@ "title": "View your work on mobile", "right": { "one": "Scan with your phone camera to see your work", - "two": "Or copy the URL to share:" + "two": "Or copy the URL to share:", + "three": "Or open it directly in Shopify POS:" }, "qrcode": { "copy": "Copy link", "copied": "Link copied", + "openPos": "Open in Shopify POS", "content": "Scan to test {title} on mobile" } } diff --git a/packages/ui-extensions-dev-console/src/utilities/device.ts b/packages/ui-extensions-dev-console/src/utilities/device.ts new file mode 100644 index 00000000000..c2d8e32687d --- /dev/null +++ b/packages/ui-extensions-dev-console/src/utilities/device.ts @@ -0,0 +1,20 @@ +type NavigatorWithUserAgentData = Navigator & { + userAgentData?: { + mobile?: boolean + } +} + +export function isMobileDevice(): boolean { + if (typeof navigator === 'undefined') return false + + const navigatorWithUserAgentData = navigator as NavigatorWithUserAgentData + if (navigatorWithUserAgentData.userAgentData?.mobile) return true + + const userAgent = navigator.userAgent ?? '' + if (/(Android|iPhone|iPad|iPod)/i.test(userAgent)) return true + + // iPadOS 13+ uses a desktop-like UA but exposes touch points. + if (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) return true + + return false +}