seven365-zyprinter 0.4.0 → 1.0.0

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.
@@ -8,6 +8,7 @@
8
8
  import Foundation
9
9
  import CoreBluetooth
10
10
  import Network
11
+ import ExternalAccessory
11
12
 
12
13
 
13
14
  @objc public class ZywellSDK: NSObject {
@@ -79,6 +80,51 @@ import Network
79
80
  scanSubnet(prefix: prefix, completion: completion)
80
81
  }
81
82
 
83
+ @objc public func discoverUSBPrinters(completion: @escaping ([[String: Any]], String?) -> Void) {
84
+ // Query External Accessory framework for connected accessories
85
+ let accessoryManager = EAAccessoryManager.shared()
86
+ let connectedAccessories = accessoryManager.connectedAccessories
87
+
88
+ var usbPrinters: [[String: Any]] = []
89
+
90
+ // Filter for printer accessories
91
+ // Note: This requires the app to declare supported accessory protocols in Info.plist
92
+ // under the key "UISupportedExternalAccessoryProtocols"
93
+ for accessory in connectedAccessories {
94
+ // Check if the accessory might be a printer
95
+ // Common printer protocol strings include manufacturer-specific identifiers
96
+ let isPotentialPrinter = accessory.protocolStrings.contains { protocolString in
97
+ protocolString.lowercased().contains("printer") ||
98
+ protocolString.lowercased().contains("print") ||
99
+ protocolString.lowercased().contains("pos")
100
+ }
101
+
102
+ if isPotentialPrinter || !accessory.protocolStrings.isEmpty {
103
+ usbPrinters.append([
104
+ "identifier": String(accessory.connectionID),
105
+ "model": "\(accessory.manufacturer) \(accessory.name)",
106
+ "status": accessory.isConnected ? "ready" : "offline",
107
+ "connectionType": "usb",
108
+ "manufacturer": accessory.manufacturer,
109
+ "modelNumber": accessory.modelNumber,
110
+ "serialNumber": accessory.serialNumber,
111
+ "firmwareRevision": accessory.firmwareRevision,
112
+ "hardwareRevision": accessory.hardwareRevision,
113
+ "protocols": accessory.protocolStrings
114
+ ])
115
+ }
116
+ }
117
+
118
+ // Log information about USB discovery
119
+ if usbPrinters.isEmpty {
120
+ print("ZywellSDK: No USB accessories found. Note: iOS requires MFi-certified accessories with declared protocol strings in Info.plist")
121
+ } else {
122
+ print("ZywellSDK: Found \(usbPrinters.count) potential USB printer(s)")
123
+ }
124
+
125
+ completion(usbPrinters, nil)
126
+ }
127
+
82
128
  private func scanSubnet(prefix: String, completion: @escaping ([[String: Any]], String?) -> Void) {
83
129
  let group = DispatchGroup()
84
130
  let queue = DispatchQueue(label: "com.seven365.printer.scan", attributes: .concurrent)
@@ -323,30 +369,267 @@ import Network
323
369
  // Center align (ESC a 1)
324
370
  printData.append(Data([0x1B, 0x61, 0x01]))
325
371
 
326
- // Header
327
- if let header = getString(template["header"]),
328
- let headerData = (header + "\n\n").data(using: .utf8) {
372
+ // ========== HEADER SECTION (NEW STRUCTURE) ==========
373
+ if let header = template["header"] as? [String: Any] {
374
+ // Get header formatting
375
+ var sizeCode: UInt8 = 0x00
376
+ var headerBold = false
329
377
 
330
- // Get header size from formatting options
331
- var sizeCode: UInt8 = 0x00 // Default: normal
332
- if let formatting = template["formatting"] as? [String: Any],
333
- let headerSize = formatting["headerSize"] {
334
- sizeCode = mapHeaderSizeToCode(headerSize)
378
+ if let size = header["size"] {
379
+ sizeCode = mapHeaderSizeToCode(size)
380
+ }
381
+ if let bold = header["bold"] as? Bool {
382
+ headerBold = bold
335
383
  }
336
- print("ZywellSDK: Header size code: \(sizeCode)")
337
384
 
338
- // Set font size (GS ! n)
339
- printData.append(Data([0x1D, 0x21, sizeCode]))
385
+ // Apply bold if needed
386
+ if headerBold {
387
+ printData.append(Data([0x1B, 0x45, 0x01])) // Bold on
388
+ }
340
389
 
341
- printData.append(headerData)
390
+ // Set font size
391
+ if sizeCode != 0x00 {
392
+ printData.append(Data([0x1D, 0x21, sizeCode]))
393
+ }
394
+
395
+ // Restaurant name
396
+ if let restaurantName = getString(header["restaurant_name"]),
397
+ let nameData = (restaurantName + "\n").data(using: .utf8) {
398
+ printData.append(nameData)
399
+ }
400
+
401
+ // Sub header
402
+ if let subHeader = getString(header["sub_header"]),
403
+ !subHeader.isEmpty,
404
+ let subData = (subHeader + "\n").data(using: .utf8) {
405
+ printData.append(subData)
406
+ }
342
407
 
343
- // Reset to normal size (GS ! 0)
344
- printData.append(Data([0x1D, 0x21, 0x00]))
408
+ // GST number
409
+ if let gstNumber = getString(header["gst_number"]),
410
+ !gstNumber.isEmpty,
411
+ let gstData = ("GST number: \(gstNumber)\n").data(using: .utf8) {
412
+ printData.append(gstData)
413
+ }
414
+
415
+ // Reset header formatting
416
+ if sizeCode != 0x00 {
417
+ printData.append(Data([0x1D, 0x21, 0x00])) // Normal size
418
+ }
419
+ if headerBold {
420
+ printData.append(Data([0x1B, 0x45, 0x00])) // Bold off
421
+ }
345
422
  }
346
423
 
347
424
  // Left align (ESC a 0)
348
425
  printData.append(Data([0x1B, 0x61, 0x00]))
349
426
 
427
+ // Separator
428
+ if let separatorData = ("--------------------------------\n").data(using: .utf8) {
429
+ printData.append(separatorData)
430
+ }
431
+
432
+ // ========== ORDER INFO SECTION ==========
433
+ // Template type (e.g., "Kitchen Tickets")
434
+ if let orderType = getString(template["order_type"]),
435
+ let orderTypeData = (orderType + "\n").data(using: .utf8) {
436
+ printData.append(orderTypeData)
437
+ }
438
+
439
+ // Table and order number with prefix
440
+ var orderInfoLine = ""
441
+ if let tableName = getString(template["table_name"]) {
442
+ orderInfoLine += tableName
443
+ }
444
+ if let orderNumber = getString(template["order_number"]) {
445
+ if !orderInfoLine.isEmpty {
446
+ orderInfoLine += " | "
447
+ }
448
+ orderInfoLine += orderNumber
449
+ }
450
+ if !orderInfoLine.isEmpty,
451
+ let orderInfoData = (orderInfoLine + "\n").data(using: .utf8) {
452
+ printData.append(orderInfoData)
453
+ }
454
+
455
+
456
+ // Separator for items section
457
+ if let separatorData = ("--------------------------------\n").data(using: .utf8) {
458
+ printData.append(separatorData)
459
+ }
460
+
461
+ // ========== ITEMS SECTION (NEW STRUCTURE) ==========
462
+ if let kitchen = template["kitchen"] as? [[String: Any]] {
463
+ // Get item formatting from item object
464
+ var itemSizeCode: UInt8 = 0x00
465
+ var itemBold = false
466
+
467
+ if let item = template["item"] as? [String: Any] {
468
+ if let size = item["size"] {
469
+ itemSizeCode = mapHeaderSizeToCode(size)
470
+ }
471
+ if let bold = item["bold"] as? Bool {
472
+ itemBold = bold
473
+ }
474
+ }
475
+
476
+ // Apply item formatting
477
+ if itemBold {
478
+ printData.append(Data([0x1B, 0x45, 0x01])) // Bold on
479
+ }
480
+ if itemSizeCode != 0x00 {
481
+ printData.append(Data([0x1D, 0x21, itemSizeCode])) // Set size
482
+ }
483
+
484
+ // Get modifier formatting from modifier object
485
+ var modifierStyle = "bullet" // default
486
+ var modifierSizeCode: UInt8 = 0x00
487
+ var modifierIndent = " " // default: medium
488
+
489
+ if let modifier = template["modifier"] as? [String: Any] {
490
+ if let style = getString(modifier["style"]) {
491
+ modifierStyle = style
492
+ }
493
+ if let size = modifier["size"] {
494
+ modifierSizeCode = mapHeaderSizeToCode(size)
495
+ }
496
+ if let indent = getString(modifier["indent"]) {
497
+ switch indent.lowercased() {
498
+ case "small": modifierIndent = " "
499
+ case "medium": modifierIndent = " "
500
+ case "large": modifierIndent = " "
501
+ default: modifierIndent = " "
502
+ }
503
+ }
504
+ }
505
+
506
+ for item in kitchen {
507
+ var itemName = ""
508
+ var quantity = "1"
509
+
510
+ // Get item name
511
+ if let name = getString(item["name"]) {
512
+ itemName = name
513
+ } else if let menu = item["menu"] as? [String: Any],
514
+ let name = getString(menu["name"]) {
515
+ itemName = name
516
+ }
517
+
518
+ // Get quantity
519
+ if let qty = item["qty"] {
520
+ quantity = "x\(getString(qty) ?? "1")"
521
+ } else if let qty = item["quantity"] {
522
+ quantity = "x\(getString(qty) ?? "1")"
523
+ }
524
+
525
+ // Print main item line
526
+ let line = String(format: "%@ %@\n", itemName, quantity)
527
+ if let lineData = line.data(using: .utf8) {
528
+ printData.append(lineData)
529
+ }
530
+
531
+ // Print modifiers
532
+ if let modifiers = item["modifiers"] as? [[String: Any]] {
533
+ // Apply modifier size if specified
534
+ if modifierSizeCode != 0x00 {
535
+ printData.append(Data([0x1D, 0x21, modifierSizeCode]))
536
+ }
537
+
538
+ for mod in modifiers {
539
+ if let modName = getString(mod["name"]) {
540
+ var prefix = ""
541
+
542
+ // Determine prefix based on style
543
+ switch modifierStyle.lowercased() {
544
+ case "dash":
545
+ prefix = "-"
546
+ case "bullet":
547
+ prefix = "•"
548
+ case "arrow":
549
+ prefix = "→"
550
+ default:
551
+ prefix = "•"
552
+ }
553
+
554
+ // Build modifier line
555
+ let modLine = "\(modifierIndent)\(prefix) \(modName)\n"
556
+
557
+ if let modData = modLine.data(using: .utf8) {
558
+ printData.append(modData)
559
+ }
560
+ }
561
+ }
562
+
563
+ // Reset modifier size if it was changed
564
+ if modifierSizeCode != 0x00 {
565
+ printData.append(Data([0x1D, 0x21, 0x00]))
566
+ }
567
+ }
568
+ }
569
+
570
+ // Reset item formatting
571
+ if itemSizeCode != 0x00 {
572
+ printData.append(Data([0x1D, 0x21, 0x00])) // Normal size
573
+ }
574
+ if itemBold {
575
+ printData.append(Data([0x1B, 0x45, 0x00])) // Bold off
576
+ }
577
+ }
578
+
579
+ // Table number
580
+ if let tableNumber = getString(template["tableNumber"]),
581
+ let tableData = ("Table:\(tableNumber)\n").data(using: .utf8) {
582
+ printData.append(tableData)
583
+ }
584
+
585
+ // Order number and type
586
+ if let orderNumber = getString(template["orderNumber"]) {
587
+ var orderLine = "ORDER NO.: \(orderNumber)\n"
588
+ if let orderType = getString(template["orderType"]) {
589
+ orderLine += "\(orderType)\n"
590
+ }
591
+ if let orderData = orderLine.data(using: .utf8) {
592
+ printData.append(orderData)
593
+ }
594
+ }
595
+
596
+ // Served by
597
+ if let servedBy = getString(template["servedBy"]),
598
+ let servedData = ("Served By:\(servedBy)\n").data(using: .utf8) {
599
+ printData.append(servedData)
600
+ }
601
+
602
+ // Date/Time
603
+ if let dateTime = template["dateTime"] {
604
+ let formatter = DateFormatter()
605
+ formatter.dateFormat = "yyyy-MMM-dd HH:mm:ss"
606
+
607
+ var dateString = ""
608
+ if let date = dateTime as? Date {
609
+ dateString = formatter.string(from: date)
610
+ } else if let dateStr = getString(dateTime) {
611
+ dateString = dateStr
612
+ }
613
+
614
+ if !dateString.isEmpty,
615
+ let dateData = ("Date:\(dateString)\n").data(using: .utf8) {
616
+ printData.append(dateData)
617
+ }
618
+ }
619
+
620
+ // Separator for items section
621
+ if let separatorData = ("--------------------------------\n").data(using: .utf8) {
622
+ printData.append(separatorData)
623
+ }
624
+
625
+ // Items header (ITEM, QTY, AMT)
626
+ if let headerData = ("ITEM\t\t\tQTY\tAMT\n").data(using: .utf8) {
627
+ printData.append(headerData)
628
+ }
629
+ if let separatorData = ("--------------------------------\n").data(using: .utf8) {
630
+ printData.append(separatorData)
631
+ }
632
+
350
633
  // Items
351
634
  if let kitchen = template["kitchen"] as? [[String: Any]] {
352
635
  // Get item formatting
@@ -370,6 +653,7 @@ import Network
370
653
  for item in kitchen {
371
654
  var itemName = ""
372
655
  var itemPrice = ""
656
+ var quantity = "1"
373
657
 
374
658
  // Get name from menu object
375
659
  if let menu = item["menu"] as? [String: Any],
@@ -377,38 +661,103 @@ import Network
377
661
  itemName = name
378
662
  }
379
663
 
380
- // Add quantity
664
+ // Get quantity
381
665
  if let qty = item["quantity"] {
382
- itemName += " x\(getString(qty) ?? "1")"
666
+ quantity = "x\(getString(qty) ?? "1")"
383
667
  }
384
668
 
385
669
  // Format price
386
670
  if let price = item["total_price"] {
387
671
  if let priceDouble = Double(getString(price) ?? "0") {
388
- itemPrice = String(format: "$%.2f", priceDouble)
672
+ itemPrice = String(format: "%.1f", priceDouble)
389
673
  } else {
390
- itemPrice = "$" + (getString(price) ?? "0.00")
674
+ itemPrice = getString(price) ?? "0.00"
391
675
  }
392
676
  }
393
677
 
394
- // Print main item
395
- let line = String(format: "%@\t%@\n", itemName, itemPrice)
678
+ // Print main item line
679
+ let line = String(format: "%@ %@\t%@\n", itemName, quantity, itemPrice)
396
680
  if let lineData = line.data(using: .utf8) {
397
681
  printData.append(lineData)
398
682
  }
399
683
 
400
684
  // Print modifiers
401
685
  if let modifiers = item["modifiers"] as? [[String: Any]] {
686
+ // Get modifier formatting options
687
+ var modifierStyle = "standard" // default
688
+ var modifierSizeCode: UInt8 = 0x00
689
+ var modifierIndent = " " // default: 2 spaces
690
+
691
+ if let formatting = template["formatting"] as? [String: Any] {
692
+ if let style = getString(formatting["modifierStyle"]) {
693
+ modifierStyle = style
694
+ }
695
+ if let size = formatting["modifierSize"] {
696
+ modifierSizeCode = mapHeaderSizeToCode(size)
697
+ }
698
+ if let indent = getString(formatting["modifierIndent"]) {
699
+ switch indent.lowercased() {
700
+ case "small": modifierIndent = " "
701
+ case "medium": modifierIndent = " "
702
+ case "large": modifierIndent = " "
703
+ default: modifierIndent = indent
704
+ }
705
+ }
706
+ }
707
+
708
+ // Apply modifier size if specified
709
+ if modifierSizeCode != 0x00 {
710
+ printData.append(Data([0x1D, 0x21, modifierSizeCode]))
711
+ }
712
+
402
713
  for mod in modifiers {
403
714
  if let modName = getString(mod["name"]) {
404
- var modLine = " + \(modName)"
715
+ var modLine = ""
716
+ var prefix = ""
405
717
 
406
- // Only show price if > 0
407
- if let modPrice = mod["price"],
408
- let priceDouble = Double(getString(modPrice) ?? "0"),
409
- priceDouble > 0 {
410
- modLine += String(format: "\t$%.2f", priceDouble)
718
+ // Determine prefix based on style
719
+ switch modifierStyle.lowercased() {
720
+ case "minimal":
721
+ prefix = "-"
722
+ case "bullet":
723
+ prefix = "•"
724
+ case "arrow":
725
+ prefix = "→"
726
+ case "detailed":
727
+ prefix = "+"
728
+ default: // "standard"
729
+ prefix = "+"
411
730
  }
731
+
732
+ // Build modifier line based on style
733
+ switch modifierStyle.lowercased() {
734
+ case "minimal":
735
+ modLine = "\(modifierIndent)\(prefix) \(modName)"
736
+ case "bullet":
737
+ modLine = "\(modifierIndent)\(prefix) \(modName)"
738
+ case "arrow":
739
+ modLine = "\(modifierIndent)\(prefix) \(modName)"
740
+ case "detailed":
741
+ // Show quantity if available
742
+ var quantityStr = ""
743
+ if let qty = mod["quantity"],
744
+ let qtyInt = qty as? Int, qtyInt > 1 {
745
+ quantityStr = " (x\(qtyInt))"
746
+ }
747
+ modLine = "\(modifierIndent)\(prefix) \(modName)\(quantityStr)"
748
+ default: // "standard"
749
+ modLine = "\(modifierIndent)\(prefix) \(modName)"
750
+ }
751
+
752
+ // Add price if > 0 (except for minimal style)
753
+ if modifierStyle.lowercased() != "minimal" {
754
+ if let modPrice = mod["price"],
755
+ let priceDouble = Double(getString(modPrice) ?? "0"),
756
+ priceDouble > 0 {
757
+ modLine += String(format: "\t$%.2f", priceDouble)
758
+ }
759
+ }
760
+
412
761
  modLine += "\n"
413
762
 
414
763
  if let modData = modLine.data(using: .utf8) {
@@ -416,6 +765,11 @@ import Network
416
765
  }
417
766
  }
418
767
  }
768
+
769
+ // Reset modifier size if it was changed
770
+ if modifierSizeCode != 0x00 {
771
+ printData.append(Data([0x1D, 0x21, 0x00]))
772
+ }
419
773
  }
420
774
  }
421
775
 
@@ -466,20 +820,51 @@ import Network
466
820
  }
467
821
  }
468
822
 
469
- // Total
470
- if let total = getString(template["total"]),
471
- let totalData = ("\nTotal: " + total + "\n").data(using: .utf8) {
472
- // Get total formatting
823
+ // Separator
824
+ if let separatorData = ("--------------------------------\n").data(using: .utf8) {
825
+ printData.append(separatorData)
826
+ }
827
+
828
+ // Subtotal, Discount, GST
829
+ if let subtotal = getString(template["subtotal"]) {
830
+ if let data = ("SUBTOTAL\t\t\t$\(subtotal)\n").data(using: .utf8) {
831
+ printData.append(data)
832
+ }
833
+ }
834
+
835
+ if let discount = getString(template["discount"]) {
836
+ if let discountDouble = Double(discount), discountDouble > 0 {
837
+ if let data = ("SAFRA Members\t\t\t-$\(discount)\n").data(using: .utf8) {
838
+ printData.append(data)
839
+ }
840
+ }
841
+ }
842
+
843
+ if let gst = getString(template["gst"]) {
844
+ if let data = ("9% (Incl.) GST\t\t\t$\(gst)\n").data(using: .utf8) {
845
+ printData.append(data)
846
+ }
847
+ }
848
+
849
+ // ========== TOTAL SECTION (NEW STRUCTURE) ==========
850
+ if let total = getString(template["total"]) {
851
+ // Separator
852
+ if let separatorData = ("--------------------------------\n").data(using: .utf8) {
853
+ printData.append(separatorData)
854
+ }
855
+
856
+ // Get total formatting from total_config object
473
857
  var totalSizeCode: UInt8 = 0x00
474
858
  var totalBold = false
475
- if let formatting = template["formatting"] as? [String: Any] {
476
- if let totalSize = formatting["totalSize"] {
477
- totalSizeCode = mapHeaderSizeToCode(totalSize)
859
+
860
+ if let totalConfig = template["total_config"] as? [String: Any] {
861
+ if let size = totalConfig["size"] {
862
+ totalSizeCode = mapHeaderSizeToCode(size)
863
+ }
864
+ if let bold = totalConfig["bold"] as? Bool {
865
+ totalBold = bold
478
866
  }
479
- totalBold = getBool(formatting["totalBold"])
480
867
  }
481
- print("ZywellSDK: Total size code: \(totalSizeCode)")
482
- print("ZywellSDK: Total bold: \(totalBold)")
483
868
 
484
869
  // Apply total formatting
485
870
  if totalBold {
@@ -489,7 +874,9 @@ import Network
489
874
  printData.append(Data([0x1D, 0x21, totalSizeCode])) // Set size
490
875
  }
491
876
 
492
- printData.append(totalData)
877
+ if let totalData = ("TOTAL\t\t\t$\(total)\n").data(using: .utf8) {
878
+ printData.append(totalData)
879
+ }
493
880
 
494
881
  // Reset total formatting
495
882
  if totalSizeCode != 0x00 {
@@ -500,22 +887,53 @@ import Network
500
887
  }
501
888
  }
502
889
 
503
- // Footer
504
- if let footer = getString(template["footer"]),
505
- let footerData = (footer + "\n").data(using: .utf8) {
890
+ // Separator
891
+ if let separatorData = ("--------------------------------\n").data(using: .utf8) {
892
+ printData.append(separatorData)
893
+ }
894
+
895
+ // Payment method
896
+ if let paymentMethod = getString(template["paymentMethod"]),
897
+ let paymentData = ("PAYMENT BY:\(paymentMethod)\t\(getString(template["total"]) ?? "")\n").data(using: .utf8) {
898
+ printData.append(paymentData)
899
+ }
900
+
901
+ // Separator
902
+ if let separatorData = ("--------------------------------\n").data(using: .utf8) {
903
+ printData.append(separatorData)
904
+ }
905
+
906
+
907
+ // ========== FOOTER SECTION (NEW STRUCTURE) ==========
908
+ // Center align for footer
909
+ printData.append(Data([0x1B, 0x61, 0x01]))
910
+
911
+ if let footer = template["footer"] as? [String: Any] {
506
912
  // Get footer formatting
507
913
  var footerSizeCode: UInt8 = 0x00
508
- if let formatting = template["formatting"] as? [String: Any],
509
- let footerSize = formatting["footerSize"] {
510
- footerSizeCode = mapHeaderSizeToCode(footerSize)
914
+ var footerBold = false
915
+
916
+ if let size = footer["size"] {
917
+ footerSizeCode = mapHeaderSizeToCode(size)
918
+ }
919
+ if let bold = footer["bold"] as? Bool {
920
+ footerBold = bold
511
921
  }
512
922
 
513
923
  // Apply footer formatting
924
+ if footerBold {
925
+ printData.append(Data([0x1B, 0x45, 0x01])) // Bold on
926
+ }
514
927
  if footerSizeCode != 0x00 {
515
928
  printData.append(Data([0x1D, 0x21, footerSizeCode])) // Set size
516
929
  }
517
930
 
518
- printData.append(footerData)
931
+ // Footer message
932
+ if let message = getString(footer["message"]),
933
+ !message.isEmpty,
934
+ let messageData = (message + "\n").data(using: .utf8) {
935
+ printData.append(messageData)
936
+ }
519
937
 
520
938
  // Reset footer formatting
521
939
  if footerSizeCode != 0x00 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seven365-zyprinter",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "description": "Capacitor plugin for Zywell/Zyprint thermal printer integration with Bluetooth and WiFi support",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",