Build a print extension
Learn how to build a POS print extension that generates, previews, and prints custom documents.
Anchor to introIntroduction
This tutorial shows you how to create a print extension that lets merchants generate and preview documents before printing.
What you'll achieve:
- Create a backend service that serves print-ready documents
- Build a POS extension with preview and print capabilities
- Implement error handling for a reliable printing experience
- Test your extension in a development environment
Anchor to requirementsBefore you start
To make the most of this tutorial, you'll need:
- Complete the Getting started guide
- Partner account
- Development store with POS
- Latest version of Shopify CLI
- Basic app setup (access scopes only needed if accessing Shopify data)
This tutorial uses simple HTML documents. If you plan to include order, customer or any other Shopify admin data in your prints, you'll need additional access scopes.
Anchor to backend-setupCreate a route to serve printable documents
First, create a new route file at that will serve your printable documents. This example uses Remix, but you can adapt the concepts to your preferred framework.
Need help setting up a Remix server? Check out the Shopify App Remix documentation.
Let's walk through each part of the implementation:
- Set up the route and handle authentication with Shopify
- Process URL parameters to determine which documents to generate
- Generate HTML content with proper styling for each document type
- Return a properly formatted response with CORS headers
Here's a complete example that supports multiple document types:
- Use only static HTML and CSS - JavaScript won't execute in print documents
- Include all styles in the section or inline
- Use @media print CSS rules for print-specific styling
- Ensure proper CORS headers are set for POS access
When returning multiple documents, use CSS page breaks to ensure proper printing:
@media print {
.page-break {
page-break-after: always;
}
}
When using Cloudflare tunnels for development, wrap email addresses in HTML comments to prevent obfuscation:
Print Route Implementation
/app/routes/print.js
Examples
Print Route Implementation
/app/routes/print.js
import {authenticate} from '../shopify.server'; export async function loader({request}) { // Step 1: Set up the route and handle authentication const {cors} = await authenticate.admin(request); // Step 2: Process URL parameters for document configuration const url = new URL(request.url); const printTypes = url.searchParams.get('printTypes')?.split(',') || []; // Step 3: Generate the HTML content const pages = printTypes.map((type) => createPage(type)); const print = printHTML(pages); // Step 4: Return properly formatted response with CORS headers return cors( new Response(print, { status: 200, headers: { 'Content-type': 'text/html', }, }), ); } // Helper function to create document pages based on type function createPage(type) { const email = '<!--email_off-->customerhelp@example.com<!--/email_off-->'; // Get document content based on type (invoice, packing slip, etc.) const getDocumentInfo = () => { switch (type) { case 'invoice': return { label: 'Receipt / Invoice', content: ` <p>Official Receipt/Invoice document</p> <p>Contains detailed payment and tax information</p> <p>Order details and pricing breakdown</p> `, }; case 'packing-slip': return { label: 'Packing Slip', content: ` <p>Shipping and fulfillment details</p> <p>Complete list of items in order</p> <p>Shipping address and instructions</p> `, }; case 'returns-form': return { label: 'Returns Form', content: ` <p>Return Authorization Form</p> <p>Return shipping instructions</p> <p>Items eligible for return</p> `, }; case 'draft-orders-quote': return { label: 'Draft Orders Quote', content: ` <p>Custom Order Quote</p> <p>Detailed pricing breakdown</p> <p>Terms and conditions</p> `, }; case 'refund-credit-note': return { label: 'Refund / Credit Note', content: ` <p>Refund Documentation</p> <p>Credit amount details</p> <p>Returned items list</p> `, }; default: return { label: type, content: ` <p>Sample document</p> <p>This is an example of a printable document.</p> `, }; } }; const {label, content} = getDocumentInfo(); return `<main> <div> <h1>${label}</h1> <div class="content"> ${content} <hr> <p>Contact us: ${email}</p> </div> </div> </main>`; } // Helper function to generate final HTML with proper styling and page breaks function printHTML(pages) { // Define page break styles for both screen and print const pageBreak = `<div class="page-break"></div>`; const pageBreakStyles = ` @media not print { .page-break { width: 100vw; height: 40px; background-color: lightgray; } } @media print { .page-break { page-break-after: always; } }`; const joinedPages = pages.join(pageBreak); return `<!DOCTYPE html> <html lang="en"> <head> <title>Print Document</title> <style> body, html { margin: 0; padding: 0; font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } main { padding: 2rem; } h1 { margin: 0 0 1rem 0; font-size: 1.5rem; } .content { font-size: 1rem; line-height: 1.5; } hr { margin: 1.5rem 0; border: none; border-top: 1px solid #000; } ${pageBreakStyles} </style> </head> <body> ${joinedPages} </body> </html>`; }
Anchor to extension-uiBuild the extension UI
Your print extension needs two main components that work together to provide a complete printing experience:
- A Tile on the POS smart grid that launches your extension
- A Modal that implements the printing workflow:
- Document selection with a list of available templates
- Document preview before printing
- Print button with proper loading and error states
Let's walk through the implementation:
- Create a tile component that opens the modal
- Set up a Navigator to manage multiple screens
- Implement document selection with a List component
- Add
and execute Print API with button
Here's how to implement both components:
- The
Tile
component usesto open the modal
- The
Navigator
component manages the document selection and preview screens - The
List
component with toggle switches for document selection - The
component shows selected documents before printing
- Loading states and error handling ensure a smooth user experience
Extension Components
Examples
Extension Components
HomeTile.tsx
import React from 'react'; import { Tile, reactExtension, useApi, } from '@shopify/ui-extensions-react/point-of-sale'; const HomeTile = () => { const api = useApi(); return ( <Tile title="Print Tutorial" onPress={() => { api.action.presentModal('print-tutorial'); }} enabled /> ); }; export default reactExtension('pos.home.tile.render', () => { return <HomeTile />; });
HomeModal.tsx
import React, {useState, useEffect} from 'react'; import type {ListRow} from '@shopify/ui-extensions-react/point-of-sale'; import { Button, Navigator, PrintPreview, Stack, reactExtension, Screen, useApi, List, SectionHeader, } from '@shopify/ui-extensions-react/point-of-sale'; interface DocumentOption { id: string; label: string; subtitle: string; selected: boolean; } const Modal = () => { // Step 1: Create state for managing document selection and loading const api = useApi(); const [isLoading, setIsLoading] = useState(false); const [src, setSrc] = useState<string | null>(null); const [documents, setDocuments] = useState<DocumentOption[]>([ { id: 'invoice', label: 'Receipt / Invoice', subtitle: 'Print a detailed sales receipt with tax and payment information', selected: true, }, { id: 'packing-slip', label: 'Packing Slip', subtitle: 'Print shipping details and item list for order fulfillment', selected: false, }, { id: 'returns-form', label: 'Returns Form', subtitle: 'Print return authorization form with shipping labels', selected: false, }, { id: 'draft-orders-quote', label: 'Draft Orders Quote', subtitle: 'Print price quotes and custom order details for customers', selected: false, }, { id: 'refund-credit-note', label: 'Refund / Credit Note', subtitle: 'Print refund documentation with returned items and amounts', selected: false, }, ]); // Step 2: Set up document selection with List component const listData: ListRow[] = documents.map((doc) => ({ id: doc.id, onPress: () => handleSelection(doc.id), leftSide: { label: doc.label, subtitle: [doc.subtitle], }, rightSide: { toggleSwitch: { value: doc.selected, disabled: false, }, }, })); // Step 3: Handle document selection and URL updates const handleSelection = (selectedId: string) => { setDocuments((prevDocs) => prevDocs.map((doc) => ({ ...doc, selected: doc.id === selectedId ? !doc.selected : doc.selected, })), ); }; useEffect(() => { const selectedDocs = documents.filter((doc) => doc.selected); if (selectedDocs.length) { const params = new URLSearchParams({ printTypes: selectedDocs.map((doc) => doc.id).join(','), }); const fullSrc = `/print?${params.toString()}`; setSrc(fullSrc); } else { setSrc(null); } }, [documents]); // Step 4: Implement print functionality with error handling const handlePrint = async () => { if (!src) return; setIsLoading(true); try { await api.print.print(src); } catch (error) { console.error('Print failed:', error); } finally { setIsLoading(false); } }; // Return Navigator component with document selection and preview screens return ( <Navigator> {/* Document selection screen */} <Screen name="print-selection" title="Print Tutorial"> <List listHeaderComponent={<SectionHeader title="Templates" />} data={listData} /> <Stack direction="vertical" paddingHorizontal="Small" paddingVertical="Small" > <Button isDisabled={isLoading || !src} isLoading={isLoading} onPress={() => api.navigation.navigate('print-preview')} title="Next" /> </Stack> </Screen> {/* Preview and print screen */} <Screen name="print-preview" title="Print Tutorial"> {src && <PrintPreview src={src} />} <Stack direction="vertical" paddingHorizontal="Small" paddingVertical="Small" > <Button isDisabled={isLoading || !src} isLoading={isLoading} onPress={handlePrint} title="Print" /> </Stack> </Screen> </Navigator> ); }; export default reactExtension('pos.home.modal.render', () => <Modal />);
Anchor to extension-configConfigure your extension
Configure your extension with the necessary permissions and settings in the shopify.extension.toml
file:
Extension Configuration
shopify.extension.toml
Examples
Extension Configuration
shopify.extension.toml
api_version = "unstable" [[extensions]] type = "ui_extension" name = "Print Tutorial" handle = "print-tutorial" description = "POS UI extension print tutorial" [[extensions.targeting]] module = "./src/HomeTile.tsx" target = "pos.home.tile.render" [[extensions.targeting]] module = "./src/HomeModal.tsx" target = "pos.home.modal.render"
Anchor to testingTesting your extension
To test your print extension:
Navigate to your app directory:
cd <directory>
Start your development server:
shopify app dev
Press
p
to open the developer consoleIn the developer console, click on the view mobile button to preview your extension
Click the
Print Tutorial
tileSelect a template, press next, and then print
Congratulations! You've built a print extension that generates, previews, and prints custom documents.
Use your browser's developer tools to monitor network requests and check for any CORS or authentication issues.
Anchor to deploy-releaseDeploy and release
Refer to Deploy app extensions for more information.
Anchor to troubleshootingSolving common challenges
Anchor to solving-common-challenges-preview-not-appearing?Preview not appearing?
Here's what might be happening:
- Your CORS settings might be blocking the preview
- Authentication could be failing
- The document URL might be incorrect
Quick fix: Open your browser's developer tools and check the network tab for any failed requests.
Anchor to solving-common-challenges-documents-looking-wrong?Documents looking wrong?
Common formatting issues:
- Print-specific styles might be missing
- Check if your document format is supported
Anchor to next-stepsNext steps
Debug your POS UI Extension.
Learn more about building with POS UI extensions by exploring the POS UI extension reference.