@ruhiverse/thermal-printer-plugin 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,76 @@ import CoreBluetooth
10
10
  @objc(ThermalPrinterPlugin)
11
11
  public class ThermalPrinterPlugin: CAPPlugin {
12
12
 
13
+ /// Protocol identifiers for supported MFi ESC/POS printers.
14
+ /// Update this list with the protocol strings for your specific printer model.
15
+ private let supportedPrinterProtocols = [
16
+ "com.epson.escpos",
17
+ "com.starmicronics.starprnt",
18
+ ]
19
+
20
+ /// Find a connected External Accessory that matches the given (optional) name
21
+ /// and supports one of the known printer protocols.
22
+ private func findPrinterAccessory(preferredName: String?) -> (EAAccessory, String)? {
23
+ let manager = EAAccessoryManager.shared()
24
+ let accessories = manager.connectedAccessories
25
+
26
+ // Filter accessories that support at least one of the known printer protocols
27
+ let printerAccessories: [(EAAccessory, String)] = accessories.compactMap { accessory in
28
+ if let protocolString = accessory.protocolStrings.first(where: { supportedPrinterProtocols.contains($0) }) {
29
+ return (accessory, protocolString)
30
+ }
31
+ return nil
32
+ }
33
+
34
+ guard !printerAccessories.isEmpty else {
35
+ return nil
36
+ }
37
+
38
+ // If a preferredName is provided, try to match it against the accessory name
39
+ if let name = preferredName, !name.isEmpty {
40
+ if let match = printerAccessories.first(where: { (accessory, _) in
41
+ accessory.name.localizedCaseInsensitiveContains(name)
42
+ }) {
43
+ return match
44
+ }
45
+ }
46
+
47
+ // Fallback: just return the first compatible printer
48
+ return printerAccessories.first
49
+ }
50
+
51
+ /// Send raw ESC/POS (or plain text) data to the given accessory/protocol.
52
+ private func sendPrintData(
53
+ _ data: Data,
54
+ to accessory: EAAccessory,
55
+ using protocolString: String
56
+ ) throws {
57
+ guard let session = EASession(accessory: accessory, forProtocol: protocolString),
58
+ let outputStream = session.outputStream else {
59
+ throw NSError(domain: "ThermalPrinterPlugin", code: 1, userInfo: [
60
+ NSLocalizedDescriptionKey: "Unable to open session/output stream for printer.",
61
+ ])
62
+ }
63
+
64
+ outputStream.open()
65
+ defer {
66
+ outputStream.close()
67
+ }
68
+
69
+ let bytesWritten = data.withUnsafeBytes { buffer -> Int in
70
+ guard let pointer = buffer.bindMemory(to: UInt8.self).baseAddress else {
71
+ return -1
72
+ }
73
+ return outputStream.write(pointer, maxLength: data.count)
74
+ }
75
+
76
+ if bytesWritten <= 0 {
77
+ throw NSError(domain: "ThermalPrinterPlugin", code: 2, userInfo: [
78
+ NSLocalizedDescriptionKey: "Failed to send data to printer.",
79
+ ])
80
+ }
81
+ }
82
+
13
83
  @objc func printByUsb(_ call: CAPPluginCall) {
14
84
  guard let textToPrint = call.getString("textToPrint") else {
15
85
  call.reject("textToPrint is required")
@@ -21,19 +91,42 @@ public class ThermalPrinterPlugin: CAPPlugin {
21
91
  // your Info.plist with supported protocols for your printer
22
92
 
23
93
  DispatchQueue.global(qos: .userInitiated).async {
24
- // For iOS, USB printing typically requires:
25
- // 1. External Accessory framework
26
- // 2. Supported protocol strings in Info.plist
27
- // 3. MFi (Made for iPhone) certification for the printer
94
+ // For many iOS thermal printers, USB/MFi printers are exposed
95
+ // via the ExternalAccessory framework using the same protocol
96
+ // strings as Bluetooth / network models.
97
+ let preferredName = call.getString("name")
28
98
 
29
- // Basic implementation - send ESC/POS commands
30
- // You'll need to implement actual USB connection based on your printer model
99
+ guard let (accessory, protocolString) = self.findPrinterAccessory(preferredName: preferredName) else {
100
+ DispatchQueue.main.async {
101
+ call.reject("No compatible printer accessory found. Make sure your printer is connected and UISupportedExternalAccessoryProtocols in Info.plist contains its protocol.")
102
+ }
103
+ return
104
+ }
31
105
 
32
- DispatchQueue.main.async {
33
- call.resolve([
34
- "success": true,
35
- "message": "USB print command sent (iOS implementation may require additional setup)"
36
- ])
106
+ // Convert text to basic ESC/POS-compatible data (UTF-8 + line feed).
107
+ // NOTE: This is a minimal implementation and does not yet parse
108
+ // formatting tags like [C], <b>, etc. Those will print as plain text.
109
+ guard let data = (textToPrint + "\n\n").data(using: .utf8) else {
110
+ DispatchQueue.main.async {
111
+ call.reject("Failed to encode text as UTF-8.")
112
+ }
113
+ return
114
+ }
115
+
116
+ do {
117
+ try self.sendPrintData(data, to: accessory, using: protocolString)
118
+ DispatchQueue.main.async {
119
+ call.resolve([
120
+ "success": true,
121
+ "message": "USB print data sent to printer.",
122
+ "printerName": accessory.name,
123
+ "protocol": protocolString,
124
+ ])
125
+ }
126
+ } catch {
127
+ DispatchQueue.main.async {
128
+ call.reject("USB print failed: \(error.localizedDescription)")
129
+ }
37
130
  }
38
131
  }
39
132
  }
@@ -49,32 +142,87 @@ public class ThermalPrinterPlugin: CAPPlugin {
49
142
  // Bluetooth permissions in Info.plist
50
143
 
51
144
  DispatchQueue.global(qos: .userInitiated).async {
52
- // For iOS, Bluetooth printing requires:
53
- // 1. Core Bluetooth framework
54
- // 2. NSBluetoothAlwaysUsageDescription in Info.plist
55
- // 3. Actual Bluetooth connection implementation
145
+ // On many MFi / ExternalAccessory-based thermal printers, the
146
+ // Bluetooth connection is abstracted behind the ExternalAccessory
147
+ // protocols (e.g., com.epson.escpos, com.starmicronics.starprnt).
148
+ // We reuse the same ExternalAccessory-based implementation here.
149
+
150
+ let preferredName = call.getString("name")
151
+
152
+ guard let (accessory, protocolString) = self.findPrinterAccessory(preferredName: preferredName) else {
153
+ DispatchQueue.main.async {
154
+ call.reject("No compatible Bluetooth printer found. Make sure it is paired, connected, and its protocol is listed in UISupportedExternalAccessoryProtocols.")
155
+ }
156
+ return
157
+ }
56
158
 
57
- // Basic implementation - send ESC/POS commands
58
- // You'll need to implement actual Bluetooth connection based on your printer model
159
+ // Minimal text ESC/POS data conversion.
160
+ // Formatting tags like [C] or <b> are not parsed yet and will
161
+ // be sent as plain text.
162
+ guard let data = (textToPrint + "\n\n").data(using: .utf8) else {
163
+ DispatchQueue.main.async {
164
+ call.reject("Failed to encode text as UTF-8.")
165
+ }
166
+ return
167
+ }
59
168
 
60
- DispatchQueue.main.async {
61
- call.resolve([
62
- "success": true,
63
- "message": "Bluetooth print command sent (iOS implementation may require additional setup)"
64
- ])
169
+ do {
170
+ try self.sendPrintData(data, to: accessory, using: protocolString)
171
+ DispatchQueue.main.async {
172
+ call.resolve([
173
+ "success": true,
174
+ "message": "Bluetooth print data sent to printer.",
175
+ "printerName": accessory.name,
176
+ "protocol": protocolString,
177
+ ])
178
+ }
179
+ } catch {
180
+ DispatchQueue.main.async {
181
+ call.reject("Bluetooth print failed: \(error.localizedDescription)")
182
+ }
65
183
  }
66
184
  }
67
185
  }
68
186
 
69
187
  @objc func listBluetoothPrinters(_ call: CAPPluginCall) {
188
+ let manager = EAAccessoryManager.shared()
189
+ let accessories = manager.connectedAccessories
190
+
191
+ let printers: [[String: String]] = accessories.compactMap { accessory in
192
+ guard accessory.protocolStrings.contains(where: { supportedPrinterProtocols.contains($0) }) else {
193
+ return nil
194
+ }
195
+ return [
196
+ "name": accessory.name,
197
+ // iOS doesn't expose a MAC address for accessories; we
198
+ // return the serial number or a generated identifier.
199
+ "address": accessory.serialNumber.isEmpty ? "\(accessory.connectionID)" : accessory.serialNumber,
200
+ ]
201
+ }
202
+
70
203
  call.resolve([
71
- "printers": []
204
+ "printers": printers,
72
205
  ])
73
206
  }
74
207
 
75
208
  @objc func listUsbPrinters(_ call: CAPPluginCall) {
209
+ // On iOS, many thermal printers expose both USB and Bluetooth via the
210
+ // ExternalAccessory framework. We reuse the same discovery logic here.
211
+ let manager = EAAccessoryManager.shared()
212
+ let accessories = manager.connectedAccessories
213
+
214
+ let printers: [[String: String]] = accessories.compactMap { accessory in
215
+ guard accessory.protocolStrings.contains(where: { supportedPrinterProtocols.contains($0) }) else {
216
+ return nil
217
+ }
218
+ return [
219
+ "name": accessory.name,
220
+ "address": accessory.serialNumber.isEmpty ? "\(accessory.connectionID)" : accessory.serialNumber,
221
+ ]
222
+ }
223
+
76
224
  call.resolve([
77
- "printers": []
225
+ "printers": printers,
78
226
  ])
79
227
  }
80
228
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruhiverse/thermal-printer-plugin",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Capacitor plugin for thermal printing via USB and Bluetooth",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",