Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3e56ab5
feat(printer): typed discovery error codes and bluetooth candidate type
kilbot Jun 12, 2026
678d8ea
feat(printer): bluetooth chooser session state machine with timeouts
kilbot Jun 12, 2026
e7a20e7
fix(printer): per-session generation guards in bluetooth scan session
kilbot Jun 12, 2026
76f2591
feat(printer): managed bluetooth chooser session, usb busy state, typ…
kilbot Jun 12, 2026
b89ab6b
fix(printer): clear stale chooser candidates on session start; cover …
kilbot Jun 12, 2026
0093a6b
feat(printer): route winspool addresses to native spooler transport r…
kilbot Jun 12, 2026
94c2827
feat(core): translated discovery error messages and Windows UA detection
kilbot Jun 12, 2026
b535757
feat(core): props-driven BT picker and Windows installed-printers sec…
kilbot Jun 12, 2026
f6f8c32
feat(core): bluetooth scan feedback states, Windows installed printer…
kilbot Jun 12, 2026
a51ec72
fix(core): suppress usb-none-found on bluetooth tab (installed sectio…
kilbot Jun 12, 2026
2d7a0b4
chore: add serialport to electron app (lockfile + submodule pointer)
kilbot Jun 12, 2026
91341c8
feat(printer): serial transport adapter and paired-serial discovery f…
kilbot Jun 12, 2026
84b9b42
chore: approve @serialport/bindings-cpp build; bump electron pointer
kilbot Jun 12, 2026
d9ebfed
feat(core): paired Bluetooth printers section for macOS/Linux via OS-…
kilbot Jun 12, 2026
bc2cacb
docs(core): qualify usb-none-found suppression comment for Windows
kilbot Jun 12, 2026
73c844c
feat(core): lead Bluetooth tab with paired/installed printers; clarif…
kilbot Jun 12, 2026
2842e8d
docs(printer): replace review-iteration comment labels with descripti…
kilbot Jun 12, 2026
a28be51
chore: point electron submodule at rebased fix/bluetooth-chooser-sess…
kilbot Jun 12, 2026
c6f7d61
feat(core): auto-load USB printer list on tab entry; scan button beco…
kilbot Jun 12, 2026
3320719
fix: override shell-quote to >=1.8.4 (GHSA-w7jw-789q-3m8p)
kilbot Jun 12, 2026
f2119d7
fix: end chooser session on synchronous Web Bluetooth failures; add m…
kilbot Jun 12, 2026
dd628cb
fix: override vulnerable tmp transitive dependency
Jun 12, 2026
f5d5d21
fix: clear serial print timeout after ipc settles
Jun 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/core/src/contexts/translations/locales/en/core.json
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,10 @@
"settings.barcode_scanning": "Barcode Scanning",
"settings.barcode_scanning_test": "Barcode Scanning Test",
"settings.bluetooth_printer": "Bluetooth Printer",
"settings.bt_connect_failed": "Could not connect to the selected Bluetooth device.",
"settings.bt_none_found": "No Bluetooth (BLE) printers found. Make sure the printer is powered on and in pairing mode. Bluetooth Classic printers cannot be discovered by this scan — if your printer is paired with this computer, it appears under Paired or Installed printers.",
"settings.bt_scan_hint": "Finds nearby Bluetooth LE printers (e.g. Epson TM-P or Star L series). Bluetooth Classic printers pair in your system settings instead.",
"settings.bt_searching": "Searching for Bluetooth printers…",
"settings.choose_a_theme_for_the_app": "Choose a theme for the app. Custom themes provide different color schemes.",
"settings.cloud_printer": "Cloud printer",
"settings.connect_bt_printer": "Connect Bluetooth printer",
Expand Down Expand Up @@ -612,23 +616,33 @@
"settings.full_receipt_raster": "Full receipt raster",
"settings.full_receipt_raster_help": "Prints the whole receipt as an image for Unicode/RTL compatibility. Slower and larger than text mode.",
"settings.general_settings": "General Settings",
"settings.installed_printers": "Installed printers",
"settings.installed_printers_hint": "Printers paired in Windows (including Bluetooth printers) appear here as installed printers.",
"settings.installed_printers_loading": "Loading installed printers…",
"settings.installed_printers_none": "No installed printers found.",
"settings.key_combination": "Key Combination",
"settings.keyboard_shortcuts": "Keyboard Shortcuts",
"settings.keypress_event": "Keypress Event",
"settings.language": "Language",
"settings.likely_reason": "Likely reason",
"settings.managed_by_wcpos": "managed by WCPOS",
"settings.network_none_found": "No network printers found. Make sure the printer is on the same network, or enter its IP address manually.",
"settings.no_devices_found": "No devices found yet.",
"settings.no_printers_body": "Add a printer to send receipts straight to your hardware. You can always use the Print Dialog without one.",
"settings.no_printers_configured": "No printers configured",
"settings.no_printers_found": "No printers found",
"settings.no_templates": "No active templates found.",
"settings.number_of_decimals": "Number of Decimals",
"settings.paired_printers": "Paired Bluetooth printers",
"settings.paired_printers_hint": "Bluetooth Classic printers paired in your system settings appear here.",
"settings.paired_printers_loading": "Loading paired printers…",
"settings.paired_printers_none": "No paired printers found.",
"settings.price_display_suffix": "Price display suffix",
"settings.prices_entered_with_tax": "Prices entered with tax",
"settings.printer_address": "IP Address",
"settings.printer_discovered_at": "Discovered: %s",
"settings.printer_discovery_error": "Printer discovery error: %s",
"settings.printer_ipc_unavailable": "Printer discovery is unavailable in this window.",
"settings.printer_language": "Printer Language",
"settings.printer_name": "Printer Name",
"settings.printer_name_placeholder": "e.g. Receipt Printer",
Expand Down Expand Up @@ -656,6 +670,7 @@
"settings.scan_candidates_progress": "Checking common printer addresses… %s / %s",
"settings.scan_for_printers": "Scan for printers",
"settings.scan_network": "Scan Network",
"settings.scanning": "Scanning…",
"settings.searching_for_printers": "Searching for printers...",
"settings.select_language": "Select printer language",
"settings.select_printer": "Select printer...",
Expand Down Expand Up @@ -693,6 +708,7 @@
"settings.thousand_separator": "Thousand Separator",
"settings.thousands_group_style": "Thousands Group Style",
"settings.try_next": "Try",
"settings.usb_none_found": "No USB printers found.",
"settings.usb_printer": "USB Printer",
"settings.use_this_printer": "Use this printer",
"settings.we_tried": "We tried",
Expand Down
132 changes: 103 additions & 29 deletions packages/core/src/screens/main/settings/printer/add-printer.electron.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ import { Pressable, View } from 'react-native';
import { useWatch } from 'react-hook-form';

import { Button } from '@wcpos/components/button';
import { HStack } from '@wcpos/components/hstack';
import { Text } from '@wcpos/components/text';
import { VStack } from '@wcpos/components/vstack';
import { type DiscoveredPrinter, type PrinterProfile, usePrinterDiscovery } from '@wcpos/printer';

import { AdvancedSettings } from './dialog/advanced-settings';
import { ConnectionTypeSegmented } from './dialog/connection/connection-type-segmented';
import { ElectronBtPicker } from './dialog/connection/electron-bt-picker';
import { OsPrintersSection } from './dialog/connection/os-printers-section';
import { UsbPrintersSection } from './dialog/connection/usb-printers-section';
import { isWindowsPlatform } from './dialog/connection/is-windows';
import { NetworkFields } from './dialog/connection/network-fields';
import { formatDiscoveryError } from './dialog/discovery-error-message';
import { PrinterDialogFooter } from './dialog/printer-dialog-footer';
import { PrinterDialogLayout } from './dialog/printer-dialog-layout';
import { TestPrintError } from './dialog/test-print-error';
Expand Down Expand Up @@ -109,7 +114,14 @@ export function PrinterDialog({
startScan,
isScanning: scanning,
connectUsbDevice,
isUsbScanning,
connectBluetoothDevice,
isBluetoothScanning,
bluetoothCandidates,
selectBluetoothCandidate,
cancelBluetoothScan,
connectSerialDevice,
isSerialScanning,
error: discoveryError,
} = usePrinterDiscovery();
const {
Expand Down Expand Up @@ -141,6 +153,12 @@ export function PrinterDialog({
defaultValue: DEFAULT_FORM_VALUES.connectionType,
});

// On Windows the BT tab auto-runs the installed-printers (usb-discovery) scan; its empty
// result is reported by the section's own empty state, not the shared error line. (The
// macOS/Linux serial auto-scan sets no error on empty by hook design.)
const suppressedDiscoveryError =
connectionType === 'bluetooth' && discoveryError?.code === 'usb-none-found';

const vendorOptions: VendorOption[] = [
{ value: 'epson', label: 'Epson' },
{ value: 'star', label: 'Star Micronics' },
Expand All @@ -150,37 +168,96 @@ export function PrinterDialog({
let connectionSection: React.ReactNode;
if (connectionType === 'usb') {
connectionSection = (
<VStack className="gap-2">
{connectUsbDevice && (
<Button
testID="add-printer-electron-usb-scan-button"
variant="outline"
size="sm"
className="self-start"
onPress={connectUsbDevice}
>
<Text>{t('settings.scan_for_printers', 'Scan for printers')}</Text>
</Button>
)}
<UsbPrintersSection onScan={connectUsbDevice} scanning={isUsbScanning}>
<DeviceList form={form} printers={printers} type="usb" />
</VStack>
</UsbPrintersSection>
);
} else if (connectionType === 'bluetooth') {
connectionSection = (
<VStack className="gap-2">
{isWindowsPlatform() ? (
<OsPrintersSection
form={form}
printers={printers}
onScan={connectUsbDevice}
scanning={isUsbScanning}
addressPrefix="winspool:"
heading={t('settings.installed_printers', 'Installed printers')}
hint={t(
'settings.installed_printers_hint',
'Printers paired in Windows (including Bluetooth printers) appear here as installed printers.'
)}
emptyText={t('settings.installed_printers_none', 'No installed printers found.')}
loadingText={t('settings.installed_printers_loading', 'Loading installed printers…')}
testIdPrefix="add-printer-installed-device"
/>
) : connectSerialDevice ? (
<OsPrintersSection
form={form}
printers={printers}
onScan={connectSerialDevice}
scanning={isSerialScanning}
addressPrefix="serial:"
heading={t('settings.paired_printers', 'Paired Bluetooth printers')}
hint={t(
'settings.paired_printers_hint',
'Bluetooth Classic printers paired in your system settings appear here.'
)}
emptyText={t('settings.paired_printers_none', 'No paired printers found.')}
loadingText={t('settings.paired_printers_loading', 'Loading paired printers…')}
testIdPrefix="add-printer-paired-device"
/>
) : null}
{connectBluetoothDevice && (
<Button
testID="add-printer-electron-bt-scan-button"
variant="outline"
size="sm"
className="self-start"
onPress={connectBluetoothDevice}
>
<Text>{t('settings.scan_for_printers', 'Scan for printers')}</Text>
</Button>
<HStack className="gap-2">
<Button
testID="add-printer-electron-bt-scan-button"
variant="outline"
size="sm"
className="self-start"
onPress={connectBluetoothDevice}
loading={!!isBluetoothScanning}
disabled={!!isBluetoothScanning}
>
<Text>
{isBluetoothScanning
? t('settings.scanning', 'Scanning…')
: t('settings.scan_for_printers', 'Scan for printers')}
</Text>
</Button>
{isBluetoothScanning && cancelBluetoothScan && (
<Button
testID="add-printer-electron-bt-cancel-button"
variant="ghost"
size="sm"
onPress={cancelBluetoothScan}
>
<Text>{t('common.cancel', 'Cancel')}</Text>
</Button>
)}
</HStack>
)}
<Text testID="add-printer-bt-scan-hint" className="text-muted-foreground text-xs">
{t(
'settings.bt_scan_hint',
'Finds nearby Bluetooth LE printers (e.g. Epson TM-P or Star L series). Bluetooth Classic printers pair in your system settings instead.'
)}
</Text>
{isBluetoothScanning && (
<Text testID="add-printer-bt-searching" className="text-muted-foreground text-xs">
{t('settings.bt_searching', 'Searching for Bluetooth printers…')}
</Text>
)}
<ElectronBtPicker />
<DeviceList form={form} printers={printers} type="bluetooth" />
<ElectronBtPicker
candidates={bluetoothCandidates ?? []}
onSelect={(id) => selectBluetoothCandidate?.(id)}
/>
{/* serial: entries are owned by the paired-printers section above; exclude them here */}
<DeviceList
form={form}
printers={printers.filter((p) => !p.address?.startsWith('serial:'))}
type="bluetooth"
/>
</VStack>
);
} else {
Expand Down Expand Up @@ -215,12 +292,9 @@ export function PrinterDialog({
}}
/>
{connectionSection}
{discoveryError && (
{discoveryError && !suppressedDiscoveryError && (
<Text testID="add-printer-discovery-error" className="text-muted-foreground text-xs">
{t('settings.printer_discovery_error', 'Printer discovery error: %s').replace(
'%s',
discoveryError
)}
{formatDiscoveryError(discoveryError, t)}
</Text>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { usePrinterDiscovery } from '@wcpos/printer';
import { Text } from '@wcpos/components/text';

import { AdvancedSettings } from './dialog/advanced-settings';
import { formatDiscoveryError } from './dialog/discovery-error-message';
import { BluetoothDevicePicker } from './dialog/connection/bluetooth-device-picker';
import { ConnectionTypeSegmented } from './dialog/connection/connection-type-segmented';
import { NetworkFields } from './dialog/connection/network-fields';
Expand Down Expand Up @@ -135,10 +136,7 @@ export function PrinterDialog({
{connectionSection}
{discoveryError && (
<Text testID="add-printer-discovery-error" className="text-muted-foreground text-xs">
{t('settings.printer_discovery_error', 'Printer discovery error: %s').replace(
'%s',
discoveryError
)}
{formatDiscoveryError(discoveryError, t)}
</Text>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '@wcpos/printer';

import { AdvancedSettings } from './dialog/advanced-settings';
import { formatDiscoveryError } from './dialog/discovery-error-message';
import { ConnectionTypeSegmented } from './dialog/connection/connection-type-segmented';
import { NetworkFields } from './dialog/connection/network-fields';
import { WebVendorSegmented } from './dialog/connection/web-vendor-segmented';
Expand Down Expand Up @@ -243,10 +244,7 @@ export function PrinterDialog({
{webDeviceSection}
{discovery.error && (
<Text testID="add-printer-discovery-error" className="text-muted-foreground text-xs">
{t('settings.printer_discovery_error', 'Printer discovery error: %s').replace(
'%s',
discovery.error
)}
{formatDiscoveryError(discovery.error, t)}
</Text>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/** @jest-environment jsdom */
import '@testing-library/jest-dom';
import * as React from 'react';

import { fireEvent, render, screen } from '@testing-library/react';

import { ElectronBtPicker } from './electron-bt-picker';

jest.mock('react-native', () => ({
Pressable: ({
children,
onPress,
testID,
}: {
children?: React.ReactNode;
onPress?: () => void;
testID?: string;
}) => (
<button type="button" data-testid={testID} onClick={onPress}>
{children}
</button>
),
View: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
}));

jest.mock('@wcpos/components/text', () => ({
Text: ({ children }: { children?: React.ReactNode }) => <span>{children}</span>,
}));

jest.mock('@wcpos/components/vstack', () => ({
VStack: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
}));

describe('ElectronBtPicker', () => {
it('renders nothing with no candidates', () => {
const { container } = render(<ElectronBtPicker candidates={[]} onSelect={jest.fn()} />);
expect(container).toBeEmptyDOMElement();
});

it('renders candidates and reports selection', () => {
const onSelect = jest.fn();
render(
<ElectronBtPicker
candidates={[
{ id: 'a', name: 'Printer A' },
{ id: 'b', name: '' },
]}
onSelect={onSelect}
/>
);
expect(screen.getByText('Printer A')).toBeInTheDocument();
// Unnamed devices fall back to their id.
expect(screen.getByText('b')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('electron-bt-device-a'));
expect(onSelect).toHaveBeenCalledWith('a');
});
});
Loading
Loading