import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react'; import { toast } from 'sonner'; export type PrinterType = 'bluetooth' | 'usb' | null; export type PaperSize = '58' | '80'; interface PrinterState { device: any; type: PrinterType; isConnected: boolean; name: string | null; paperSize: PaperSize; } interface PrinterContextType extends PrinterState { connectBluetooth: () => Promise; connectUsb: () => Promise; disconnect: () => Promise; sendData: (data: Uint8Array) => Promise; setPaperSize: (size: PaperSize) => void; } const PrinterContext = createContext(undefined); async function findWriteCharacteristic(services: any[]) { for (const service of services) { const characteristics = await service.getCharacteristics(); for (const char of characteristics) { if (char.properties.write || char.properties.writeWithoutResponse) { return char; } } } return null; } export function PrinterProvider({ children }: { children: ReactNode }) { const [state, setState] = useState({ device: null, type: null, isConnected: false, name: null, paperSize: (localStorage.getItem('printer_paper_size') as PaperSize) || '58', }); const [characteristic, setCharacteristic] = useState(null); // Save paper size to local storage useEffect(() => { localStorage.setItem('printer_paper_size', state.paperSize); }, [state.paperSize]); // USB disconnect listener useEffect(() => { const nav = navigator as any; if (!nav.usb) return; const handleUsbDisconnect = (event: any) => { // Check if the disconnected device matches our current device // We use vendorId and productId as a fallback if reference check fails const currentDevice = state.device; if (state.type === 'usb' && currentDevice && (event.device === currentDevice || (event.device.vendorId === currentDevice.vendorId && event.device.productId === currentDevice.productId))) { setState(prev => ({ ...prev, device: null, type: null, isConnected: false, name: null })); toast.error('Printer USB terputus'); } }; nav.usb.addEventListener('disconnect', handleUsbDisconnect); return () => nav.usb.removeEventListener('disconnect', handleUsbDisconnect); }, [state.type, state.device]); // Bluetooth disconnect listener useEffect(() => { if (state.type === 'bluetooth' && state.device) { const device = state.device; const handleDisconnect = () => { setState(prev => ({ ...prev, device: null, type: null, isConnected: false, name: null })); setCharacteristic(null); toast.error('Printer Bluetooth terputus'); }; device.addEventListener('gattserverdisconnected', handleDisconnect); return () => device.removeEventListener('gattserverdisconnected', handleDisconnect); } }, [state.type, state.device]); // Auto-reconnect on mount useEffect(() => { const autoReconnect = async () => { const nav = navigator as any; const lastType = localStorage.getItem('last_printer_type'); if (lastType === 'bluetooth' && nav.bluetooth?.getDevices) { try { const devices = await nav.bluetooth.getDevices(); if (devices.length > 0) { const device = devices[0]; const server = await device.gatt?.connect(); const services = await server.getPrimaryServices(); const writeChar = await findWriteCharacteristic(services); if (writeChar) { setCharacteristic(writeChar); setState(prev => ({ ...prev, device, type: 'bluetooth', isConnected: true, name: device.name || 'Bluetooth Printer', })); } } } catch (e) { console.error('Auto-reconnect bluetooth failed:', e); } } else if (lastType === 'usb' && nav.usb?.getDevices) { try { const devices = await nav.usb.getDevices(); if (devices.length > 0) { const device = devices[0]; await device.open(); if (device.configuration === null) await device.selectConfiguration(1); const iface = device.configuration.interfaces[0]; await device.claimInterface(iface.interfaceNumber); setState(prev => ({ ...prev, device, type: 'usb', isConnected: true, name: device.productName || 'USB Printer', })); } } catch (e) { console.error('Auto-reconnect usb failed:', e); } } }; autoReconnect(); }, []); const connectBluetooth = useCallback(async () => { try { const nav = navigator as any; if (!nav.bluetooth) { toast.error('Bluetooth tidak didukung di browser ini'); return; } const device = await nav.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: [ '000018f0-0000-1000-8000-00805f9b34fb', '49535343-fe7d-41aa-83b1-d10935904914', '0000ff00-0000-1000-8000-00805f9b34fb', '00001101-0000-1000-8000-00805f9b34fb' ] }); const server = await device.gatt?.connect(); const services = await server.getPrimaryServices(); const writeChar = await findWriteCharacteristic(services); if (!writeChar) { throw new Error('Tidak dapat menemukan characteristic untuk menulis data'); } setCharacteristic(writeChar); setState(prev => ({ ...prev, device, type: 'bluetooth', isConnected: true, name: device.name || 'Bluetooth Printer', })); localStorage.setItem('last_printer_type', 'bluetooth'); toast.success(`Terhubung ke ${device.name || 'Printer Bluetooth'}`); } catch (error: any) { console.error('Bluetooth connection error:', error); if (error.name !== 'NotFoundError') { toast.error(error.message || 'Gagal menghubungkan printer bluetooth'); } } }, []); const connectUsb = useCallback(async () => { try { const nav = navigator as any; if (!nav.usb) { toast.error('USB tidak didukung di browser ini'); return; } const device = await nav.usb.requestDevice({ filters: [] }); await device.open(); if (device.configuration === null) { await device.selectConfiguration(1); } const iface = device.configuration.interfaces[0]; await device.claimInterface(iface.interfaceNumber); setState(prev => ({ ...prev, device, type: 'usb', isConnected: true, name: device.productName || 'USB Printer', })); localStorage.setItem('last_printer_type', 'usb'); toast.success(`Terhubung ke ${device.productName || 'Printer USB'}`); } catch (error: any) { console.error('USB connection error:', error); if (error.name !== 'NotFoundError') { toast.error(error.message || 'Gagal menghubungkan printer USB'); } } }, []); const setPaperSize = useCallback((size: PaperSize) => { setState(prev => ({ ...prev, paperSize: size })); }, []); const sendData = useCallback(async (data: Uint8Array) => { if (!state.isConnected || !state.device) { toast.error('Printer belum terhubung'); return; } try { if (state.type === 'bluetooth') { const device = state.device; let activeChar = characteristic; // Try to reconnect if GATT is disconnected if (!device.gatt?.connected) { toast.loading('Menghubungkan kembali...', { id: 'printer-reconnect' }); try { const server = await device.gatt?.connect(); const services = await server.getPrimaryServices(); const writeChar = await findWriteCharacteristic(services); if (writeChar) { setCharacteristic(writeChar); activeChar = writeChar; toast.success('Printer terhubung kembali', { id: 'printer-reconnect' }); } else { throw new Error('Gagal menemukan characteristic'); } } catch (e) { toast.error('Printer terputus', { id: 'printer-reconnect' }); setState(prev => ({ ...prev, device: null, type: null, isConnected: false, name: null })); setCharacteristic(null); return; } } if (!activeChar) { throw new Error('Characteristic tidak ditemukan'); } // Bluetooth printers often have small buffers and limited MTU. // 20 bytes is the standard BLE MTU. const chunkSize = 20; for (let i = 0; i < data.length; i += chunkSize) { const chunk = data.slice(i, i + chunkSize); // Prefer writeValue (with response) for better reliability // it acts as a primitive flow control await activeChar.writeValue(chunk); // Add a tiny delay between chunks if the data is large to avoid buffer overflow // especially for cheaper printers if (i % 100 === 0 && i > 0) { await new Promise(resolve => setTimeout(resolve, 50)); } } } else if (state.type === 'usb') { const device = state.device as any; try { // Check if device is still open if (!device.opened) { await device.open(); } const endpoint = device.configuration.interfaces[0].alternates[0].endpoints.find((e: any) => e.direction === 'out' && e.type === 'bulk'); if (!endpoint) { throw new Error('Tidak dapat menemukan USB endpoint'); } await device.transferOut(endpoint.endpointNumber, data); } catch (e) { console.error('USB print error:', e); setState(prev => ({ ...prev, device: null, type: null, isConnected: false, name: null })); toast.error('Printer USB terputus'); return; } } } catch (error: any) { console.error('Print error:', error); toast.error('Gagal mengirim data ke printer: ' + (error.message || 'Error tidak diketahui')); } }, [state.isConnected, state.device, state.type, characteristic]); const disconnect = useCallback(async () => { if (state.type === 'bluetooth' && state.device) { if (state.device.gatt?.connected) { state.device.gatt.disconnect(); } } else if (state.type === 'usb' && state.device) { try { await state.device.close(); } catch (e) { console.error('Error closing USB device:', e); } } localStorage.removeItem('last_printer_type'); setState(prev => ({ ...prev, device: null, type: null, isConnected: false, name: null })); setCharacteristic(null); toast.info('Printer terputus'); }, [state.device, state.type]); return ( {children} ); } export function usePrinterContext() { const context = useContext(PrinterContext); if (context === undefined) { throw new Error('usePrinterContext must be used within a PrinterProvider'); } return context; }