From 32236ada9f44e744c238ae98a9fe5f93e6e7875f Mon Sep 17 00:00:00 2001 From: "Sameer .I. Siddiqui" Date: Tue, 10 Nov 2026 18:31:41 +0530 Subject: [PATCH 1/4] fix(android): preserve user gesture for native thermal printing --- .../[saleId]/receipt-print-actions.tsx | 57 ++++++++++++++----- .../pos/receipts/[saleId]/receipt.css | 7 +++ src/lib/pos/native-print.ts | 31 ++++++++-- tests/native-print-client.test.ts | 51 ++++++++++++++++- 4 files changed, 125 insertions(+), 21 deletions(-) diff --git a/src/app/(dashboard)/dashboard/pos/receipts/[saleId]/receipt-print-actions.tsx b/src/app/(dashboard)/dashboard/pos/receipts/[saleId]/receipt-print-actions.tsx index 973e1a44d..313894525 100644 --- a/src/app/(dashboard)/dashboard/pos/receipts/[saleId]/receipt-print-actions.tsx +++ b/src/app/(dashboard)/dashboard/pos/receipts/[saleId]/receipt-print-actions.tsx @@ -1,8 +1,12 @@ "use client"; -import { useEffect, useRef, useState, useSyncExternalStore } from "react"; +import { type MouseEvent, useEffect, useRef, useState, useSyncExternalStore } from "react"; import { isAndroidInstalledClient, printThermalBle } from "@/lib/pos/thermal-ble"; -import { printViaNativeBridge } from "@/lib/pos/native-print"; +import { + nativePrintIsExpired, + prepareNativePrint, + type PreparedNativePrint, +} from "@/lib/pos/native-print"; import type { ThermalPaper, ThermalReceipt } from "@/lib/pos/thermal-receipt"; function subscribeInstalledApp(onChange: () => void) { @@ -48,6 +52,11 @@ export function ReceiptPrintActions({ ); const [printing, setPrinting] = useState(false); const [message, setMessage] = useState(null); + const [preparedNative, setPreparedNative] = useState< + (PreparedNativePrint & { saleId: string; paper: ThermalPaper }) | null + >(null); + const readyNative = + preparedNative?.saleId === saleId && preparedNative.paper === paper ? preparedNative : null; useEffect(() => { if (!auto || triggered.current || environment !== "browser") return; @@ -56,20 +65,33 @@ export function ReceiptPrintActions({ return () => window.clearTimeout(id); }, [auto, environment]); - async function printNative() { + async function prepareNative() { if (printing) return; setPrinting(true); + setPreparedNative(null); setMessage(null); try { - await printViaNativeBridge(saleId, paper); - setMessage("Opening the printer screen in the MenuMinute app…"); + const prepared = await prepareNativePrint(saleId, paper, window.location.href); + setPreparedNative({ ...prepared, saleId, paper }); + setMessage("Receipt ready for two minutes. Tap Open printer to choose your device."); } catch (error) { - setMessage(error instanceof Error ? error.message : "Could not start printing."); + setMessage(error instanceof Error ? error.message : "Could not prepare printing."); } finally { setPrinting(false); } } + // This callback runs synchronously on a fresh link click, with no awaited network work. + function openNative(event: MouseEvent) { + if (!readyNative || nativePrintIsExpired(readyNative)) { + event.preventDefault(); + setPreparedNative(null); + setMessage("This print request expired. Tap Prepare receipt again."); + return; + } + setMessage("Opening the MenuMinute printer screen…"); + } + async function printBluetooth() { if (printing) return; setPrinting(true); @@ -97,14 +119,21 @@ export function ReceiptPrintActions({ return (
{environment === "ble" && nativePrint && ( - + <> + + {readyNative && ( + + Open printer (Bluetooth / USB) + + )} + )} {environment === "ble" && (