@sincpro/printer-expo
v1.0.6
Published
Expo module for controlling Bixolon thermal printers with Bluetooth connectivity
Maintainers
Readme
@sincpro/printer-expo
A powerful React Native module for controlling thermal printers in Expo applications. Built with Clean Architecture and Hexagonal Architecture (Ports & Adapters) for maximum flexibility and maintainability.
✨ Features
- 🔗 Multiple Connectivity: Bluetooth, WiFi, and USB support
- 🖨️ Advanced Printing: Text, QR codes, barcodes, images, and PDFs
- 📋 Structured Receipts: Header/body/footer with flexible line types
- ⚙️ Configurable: Margins, density, speed, orientation, auto-cutter
- 🏗️ Clean Architecture: SOLID principles, testable, swappable adapters
- 📚 Official SDK: Integration with Bixolon SDK (extensible to other brands)
- 📝 TypeScript: 100% type-safe API with comprehensive definitions
- 🛠️ Easy Setup: Simple installation and minimal configuration
- 📱 Android Focus: Optimized for Android thermal printers
📦 Installation
npm install @sincpro/printer-expo
# or
yarn add @sincpro/printer-expoRequirements
- Expo SDK:
>=52.0.0 - React Native: Compatible with Expo SDK
- Platform: Android (iOS not currently supported)
Post-Installation
# Rebuild native modules
npx expo prebuild --clean
# Run on device
npx expo run:android⚙️ Configuration
Android Permissions
The module requires Bluetooth permissions. Add to your app.json:
{
"expo": {
"android": {
"permissions": [
"android.permission.BLUETOOTH",
"android.permission.BLUETOOTH_ADMIN",
"android.permission.BLUETOOTH_SCAN",
"android.permission.BLUETOOTH_CONNECT",
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.ACCESS_COARSE_LOCATION"
]
}
}
}Note: These permissions are required for Bluetooth device discovery and connection on Android 12+.
🚀 Quick Start
Basic Example
import { bluetooth, connection, config, print } from '@sincpro/printer-expo';
// 1. Get paired devices
const devices = bluetooth.getPairedDevices();
const printers = devices.filter(d => d.isPrinter);
// 2. Connect to printer via Bluetooth
await connection.connectBluetooth(printers[0].address, 30000); // 30s timeout
// 3. Optional: Configure printer
await config.set({
marginLeft: 10,
marginTop: 5,
density: 'dark',
speed: 'medium',
orientation: 'top_to_bottom',
autoCutter: { enabled: true, fullCut: true }
});
// 4. Print a simple text
await print.text('Hello World!', {
fontSize: 'large',
alignment: 'center',
bold: true,
media: { preset: 'continuous80mm' }
});
// 5. Print a complete receipt
await print.receipt({
header: [
{ type: 'text', content: 'MY STORE', fontSize: 'large', alignment: 'center', bold: true },
{ type: 'text', content: '123 Main Street', alignment: 'center' },
{ type: 'separator' },
],
body: [
{ type: 'keyValue', key: 'Product 1', value: '$10.00' },
{ type: 'keyValue', key: 'Product 2', value: '$15.00' },
{ type: 'separator' },
{ type: 'keyValue', key: 'TOTAL', value: '$25.00', bold: true },
],
footer: [
{ type: 'qr', data: 'https://mystore.com/receipt/123', alignment: 'center' },
{ type: 'text', content: 'Thank you!', alignment: 'center' },
{ type: 'space', lines: 2 },
]
}, { media: { preset: 'continuous80mm' } });
// 6. Disconnect
await connection.disconnect();Complete React Component
import React, { useState } from 'react';
import { View, Button, FlatList, Text, Alert } from 'react-native';
import { bluetooth, connection, print } from '@sincpro/printer-expo';
import type { BluetoothDevice } from '@sincpro/printer-expo';
export default function PrinterScreen() {
const [devices, setDevices] = useState<BluetoothDevice[]>([]);
const [connected, setConnected] = useState(false);
const scanDevices = async () => {
try {
const foundDevices = bluetooth.getPairedDevices();
setDevices(foundDevices.filter((d) => d.isPrinter));
} catch (error) {
Alert.alert('Error', error.message);
}
};
const connectDevice = async (device: BluetoothDevice) => {
try {
await connection.connectBluetooth(device.address, 30000);
setConnected(true);
Alert.alert('Success', `Connected to ${device.name}`);
} catch (error) {
Alert.alert('Error', 'Connection failed');
}
};
const printTest = async () => {
try {
await print.receipt({
header: [
{ type: 'text', content: 'Test Receipt', fontSize: 'large', alignment: 'center' },
{ type: 'separator' },
],
body: [{ type: 'text', content: 'This is a test print' }],
footer: [{ type: 'space', lines: 2 }],
});
Alert.alert('Success', 'Receipt printed');
} catch (error) {
Alert.alert('Error', error.message);
}
};
return (
<View style={{ padding: 20 }}>
<Button title="Scan Devices" onPress={scanDevices} />
<Button title="Print Test" onPress={printTest} disabled={!connected} />
<FlatList
data={devices}
keyExtractor={(item) => item.address}
renderItem={({ item }) => (
<Button title={`${item.name} (${item.address})`} onPress={() => connectDevice(item)} />
)}
/>
</View>
);
}📚 API Reference
Connectivity API
bluetooth.getPairedDevices(): BluetoothDevice[]
Get all paired/bonded Bluetooth devices (synchronous).
import { bluetooth } from '@sincpro/printer-expo';
const devices = bluetooth.getPairedDevices();
const printers = devices.filter(d => d.isPrinter);bluetooth.getPairedPrinters(): PairedPrinter[]
Get only paired devices that are printers (synchronous).
const printers = bluetooth.getPairedPrinters();
console.log(printers); // [{ name: 'SPP-R200III', address: '00:11:22:AA:BB:CC' }]connection.connectBluetooth(address: string, timeoutMs?: number): Promise<void>
Connect to a printer via Bluetooth.
Parameters:
address: MAC address of the printer (e.g.,"00:11:22:AA:BB:CC")timeoutMs: Connection timeout in milliseconds (default:10000)
import { connection } from '@sincpro/printer-expo';
await connection.connectBluetooth('00:11:22:AA:BB:CC', 30000);connection.connectWifi(ip: string, port?: number, timeoutMs?: number): Promise<void>
Connect to a printer via WiFi.
Parameters:
ip: IP address (e.g.,"192.168.1.100")port: TCP port (default:9100)timeoutMs: Connection timeout in milliseconds (default:10000)
await connection.connectWifi('192.168.1.100', 9100, 30000);connection.connectUsb(): Promise<void>
Connect to a printer via USB.
await connection.connectUsb();connection.disconnect(): Promise<void>
Disconnect from the current printer.
await connection.disconnect();connection.isConnected(): boolean
Check if currently connected (synchronous).
const connected = connection.isConnected();connection.getStatus(): Promise<PrinterStatus>
Get current printer status (paper, cover, errors).
const status = await connection.getStatus();
console.log('Connection:', status.connectionState); // 'CONNECTED' | 'CONNECTING' | 'DISCONNECTED' | 'ERROR'
console.log('Has paper:', status.hasPaper);
console.log('Cover open:', status.isCoverOpen);
console.log('Error:', status.errorMessage);connection.getInfo(): Promise<PrinterInfo>
Get printer information (model, firmware, serial).
const info = await connection.getInfo();
console.log('Model:', info.model);
console.log('Firmware:', info.firmware);
console.log('Serial:', info.serial);
console.log('DPI:', info.dpi);connection.getDpi(): number
Get printer DPI (synchronous).
const dpi = connection.getDpi(); // e.g., 203 or 300Configuration API
config.set(printerConfig: PrinterConfig): Promise<void>
Set printer configuration (margins, density, speed, orientation, cutter). This sets the default config and applies it immediately if connected.
import { config } from '@sincpro/printer-expo';
await config.set({
marginLeft: 10, // Left margin in dots
marginTop: 5, // Top margin in dots
density: 'dark', // 'light' | 'medium' | 'dark' | 'extra_dark'
speed: 'medium', // 'slow' | 'medium' | 'fast' | 'extra_fast'
orientation: 'top_to_bottom', // 'top_to_bottom' | 'bottom_to_top'
autoCutter: {
enabled: true,
fullCut: true // true = full cut, false = partial cut
}
});config.get(): PrinterConfig
Get current printer configuration (synchronous).
const currentConfig = config.get();
console.log('Density:', currentConfig.density);
console.log('Speed:', currentConfig.speed);Print API
print.text(text: string, options?: PrintTextOptions): Promise<void>
Print a single line of text.
Options:
fontSize:'small'|'medium'|'large'|'xlarge'alignment:'left'|'center'|'right'bold:booleanmedia:MediaConfig
import { print } from '@sincpro/printer-expo';
await print.text('Hello World!', {
fontSize: 'large',
alignment: 'center',
bold: true,
media: { preset: 'continuous80mm' }
});print.texts(texts: string[], options?: PrintTextsOptions): Promise<void>
Print multiple lines of text.
await print.texts(
['Line 1', 'Line 2', 'Line 3'],
{ fontSize: 'medium', media: { preset: 'continuous80mm' } }
);print.qr(data: string, options?: PrintQROptions): Promise<void>
Print a QR code.
Options:
size: QR size 1-10 (default:5)alignment:'left'|'center'|'right'media:MediaConfig
await print.qr('https://sincpro.com', {
size: 8,
alignment: 'center',
media: { preset: 'continuous80mm' }
});print.barcode(data: string, options?: PrintBarcodeOptions): Promise<void>
Print a barcode.
Options:
type:'CODE128'|'CODE39'|'EAN13'|'EAN8'|'UPCA'|'UPCE'|'CODE93'|'CODABAR'height: Barcode height in dotsalignment:'left'|'center'|'right'media:MediaConfig
await print.barcode('123456789012', {
type: 'CODE128',
height: 80,
alignment: 'center',
media: { preset: 'continuous80mm' }
});print.imageBase64(base64Data: string, options?: PrintImageOptions): Promise<void>
Print an image from base64 data.
Options:
alignment:'left'|'center'|'right'media:MediaConfig
await print.imageBase64(base64ImageData, {
alignment: 'center',
media: { preset: 'continuous80mm' }
});print.pdfBase64(base64Data: string, options?: PrintPdfOptions): Promise<void>
Print a PDF page from base64 data.
Options:
page: Page number to print (default:0)alignment:'left'|'center'|'right'media:MediaConfig
await print.pdfBase64(base64PdfData, {
page: 0,
alignment: 'center',
media: { preset: 'continuous80mm' }
});print.getPdfPageCount(base64Data: string): number
Get page count from a PDF (synchronous).
const pageCount = print.getPdfPageCount(base64PdfData);print.keyValue(key: string, value: string, options?: PrintKeyValueOptions): Promise<void>
Print a key-value pair (two columns).
await print.keyValue('Total', '$25.00', {
fontSize: 'large',
bold: true,
media: { preset: 'continuous80mm' }
});print.receipt(receipt: Receipt, options?: PrintReceiptOptions): Promise<void>
Print a complete structured receipt with header, body, and footer sections.
Options:
media:MediaConfigcopies: Number of copies to print (default:1)
await print.receipt({
header: [
{ type: 'text', content: 'MY STORE', fontSize: 'large', alignment: 'center', bold: true },
{ type: 'text', content: '123 Main Street', alignment: 'center' },
{ type: 'separator' },
],
body: [
{ type: 'keyValue', key: 'Product 1', value: '$10.00' },
{ type: 'keyValue', key: 'Product 2', value: '$15.00' },
{ type: 'separator' },
{ type: 'keyValue', key: 'Tax', value: '$2.50' },
],
footer: [
{ type: 'separator' },
{ type: 'keyValue', key: 'TOTAL', value: '$27.50', bold: true, fontSize: 'large' },
{ type: 'qr', data: 'https://mystore.com/receipt/123', alignment: 'center', size: 6 },
{ type: 'text', content: 'Thank you for your purchase!', alignment: 'center' },
{ type: 'space', lines: 2 },
]
}, { media: { preset: 'continuous80mm' }, copies: 1 });Receipt Line Types
Receipt lines are the building blocks of structured receipts. Each line type has specific properties.
TextLine
Print formatted text with customizable style.
{
type: 'text',
content: string,
fontSize?: 'small' | 'medium' | 'large' | 'xlarge',
bold?: boolean,
alignment?: 'left' | 'center' | 'right'
}Example:
{ type: 'text', content: 'INVOICE', fontSize: 'xlarge', alignment: 'center', bold: true }KeyValueLine
Print key-value pairs in two columns (common in receipts).
{
type: 'keyValue',
key: string,
value: string,
fontSize?: 'small' | 'medium' | 'large' | 'xlarge',
bold?: boolean
}Example:
{ type: 'keyValue', key: 'Subtotal', value: '$25.00' }
{ type: 'keyValue', key: 'TOTAL', value: '$27.50', bold: true, fontSize: 'large' }QRLine
Embed QR codes in receipts.
{
type: 'qr',
data: string,
size?: number, // 1-10, default: 5
alignment?: 'left' | 'center' | 'right'
}Example:
{ type: 'qr', data: 'https://store.com/receipt/12345', size: 6, alignment: 'center' }BarcodeLine
Embed barcodes in receipts.
{
type: 'barcode',
data: string,
barcodeType?: 'CODE128' | 'CODE39' | 'EAN13' | 'EAN8' | 'UPCA' | 'UPCE' | 'CODE93' | 'CODABAR',
height?: number,
alignment?: 'left' | 'center' | 'right'
}Example:
{ type: 'barcode', data: '123456789012', barcodeType: 'EAN13', height: 80, alignment: 'center' }ImageLine
Embed images (base64) in receipts.
{
type: 'image',
base64: string,
alignment?: 'left' | 'center' | 'right'
}Example:
{ type: 'image', base64: 'iVBORw0KGgoAAAANS...', alignment: 'center' }SeparatorLine
Print horizontal separator lines.
{
type: 'separator',
char?: string, // Character to repeat, default: '-'
length?: number // Line length in characters, default: 48
}Examples:
{ type: 'separator' }
{ type: 'separator', char: '=', length: 32 }SpaceLine
Add blank lines for spacing.
{
type: 'space',
lines?: number // Number of blank lines, default: 1
}Example:
{ type: 'space', lines: 2 }ColumnsLine
Print multiple columns in one row.
{
type: 'columns',
columns: Array<{
text: string,
widthRatio?: number,
alignment?: 'left' | 'center' | 'right'
}>,
fontSize?: 'small' | 'medium' | 'large' | 'xlarge',
bold?: boolean
}Example:
{
type: 'columns',
columns: [
{ text: 'Item', widthRatio: 2, alignment: 'left' },
{ text: 'Qty', widthRatio: 1, alignment: 'center' },
{ text: 'Price', widthRatio: 1, alignment: 'right' }
],
bold: true
}MediaConfig - Paper Configuration
Configure paper/label dimensions for printing. You can use presets or custom configurations.
Using Presets
// Continuous paper presets
{ preset: 'continuous58mm' } // 58mm continuous paper
{ preset: 'continuous72mm' } // 72mm continuous paper
{ preset: 'continuous80mm' } // 80mm continuous paper (most common)Custom Configuration (Millimeters)
// Custom continuous paper
{
widthMm: 72,
type: 'continuous'
}
// Labels with gap
{
widthMm: 50,
heightMm: 30,
gapMm: 3,
type: 'gap'
}
// Labels with black mark
{
widthMm: 60,
heightMm: 40,
type: 'black_mark'
}Custom Configuration (Dots)
If you need precise control, you can specify dimensions in dots (based on printer DPI).
{
widthDots: 576, // 72mm at 203 DPI
heightDots: 240, // 30mm at 203 DPI
gapDots: 24, // 3mm at 203 DPI
type: 'gap'
}Note: The module automatically converts millimeters to dots based on the printer's DPI. Using millimeters is recommended for easier configuration.
📦 TypeScript Types
BluetoothDevice
interface BluetoothDevice {
name: string; // Device name
address: string; // MAC address (e.g., "00:11:22:AA:BB:CC")
isPrinter: boolean; // True if device is identified as a printer
}PairedPrinter
interface PairedPrinter {
name: string; // Printer name
address: string; // MAC address
}PrinterStatus
interface PrinterStatus {
connectionState: 'DISCONNECTED' | 'CONNECTING' | 'CONNECTED' | 'ERROR';
hasPaper: boolean; // True if paper is available
isCoverOpen: boolean; // True if printer cover is open
isOverheated: boolean; // True if printer is overheated
hasError: boolean; // True if printer has an error
errorMessage: string | null; // Error message if hasError is true
}PrinterInfo
interface PrinterInfo {
model: string; // Printer model (e.g., "SPP-R200III")
firmware: string; // Firmware version
serial: string; // Serial number
dpi: number; // Printer DPI (e.g., 203 or 300)
}PrinterConfig
interface PrinterConfig {
marginLeft?: number; // Left margin in dots
marginTop?: number; // Top margin in dots
density?: 'light' | 'medium' | 'dark' | 'extra_dark';
speed?: 'slow' | 'medium' | 'fast' | 'extra_fast';
orientation?: 'top_to_bottom' | 'bottom_to_top';
autoCutter?: {
enabled: boolean; // Enable auto cutter
fullCut?: boolean; // Full cut (true) or partial cut (false)
};
}MediaConfig
interface MediaConfig {
// Use preset for common paper sizes
preset?: 'continuous58mm' | 'continuous72mm' | 'continuous80mm';
// Or custom configuration in millimeters
widthMm?: number; // Paper width in mm
heightMm?: number; // Label height in mm (for labels)
gapMm?: number; // Gap size in mm (for labels)
// Or custom configuration in dots
widthDots?: number; // Paper width in dots
heightDots?: number; // Label height in dots
gapDots?: number; // Gap size in dots
// Media type
type?: 'continuous' | 'gap' | 'label' | 'black_mark';
}Receipt
interface Receipt {
header?: ReceiptLine[]; // Header section (logo, store info)
body?: ReceiptLine[]; // Body section (items, details)
footer?: ReceiptLine[]; // Footer section (totals, QR, thanks)
}ReceiptLine Types
type ReceiptLine =
| TextLine
| KeyValueLine
| QRLine
| BarcodeLine
| ImageLine
| SeparatorLine
| SpaceLine
| ColumnsLine;🎯 Usage Examples
Example 1: Basic Connection and Text Printing
import { bluetooth, connection, print } from '@sincpro/printer-expo';
async function basicPrint() {
try {
// 1. Get paired printers
const printers = bluetooth.getPairedPrinters();
if (printers.length === 0) {
throw new Error('No paired printers found');
}
// 2. Connect to first printer
await connection.connectBluetooth(printers[0].address, 30000);
console.log('Connected to', printers[0].name);
// 3. Print simple text
await print.text('Hello from Expo!', {
fontSize: 'large',
alignment: 'center',
bold: true,
media: { preset: 'continuous80mm' }
});
// 4. Disconnect
await connection.disconnect();
console.log('Print complete!');
} catch (error) {
console.error('Print failed:', error);
}
}Example 2: Sales Receipt
import { connection, print } from '@sincpro/printer-expo';
async function printSalesReceipt(items: Array<{ name: string; qty: number; price: number }>) {
const subtotal = items.reduce((sum, item) => sum + (item.qty * item.price), 0);
const tax = subtotal * 0.08; // 8% tax
const total = subtotal + tax;
await print.receipt({
header: [
{ type: 'text', content: '🏪 MY RETAIL STORE', fontSize: 'xlarge', alignment: 'center', bold: true },
{ type: 'text', content: '123 Commerce Street', alignment: 'center' },
{ type: 'text', content: 'Phone: (555) 123-4567', alignment: 'center' },
{ type: 'separator', char: '=' },
{ type: 'text', content: 'SALES RECEIPT', fontSize: 'large', alignment: 'center' },
{ type: 'separator', char: '=' },
{ type: 'space' },
{ type: 'keyValue', key: 'Date', value: new Date().toLocaleDateString() },
{ type: 'keyValue', key: 'Receipt #', value: 'RCP-' + Date.now() },
{ type: 'space' },
{ type: 'separator' },
],
body: [
{ type: 'text', content: 'ITEMS', bold: true },
{ type: 'separator' },
...items.map(item => ({
type: 'keyValue' as const,
key: `${item.name} (x${item.qty})`,
value: `$${(item.qty * item.price).toFixed(2)}`
})),
{ type: 'space' },
{ type: 'separator' },
{ type: 'keyValue', key: 'Subtotal', value: `$${subtotal.toFixed(2)}` },
{ type: 'keyValue', key: 'Tax (8%)', value: `$${tax.toFixed(2)}` },
],
footer: [
{ type: 'separator', char: '=' },
{ type: 'keyValue', key: 'TOTAL', value: `$${total.toFixed(2)}`, fontSize: 'large', bold: true },
{ type: 'separator', char: '=' },
{ type: 'space', lines: 2 },
{ type: 'text', content: 'Thank you for your purchase!', alignment: 'center' },
{ type: 'text', content: 'Visit us at www.mystore.com', alignment: 'center' },
{ type: 'space' },
{ type: 'qr', data: `https://mystore.com/receipt/${Date.now()}`, size: 6, alignment: 'center' },
{ type: 'space', lines: 3 },
]
}, { media: { preset: 'continuous80mm' } });
}
// Usage
printSalesReceipt([
{ name: 'Coffee', qty: 2, price: 4.50 },
{ name: 'Croissant', qty: 1, price: 3.25 },
{ name: 'Orange Juice', qty: 1, price: 2.75 }
]);Example 3: Product Label Printing
import { config, print } from '@sincpro/printer-expo';
async function printProductLabel(product: { name: string; sku: string; price: number }) {
// Configure for label printing
await config.set({
marginLeft: 5,
marginTop: 5,
density: 'dark',
speed: 'medium'
});
await print.receipt({
header: [
{ type: 'text', content: product.name, fontSize: 'large', alignment: 'center', bold: true },
{ type: 'space' },
],
body: [
{ type: 'barcode', data: product.sku, barcodeType: 'CODE128', height: 60, alignment: 'center' },
{ type: 'space' },
{ type: 'text', content: `SKU: ${product.sku}`, alignment: 'center' },
],
footer: [
{ type: 'space' },
{ type: 'text', content: `$${product.price.toFixed(2)}`, fontSize: 'xlarge', alignment: 'center', bold: true },
]
}, {
media: {
widthMm: 50,
heightMm: 30,
gapMm: 3,
type: 'gap'
}
});
}
// Usage
printProductLabel({
name: 'Premium Coffee Beans',
sku: '1234567890',
price: 12.99
});Example 4: Error Handling Pattern
import { connection, print } from '@sincpro/printer-expo';
import type { Receipt } from '@sincpro/printer-expo';
async function safePrint(receipt: Receipt) {
try {
// Check connection
if (!connection.isConnected()) {
throw new Error('Printer not connected');
}
// Check printer status
const status = await connection.getStatus();
if (status.connectionState !== 'CONNECTED') {
throw new Error(`Printer is ${status.connectionState.toLowerCase()}`);
}
if (!status.hasPaper) {
throw new Error('Printer is out of paper');
}
if (status.isCoverOpen) {
throw new Error('Printer cover is open');
}
if (status.isOverheated) {
throw new Error('Printer is overheated');
}
if (status.hasError) {
throw new Error(`Printer error: ${status.errorMessage}`);
}
// Print receipt
await print.receipt(receipt, { media: { preset: 'continuous80mm' } });
console.log('✅ Print successful');
return { success: true };
} catch (error) {
console.error('❌ Print failed:', error);
// Return user-friendly error
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
// Usage in React component
function PrintButton() {
const handlePrint = async () => {
const result = await safePrint(myReceipt);
if (result.success) {
Alert.alert('Success', 'Receipt printed successfully');
} else {
Alert.alert('Print Error', result.error);
}
};
return <Button title="Print" onPress={handlePrint} />;
}Example 5: Custom Hook for Printer Management
import { useState, useEffect } from 'react';
import { bluetooth, connection, print } from '@sincpro/printer-expo';
import type { BluetoothDevice, PrinterStatus, Receipt } from '@sincpro/printer-expo';
export function usePrinter() {
const [devices, setDevices] = useState<BluetoothDevice[]>([]);
const [connected, setConnected] = useState(false);
const [status, setStatus] = useState<PrinterStatus | null>(null);
useEffect(() => {
checkConnection();
}, []);
const checkConnection = () => {
setConnected(connection.isConnected());
};
const scanDevices = () => {
try {
const foundDevices = bluetooth.getPairedDevices();
setDevices(foundDevices.filter(d => d.isPrinter));
return foundDevices;
} catch (error) {
console.error('Scan failed:', error);
throw error;
}
};
const connect = async (address: string, timeoutMs = 30000) => {
try {
await connection.connectBluetooth(address, timeoutMs);
setConnected(true);
const printerStatus = await connection.getStatus();
setStatus(printerStatus);
return true;
} catch (error) {
console.error('Connection failed:', error);
throw error;
}
};
const disconnect = async () => {
try {
await connection.disconnect();
setConnected(false);
setStatus(null);
return true;
} catch (error) {
console.error('Disconnection failed:', error);
throw error;
}
};
const printReceipt = async (receipt: Receipt) => {
if (!connected) {
throw new Error('Not connected to printer');
}
await print.receipt(receipt, { media: { preset: 'continuous80mm' } });
};
const refreshStatus = async () => {
if (connected) {
const printerStatus = await connection.getStatus();
setStatus(printerStatus);
return printerStatus;
}
return null;
};
return {
devices,
connected,
status,
scanDevices,
connect,
disconnect,
printReceipt,
refreshStatus,
};
}
// Usage in component
function MyPrinterComponent() {
const printer = usePrinter();
return (
<View>
<Button title="Scan Printers" onPress={printer.scanDevices} />
<Button
title="Print Receipt"
onPress={() => printer.printReceipt(myReceipt)}
disabled={!printer.connected}
/>
{printer.status && (
<Text>Status: {printer.status.connectionState}</Text>
)}
</View>
);
}🏗️ Architecture
This module follows Clean Architecture with Hexagonal Architecture (Ports & Adapters) principles for maximum maintainability and extensibility.
Architecture Layers
┌─────────────────────────────────────────────────────────┐
│ Dependencies ALWAYS point inward (toward Domain) │
└─────────────────────────────────────────────────────────┘
TypeScript (React Native)
↓
ENTRYPOINT ← Expo Modules API bridge
↓
SERVICE ← Use cases & orchestration
↓
DOMAIN ← Business entities & rules (interfaces)
↑
┌─────┴─────┐
ADAPTER INFRASTRUCTURE
(Vendors) (Platform APIs)Layer Responsibilities
| Layer | Purpose | Examples |
|-------|---------|----------|
| TypeScript | React Native API | bluetooth, connection, config, print |
| Entrypoint | Expo ↔ Kotlin bridge | PrinterModule.kt |
| Service | Business logic | ConnectivityService, PrintService |
| Domain | Contracts & entities | IPrinter, Receipt, MediaConfig |
| Adapter | Vendor SDKs | BixolonPrinterAdapter |
| Infrastructure | Platform utilities | AndroidBluetoothProvider, BinaryConverter |
Benefits
- ✅ Testable: Mock adapters and services independently
- ✅ Maintainable: Clear separation of concerns
- ✅ Extensible: Easy to add new printer brands (Zebra, Epson, Star, etc.)
- ✅ Swappable: Change implementations without affecting business logic
- ✅ Framework-independent: Domain layer has no Android/iOS dependencies
Adding New Printer Brands
The architecture makes it easy to support additional printer brands:
- Create Adapter: Implement
IPrinterinterface for new vendor SDK - Register in SDK: Add to
SincproPrinterSdkentry point - No changes needed: Business logic and API remain unchanged
See ARCHITECTURE.md for detailed architecture documentation.
🖨️ Supported Printers
Bixolon Printers
This module currently supports Bixolon thermal printers via the official Bixolon SDK.
Tested Models
- ✅ SPP-R200III - 2" mobile printer (58mm)
- ✅ SPP-R300 - 3" mobile printer (80mm)
- ✅ SPP-R400 - 4" mobile printer (112mm)
- ✅ SRP-275III - 3" desktop printer (80mm)
- ✅ SRP-350III - 3" desktop printer (80mm)
- ✅ SRP-352III - 3" desktop printer (80mm)
Compatible Models
The following Bixolon models should work but have not been tested:
- SPP-R210, SPP-R220, SPP-R310, SPP-R410
- SRP-330II, SRP-350plusIII, SRP-380
- XD3-40d, XD5-40d
- XT5-40, XT5-43
Connectivity Support
| Connection Type | Status | Notes | |----------------|--------|-------| | Bluetooth | ✅ Fully supported | Most common for mobile printers | | WiFi | ✅ Supported | For network-connected printers | | USB | ⚠️ Limited | Requires USB OTG on Android |
Adding Support for Other Brands
The architecture supports adding other printer brands. To add support:
- Implement the
IPrinterinterface in a new adapter - Integrate the vendor's SDK (Zebra, Epson, Star, etc.)
- Register the adapter in the SDK entry point
See CONTRIBUTING.md for development guidelines.
🛠️ Troubleshooting
Connection Issues
Problem: Connection fails or times out
Solutions:
- Verify Bluetooth is enabled on the device
- Check device is paired:
bluetooth.getPairedDevices() - Ensure printer is powered on and in range (< 10 meters)
- Try increasing timeout:
connectBluetooth(address, 60000)(60 seconds) - Restart printer and retry connection
Example:
try {
await connection.connectBluetooth(address, 60000);
} catch (error) {
console.error('Connection failed:', error);
// Try restarting printer
}Print Failures
Problem: Print command succeeds but nothing prints
Solutions:
- Check printer status:
const status = await connection.getStatus(); if (!status.hasPaper) console.error('No paper!'); if (status.isCoverOpen) console.error('Cover open!'); - Verify connection:
connection.isConnected() - Check media configuration matches paper type
- Ensure printer is not in an error state
Paper Not Feeding
Problem: Paper doesn't feed after printing
Solutions:
- Add space lines at the end of receipt:
footer: [ // ... other lines { type: 'space', lines: 3 } // Add extra space ] - Configure auto-cutter:
await config.set({ autoCutter: { enabled: true, fullCut: true } });
Bluetooth Permissions Denied
Problem: "Permission denied" errors
Solution: Ensure permissions are declared in app.json and granted at runtime. Android 12+ requires runtime permission grants.
{
"expo": {
"android": {
"permissions": [
"android.permission.BLUETOOTH",
"android.permission.BLUETOOTH_ADMIN",
"android.permission.BLUETOOTH_SCAN",
"android.permission.BLUETOOTH_CONNECT"
]
}
}
}Image/QR Not Printing
Problem: Images or QR codes don't appear
Solutions:
- Check base64 data is valid
- Ensure alignment is correct
- Try different QR size (1-10)
- Verify printer supports graphics
Module Not Found Error
Problem: Module "SincproPrinter" not found
Solutions:
- Rebuild native modules:
npx expo prebuild --clean npx expo run:android - Clear cache:
npm start -- --clear
📖 Resources
- Package: NPM Package
- Repository: GitHub
- Architecture: ARCHITECTURE.md - Detailed architecture guide
- Contributing: CONTRIBUTING.md - Development guidelines
- Expo Modules: Official Documentation
- Bixolon: Official Website
🤝 Contributing
We welcome contributions! Please see CONTRIBUTING.md for:
- Development setup and environment
- Code standards (ktlint, Prettier, ESLint)
- Architecture guidelines and patterns
- Git workflow and branch naming
- Pull request process and review
- Testing requirements
Quick Start for Contributors
# Clone the repository
git clone https://github.com/Sincpro-SRL/sincpro_printer_expo.git
cd sincpro_printer_expo
# Install dependencies
npm install
# Build TypeScript
npm run build
# Format code
npm run format
npm run format:kotlin
# Lint code
npm run lint
npm run lint:kotlin📄 License
MIT License - see LICENSE file for details.
Copyright (c) 2024 Sincpro SRL
🙏 Acknowledgments
- Bixolon for the official printer SDK
- Expo team for the Modules API
- Contributors and testers who helped improve this module
- Open source community for inspiration and support
📞 Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: [email protected]
Made with ❤️ by Sincpro SRL
