@padosoft/react-native-ecr17 0.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.
Files changed (107) hide show
  1. package/Ecr17.podspec +39 -0
  2. package/README.md +348 -0
  3. package/android/CMakeLists.txt +41 -0
  4. package/android/build.gradle +149 -0
  5. package/android/fix-prefab.gradle +51 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +9 -0
  9. package/android/src/main/java/com/margelo/nitro/ecr17/HybridEcr17Transport.kt +233 -0
  10. package/android/src/main/java/com/padosoft/ecr17/Ecr17Package.kt +30 -0
  11. package/cpp/Ecr17.cpp +1 -0
  12. package/cpp/Ecr17.hpp +2 -0
  13. package/cpp/Ecr17Client/HybridEcr17Client.cpp +598 -0
  14. package/cpp/Ecr17Client/HybridEcr17Client.hpp +85 -0
  15. package/cpp/Ecr17Protocol/Ecr17Protocol.cpp +277 -0
  16. package/cpp/Ecr17Protocol/Ecr17Protocol.hpp +103 -0
  17. package/cpp/Ecr17Response/Ecr17Response.cpp +155 -0
  18. package/cpp/Ecr17Response/Ecr17Response.hpp +113 -0
  19. package/cpp/Lcr/Lcr.cpp +42 -0
  20. package/cpp/Lcr/Lcr.hpp +22 -0
  21. package/cpp/PacketCodec/PacketCodec.cpp +146 -0
  22. package/cpp/PacketCodec/PacketCodec.hpp +48 -0
  23. package/cpp/Session/Ecr17Session.cpp +260 -0
  24. package/cpp/Session/Ecr17Session.hpp +97 -0
  25. package/cpp/Session/RetryPolicy.hpp +23 -0
  26. package/cpp/Transport/FakeTransport.hpp +95 -0
  27. package/cpp/Transport/NativeTransportAdapter.cpp +42 -0
  28. package/cpp/Transport/NativeTransportAdapter.hpp +32 -0
  29. package/cpp/Transport/Transport.hpp +31 -0
  30. package/cpp/tests/CMakeLists.txt +55 -0
  31. package/cpp/tests/PosixTcpTransport.hpp +105 -0
  32. package/cpp/tests/stubs/LrcMode.hpp +25 -0
  33. package/cpp/tests/test_flows.cpp +148 -0
  34. package/cpp/tests/test_integration_terminal.cpp +72 -0
  35. package/cpp/tests/test_lrc.cpp +66 -0
  36. package/cpp/tests/test_packet_codec.cpp +164 -0
  37. package/cpp/tests/test_protocol.cpp +102 -0
  38. package/cpp/tests/test_protocol_commands.cpp +190 -0
  39. package/cpp/tests/test_response.cpp +164 -0
  40. package/cpp/tests/test_retry_policy.cpp +28 -0
  41. package/cpp/tests/test_session.cpp +262 -0
  42. package/ios/Bridge.h +1 -0
  43. package/ios/HybridEcr17Transport.swift +103 -0
  44. package/nitro.json +30 -0
  45. package/nitrogen/generated/.gitattributes +1 -0
  46. package/nitrogen/generated/android/Ecr17+autolinking.cmake +82 -0
  47. package/nitrogen/generated/android/Ecr17+autolinking.gradle +27 -0
  48. package/nitrogen/generated/android/Ecr17OnLoad.cpp +68 -0
  49. package/nitrogen/generated/android/Ecr17OnLoad.hpp +34 -0
  50. package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  51. package/nitrogen/generated/android/c++/JFunc_void_std__shared_ptr_ArrayBuffer_.hpp +77 -0
  52. package/nitrogen/generated/android/c++/JHybridEcr17TransportSpec.cpp +93 -0
  53. package/nitrogen/generated/android/c++/JHybridEcr17TransportSpec.hpp +68 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Ecr17OnLoad.kt +35 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Func_void.kt +80 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Func_void_std__shared_ptr_ArrayBuffer_.kt +80 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/HybridEcr17TransportSpec.kt +86 -0
  58. package/nitrogen/generated/ios/Ecr17+autolinking.rb +62 -0
  59. package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Bridge.cpp +57 -0
  60. package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Bridge.hpp +154 -0
  61. package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Umbrella.hpp +47 -0
  62. package/nitrogen/generated/ios/Ecr17Autolinking.mm +43 -0
  63. package/nitrogen/generated/ios/Ecr17Autolinking.swift +26 -0
  64. package/nitrogen/generated/ios/c++/HybridEcr17TransportSpecSwift.cpp +11 -0
  65. package/nitrogen/generated/ios/c++/HybridEcr17TransportSpecSwift.hpp +119 -0
  66. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  67. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  68. package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
  69. package/nitrogen/generated/ios/swift/HybridEcr17TransportSpec.swift +60 -0
  70. package/nitrogen/generated/ios/swift/HybridEcr17TransportSpec_cxx.swift +211 -0
  71. package/nitrogen/generated/shared/c++/CardType.hpp +84 -0
  72. package/nitrogen/generated/shared/c++/CardVerificationRequest.hpp +97 -0
  73. package/nitrogen/generated/shared/c++/CardVerificationResult.hpp +136 -0
  74. package/nitrogen/generated/shared/c++/CloseSessionResult.hpp +106 -0
  75. package/nitrogen/generated/shared/c++/ConnectionState.hpp +80 -0
  76. package/nitrogen/generated/shared/c++/CurrencyExchange.hpp +100 -0
  77. package/nitrogen/generated/shared/c++/Ecr17Config.hpp +138 -0
  78. package/nitrogen/generated/shared/c++/HybridEcr17ClientSpec.cpp +42 -0
  79. package/nitrogen/generated/shared/c++/HybridEcr17ClientSpec.hpp +138 -0
  80. package/nitrogen/generated/shared/c++/HybridEcr17TransportSpec.cpp +26 -0
  81. package/nitrogen/generated/shared/c++/HybridEcr17TransportSpec.hpp +70 -0
  82. package/nitrogen/generated/shared/c++/IncrementalAuthRequest.hpp +96 -0
  83. package/nitrogen/generated/shared/c++/LrcMode.hpp +84 -0
  84. package/nitrogen/generated/shared/c++/PaymentCardType.hpp +84 -0
  85. package/nitrogen/generated/shared/c++/PaymentRequest.hpp +109 -0
  86. package/nitrogen/generated/shared/c++/PaymentResult.hpp +139 -0
  87. package/nitrogen/generated/shared/c++/PosStatusResponse.hpp +96 -0
  88. package/nitrogen/generated/shared/c++/PreAuthClosureRequest.hpp +96 -0
  89. package/nitrogen/generated/shared/c++/PreAuthRequest.hpp +109 -0
  90. package/nitrogen/generated/shared/c++/PreAuthResult.hpp +144 -0
  91. package/nitrogen/generated/shared/c++/ProgressEvent.hpp +83 -0
  92. package/nitrogen/generated/shared/c++/ReceiptLine.hpp +83 -0
  93. package/nitrogen/generated/shared/c++/ReversalRequest.hpp +88 -0
  94. package/nitrogen/generated/shared/c++/ReversalResult.hpp +132 -0
  95. package/nitrogen/generated/shared/c++/TokenizationRequest.hpp +89 -0
  96. package/nitrogen/generated/shared/c++/TokenizationService.hpp +76 -0
  97. package/nitrogen/generated/shared/c++/TotalsResult.hpp +93 -0
  98. package/nitrogen/generated/shared/c++/TransactionEntryMode.hpp +92 -0
  99. package/nitrogen/generated/shared/c++/TransactionOutcome.hpp +88 -0
  100. package/nitrogen/generated/shared/c++/VasResult.hpp +96 -0
  101. package/package.json +102 -0
  102. package/react-native.config.js +18 -0
  103. package/src/index.ts +4 -0
  104. package/src/specs/client.nitro.ts +102 -0
  105. package/src/specs/transport.nitro.ts +25 -0
  106. package/src/types/client.ts +196 -0
  107. package/src/utils/client.ts +10 -0
package/Ecr17.podspec ADDED
@@ -0,0 +1,39 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "Ecr17"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported, :visionos => 1.0 }
14
+ s.source = { :git => "https://github.com/padosoft/react-native-ecr17-protocol.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = [
17
+ # Implementation (Swift)
18
+ "ios/**/*.{swift}",
19
+ # Autolinking/Registration (Objective-C++)
20
+ "ios/**/*.{m,mm}",
21
+ # Implementation (C++ objects)
22
+ "cpp/**/*.{hpp,cpp}",
23
+ ]
24
+
25
+ s.exclude_files = [
26
+ "cpp/tests/**/*"
27
+ ]
28
+
29
+ s.pod_target_xcconfig = {
30
+ 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/cpp"'
31
+ }
32
+
33
+ load 'nitrogen/generated/ios/Ecr17+autolinking.rb'
34
+ add_nitrogen_files(s)
35
+
36
+ s.dependency 'React-jsi'
37
+ s.dependency 'React-callinvoker'
38
+ install_modules_dependencies(s)
39
+ end
package/README.md ADDED
@@ -0,0 +1,348 @@
1
+ <div align="center">
2
+
3
+ # 💳 @padosoft/react-native-ecr17
4
+
5
+ **A React Native / Nitro module for the Italian ECR17 payment protocol — drive Nexi Group POS terminals over LAN, straight from your cash-register app.**
6
+
7
+ **The most complete open-source ECR17 toolkit for React Native & native mobile (iOS/Android).**
8
+
9
+ [![C++ tests](https://github.com/padosoft/react-native-ecr17-protocol/actions/workflows/cpp-tests.yml/badge.svg?branch=main)](https://github.com/padosoft/react-native-ecr17-protocol/actions/workflows/cpp-tests.yml)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green?style=flat-square)](https://github.com/padosoft/react-native-ecr17-protocol/blob/main/LICENSE)
11
+ [![Built with Nitro](https://img.shields.io/badge/built%20with-Nitro-8B5CF6?style=flat-square)](https://nitro.margelo.com)
12
+ [![Platforms](https://img.shields.io/badge/platforms-iOS%20%7C%20Android-555?style=flat-square)](#requirements)
13
+
14
+ <img src="https://raw.githubusercontent.com/padosoft/react-native-ecr17-protocol/main/resources/banner.png" alt="@padosoft/react-native-ecr17 banner" width="100%" />
15
+
16
+ </div>
17
+
18
+ > 🐘 **Using PHP / Laravel?** There's a sibling port:
19
+ > **[padosoft/laravel-ecr17](https://github.com/padosoft/laravel-ecr17)** —
20
+ > the same ECR17 protocol as a Laravel package + debug console.
21
+
22
+ ---
23
+
24
+ ## 📚 Table of contents
25
+
26
+ - [What is ECR17?](#-what-is-ecr17)
27
+ - [Why this exists](#-why-this-exists)
28
+ - [Highlights](#-highlights)
29
+ - [Screenshots](#-screenshots)
30
+ - [Feature status](#-feature-status)
31
+ - [Requirements](#requirements)
32
+ - [Installation](#-installation)
33
+ - [Quick start](#-quick-start)
34
+ - [React hook example](#-react-hook-example)
35
+ - [Configuration](#%EF%B8%8F-configuration)
36
+ - [API reference](#-api-reference)
37
+ - [Events](#-events)
38
+ - [Protocol cheat-sheet](#-protocol-cheat-sheet)
39
+ - [Architecture](#%EF%B8%8F-architecture)
40
+ - [Testing](#-testing)
41
+ - [Vibe-coding batteries included](#-vibe-coding-batteries-included)
42
+ - [License](#-license)
43
+
44
+ ## 🧭 What is ECR17?
45
+
46
+ **ECR17** is the Italian standard protocol — supported by **Nexi Group** terminals —
47
+ that integrates an *Electronic Cash Register* (ECR) with an *EFT-POS* payment
48
+ terminal over a local LAN connection. The cash register sends a request
49
+ (payment, reversal, status…), the terminal talks to the acquiring host, and
50
+ replies synchronously.
51
+
52
+ This library speaks that protocol from React Native, with the protocol engine
53
+ written in C++ and bridged via [Nitro Modules](https://nitro.margelo.com).
54
+
55
+ > 📚 **Official protocol reference (public):**
56
+ > <https://developer.nexigroup.com/traditionalpos/en-EU/docs/> — the
57
+ > authoritative source. Field positions, message codes and `lrcMode` may vary by
58
+ > terminal/firmware; always check against the official docs.
59
+
60
+ ## 🎯 Why this exists
61
+
62
+ Integrating Italian POS terminals has long been needlessly painful. The ECR17
63
+ protocol is **not publicly documented** — the specifications are shared under NDA,
64
+ mostly with established point-of-sale software vendors — so everyone else
65
+ reverse-engineers it by trial and error across terminals and firmware versions.
66
+ (The classic trap that blocks almost everyone: the LRC is computed over a base of
67
+ `0x7F`, not `0x00` — handled here, and configurable per terminal.)
68
+
69
+ A few community efforts exist for server-side languages, but there was **nothing
70
+ for React Native or native mobile (iOS/Android)**. To our knowledge this is the
71
+ **most complete open-source ECR17 toolkit for React Native and native mobile**:
72
+ the full command set, response parsing, the ACK/NAK + retransmit orchestration,
73
+ configurable LRC modes, and payment-safety — all tested.
74
+
75
+ The goal is simple: **low-level, Android and iOS developers should no longer
76
+ struggle to talk to Italian POS terminals.** No NDA hunting, no guesswork — just
77
+ `await client.pay({ amountCents })`. These protocols should be this approachable
78
+ for everyone, and now, for mobile, they are.
79
+
80
+ > 🤝 Compatibility notes (lrcMode, field quirks per terminal/firmware) are
81
+ > welcome as issues, so we can build, together, the reference the ecosystem
82
+ > never had.
83
+
84
+ ## ✨ Highlights
85
+
86
+ - ⚡️ **C++ protocol core, Nitro-bridged** — framing/LRC/orchestration run natively on iOS & Android.
87
+ - 🔄 **Async, Promise-based API** — `await client.pay({ amountCents })`.
88
+ - 🧱 **Full command set** — payment, extended payment, reversal, pre-auth (request/incremental/closure), card verification, close session, totals, last result, ECR printing, reprint, VAS.
89
+ - 🛡️ **Robust by design** — fixed-width field validation, defensive response parsing, ACK/NAK handshake with **retransmit-up-to-3** and timeouts.
90
+ - 📡 **Live events** — progress messages, streamed receipt lines, connection state.
91
+ - 🧩 **Shared C++ ↔ native bridge** — one C++ protocol engine talks to the native
92
+ TCP socket (Kotlin/Swift) through Nitro's auto-generated **C++↔Kotlin JNI**
93
+ bridge — a notoriously fiddly piece on Android, here done cleanly with no
94
+ hand-written JNI.
95
+ - ✅ **Heavily tested** — 83 C++ unit/flow/safety tests (LRC, codec, every builder, every parser, full session orchestration) run in CI.
96
+ - 🤖 **Vibe-coding batteries included** — ships first-class AI-agent context
97
+ (`AGENTS.md`, `CLAUDE.md`, `docs/LESSON.md`, `PROGRESS.md`) so contributors
98
+ using AI assistants get accurate, instant project context. See [below](#-vibe-coding-batteries-included).
99
+
100
+ ## 📱 Screenshots
101
+
102
+ The repo ships an example **Debug Console** app (iOS & Android) that exercises every
103
+ ECR17 command against a real terminal and streams the behind-the-scenes log
104
+ (sent / progress / receipt / result / error) live.
105
+
106
+ <table>
107
+ <tr>
108
+ <td align="center" width="33%">
109
+ <img src="https://raw.githubusercontent.com/padosoft/react-native-ecr17-protocol/main/resources/screenshoots/demo-app-android.jpeg" alt="Debug Console on Android — commands & configuration" width="240" /><br/>
110
+ <sub>Android — commands &amp; configuration</sub>
111
+ </td>
112
+ <td align="center" width="33%">
113
+ <img src="https://raw.githubusercontent.com/padosoft/react-native-ecr17-protocol/main/resources/screenshoots/demo-app-iOS.jpeg" alt="Debug Console on iOS — commands & configuration" width="240" /><br/>
114
+ <sub>iOS — commands &amp; configuration</sub>
115
+ </td>
116
+ <td align="center" width="33%">
117
+ <img src="https://raw.githubusercontent.com/padosoft/react-native-ecr17-protocol/main/resources/screenshoots/demo-app-iOS-logs.jpeg" alt="Debug Console on iOS — live logs tab" width="240" /><br/>
118
+ <sub>iOS — live logs tab</sub>
119
+ </td>
120
+ </tr>
121
+ </table>
122
+
123
+ ## 🛡️ Enterprise robustness & payment safety
124
+
125
+ This module handles real money, so correctness and failure handling are
126
+ first-class:
127
+
128
+ - **Physical handshake** — every application frame is confirmed with ACK/NAK and
129
+ **retransmitted up to 3 times** (per spec) on NAK or timeout, with separate
130
+ ACK and response timeouts.
131
+ - **Integrity** — LRC validated on every received frame; invalid frames are
132
+ NAKed to request retransmission. Outgoing fixed-width fields are validated, so
133
+ a malformed frame is never sent to the terminal.
134
+ - **No double charge** — on a connection drop, `autoReconnect` restores the
135
+ socket but a **financial command is never blindly re-sent** (a re-send could
136
+ charge the cardholder twice). Read-only/idempotent commands (status, totals,
137
+ `sendLastResult`, enable-printing) are retried; payments/reversals/pre-auths
138
+ reconnect and surface the error so you recover the outcome via
139
+ `sendLastResult()` (the spec's `G` command). This invariant is unit-tested.
140
+ - **Defensive parsing** — response parsers never read out of bounds on short or
141
+ malformed payloads.
142
+ - **One transaction at a time** — matches the protocol's request/response model.
143
+ - **Tested** — 83 C++ unit/flow/safety tests in CI, plus an opt-in real-terminal
144
+ integration test.
145
+
146
+ ## 📊 Feature status
147
+
148
+ | Area | Status |
149
+ |------|:------:|
150
+ | Packet framing + LRC (4 modes) | ✅ |
151
+ | All request builders (`P X p i c H U C T G E R K s S`) | ✅ |
152
+ | Response parsing (`E/V/s/T/C/e/K`, incl. DCC) | ✅ |
153
+ | Session orchestration (ACK/NAK, retransmit, timeout, progress/receipt) | ✅ |
154
+ | Async client API + events | ✅ |
155
+ | Auto-connect, tokenization (`U`) flow, receipt streaming | ✅ |
156
+ | Android native transport (Kotlin TCP) | ✅ *(CI-built)* |
157
+ | iOS native transport (Swift / Network.framework) | ✅ *(verified on device)* |
158
+
159
+ ## Requirements
160
+
161
+ - **React Native** 0.76+ (new architecture) — the example uses Expo SDK 56 / RN 0.85
162
+ - **react-native-nitro-modules** (peer dependency)
163
+ - A Nexi Group ECR17-compatible terminal configured for **LAN integration**
164
+
165
+ ## 📦 Installation
166
+
167
+ ```bash
168
+ bun add @padosoft/react-native-ecr17 react-native-nitro-modules
169
+ # or: npm install react-native-ecr17 react-native-nitro-modules
170
+ cd ios && pod install # iOS
171
+ ```
172
+
173
+ > Nitro module: requires the RN **new architecture** (default on 0.76+).
174
+
175
+ ## 🚀 Quick start
176
+
177
+ ```ts
178
+ import { createEcr17Client } from '@padosoft/react-native-ecr17';
179
+
180
+ const client = createEcr17Client({
181
+ host: '192.168.1.50', // terminal IP on the LAN
182
+ port: 10000, // configured ECR port
183
+ terminalId: '12345678',
184
+ cashRegisterId: '00000001',
185
+ lrcMode: 'std',
186
+ responseTimeoutMs: 60000,
187
+ });
188
+
189
+ await client.connect();
190
+
191
+ const result = await client.pay({ amountCents: 650 });
192
+ if (result.outcome === 'ok') {
193
+ console.log('Approved', result.authCode, 'PAN', result.pan);
194
+ } else {
195
+ console.warn('Declined:', result.errorDescription);
196
+ }
197
+
198
+ // Reversal ("annullamento") of the last transaction:
199
+ await client.reverse({});
200
+
201
+ const status = await client.status(); // PosStatusResponse
202
+ await client.disconnect();
203
+ ```
204
+
205
+ ## ⚛️ React hook example
206
+
207
+ ```tsx
208
+ import { useEffect, useMemo, useState } from 'react';
209
+ import { createEcr17Client, type Ecr17Config, type ProgressEvent } from '@padosoft/react-native-ecr17';
210
+
211
+ export function useEcr17(config: Ecr17Config) {
212
+ const client = useMemo(() => createEcr17Client(config), [config]);
213
+ const [progress, setProgress] = useState<string>('');
214
+
215
+ useEffect(() => {
216
+ client.setOnProgress((e: ProgressEvent) => setProgress(e.message));
217
+ client.connect();
218
+ return () => client.disconnect();
219
+ }, [client]);
220
+
221
+ return {
222
+ progress,
223
+ pay: (amountCents: number) => client.pay({ amountCents }),
224
+ reverse: () => client.reverse({}),
225
+ status: () => client.status(),
226
+ };
227
+ }
228
+ ```
229
+
230
+ ## ⚙️ Configuration
231
+
232
+ `Ecr17Config`: `host` (required), `port?`, `terminalId` (required), `cashRegisterId`
233
+ (required), `lrcMode?`, `keepAlive?`, `autoReconnect?`, `connectionTimeoutMs?`,
234
+ `responseTimeoutMs?`, `ackTimeoutMs?`, `retryCount?`, `retryDelayMs?`, `debug?`.
235
+
236
+ ## 📖 API reference
237
+
238
+ All commands are **async** (`Promise`) and perform a full request/response
239
+ exchange. `configure`/`configuration` are synchronous.
240
+
241
+ | Method | Command | Returns |
242
+ |--------|:------:|---------|
243
+ | `connect()` / `disconnect()` / `isConnected()` | — | `Promise<void>` / `void` / `bool` |
244
+ | `status()` | `s` | `PosStatusResponse` |
245
+ | `pay(req)` / `payExtended(req)` | `P` / `X` | `PaymentResult` |
246
+ | `reverse(req)` | `S` | `ReversalResult` |
247
+ | `preAuth(req)` / `incrementalAuth(req)` / `preAuthClosure(req)` | `p` / `i` / `c` | `PreAuthResult` / `PaymentResult` |
248
+ | `verifyCard(req)` | `H` | `CardVerificationResult` |
249
+ | `closeSession()` / `totals()` | `C` / `T` | `CloseSessionResult` / `TotalsResult` |
250
+ | `sendLastResult()` | `G` | `PaymentResult` |
251
+ | `enableEcrPrinting(bool)` / `reprint(bool)` | `E` / `R` | `Promise<void>` |
252
+ | `vas(xml)` | `K` | `VasResult` |
253
+
254
+ Commands require an open connection (`connect()` first) and reject on
255
+ timeout / retransmission exhaustion / disconnect.
256
+
257
+ ## 📡 Events
258
+
259
+ ```ts
260
+ client.setOnProgress((e) => {/* e.message — display text during a procedure */});
261
+ client.setOnReceiptLine((l) => {/* l.text — a receipt line when ECR printing is on */});
262
+ client.setOnConnectionStateChange((s) => {/* 'disconnected' | 'connecting' | 'connected' */});
263
+ ```
264
+
265
+ ## 🔐 Protocol cheat-sheet
266
+
267
+ App frame: `STX(0x02)` · payload · `ETX(0x03)` · `LRC`. Progress: `SOH(0x01)` ·
268
+ 20 chars · `EOT(0x04)`. Confirmation: `ACK(0x06)` / `NAK(0x15)` · `ETX` · `LRC`.
269
+ LRC = `0x7F` XOR-folded; framing bytes folded in are selectable via `lrcMode`
270
+ (`stx` / `std` / `noext` / `stx_noext`).
271
+
272
+ ## 🏗️ Architecture
273
+
274
+ ```
275
+ package/cpp/
276
+ ├── Lcr/ # LRC (4 modes, base 0x7F)
277
+ ├── PacketCodec/ # framing: STX·ETX·SOH·EOT·ACK·NAK + LRC
278
+ ├── Ecr17Protocol/ # request builders (all commands), fixed-width + validated
279
+ ├── Ecr17Response/ # response field parsers -> plain structs
280
+ ├── Session/ # ACK/NAK + retransmit + timeout orchestration
281
+ ├── Transport/ # abstract Transport + NativeTransportAdapter + FakeTransport (tests)
282
+ └── Ecr17Client/ # HybridEcr17Client (Nitro async API)
283
+ package/android/.../HybridEcr17Transport.kt # Kotlin TCP transport
284
+ package/ios/HybridEcr17Transport.swift # Swift (Network.framework) transport
285
+ ```
286
+
287
+ ## 🧪 Testing
288
+
289
+ ```bash
290
+ cmake -S package/cpp/tests -B build -DCMAKE_BUILD_TYPE=Release
291
+ cmake --build build && ctest --test-dir build --output-on-failure
292
+ ```
293
+
294
+ 83 tests cover LRC, packet (de)framing edge cases, every builder's byte layout,
295
+ every response parser, and the documented payment / reversal / re-pay / progress
296
+ / receipt / NAK-retransmit / timeout flows (against an in-memory `FakeTransport`).
297
+
298
+ ## 🧾 Tokenization & receipts
299
+
300
+ ```ts
301
+ // Tokenization: attach a contract to a payment/preAuth/verifyCard. The 'U'
302
+ // additional-data message is sent automatically (P -> ACK -> U -> ACK -> result).
303
+ await client.pay({
304
+ amountCents: 1000,
305
+ tokenization: { service: 'recurring', contractCode: '1666354841608' },
306
+ });
307
+
308
+ // Receipts printed by the ECR: enable printing, set receiptDrainMs in the config,
309
+ // and receive lines via the event.
310
+ await client.enableEcrPrinting(true);
311
+ client.setOnReceiptLine((l) => appendToReceipt(l.text));
312
+ ```
313
+
314
+ ## 🔌 Testing against a real terminal (opt-in)
315
+
316
+ An opt-in C++ integration test runs the full core over a real TCP socket. It is
317
+ **skipped** unless `ECR17_TERMINAL_HOST` is set:
318
+
319
+ ```bash
320
+ cmake -S package/cpp/tests -B build && cmake --build build
321
+ ECR17_TERMINAL_HOST=192.168.1.50 ECR17_TERMINAL_PORT=10000 \
322
+ ECR17_TERMINAL_ID=00000000 ECR17_LRC_MODE=std \
323
+ ctest --test-dir build -R Integration --output-on-failure
324
+ ```
325
+
326
+ ## 🤖 Vibe-coding batteries included
327
+
328
+ Building on an undocumented payment protocol is exactly where AI assistants get
329
+ things subtly wrong. This repo ships the context to prevent that, so an agent (or
330
+ a new contributor) is productive and *safe* from minute one:
331
+
332
+ - **[`AGENTS.md`](https://github.com/padosoft/react-native-ecr17-protocol/blob/main/AGENTS.md)** /
333
+ **[`CLAUDE.md`](https://github.com/padosoft/react-native-ecr17-protocol/blob/main/CLAUDE.md)** — project guide, the mandatory
334
+ per-phase workflow, CI strategy, and the **money-critical** rules (e.g. never
335
+ blindly retry a payment).
336
+ - **`docs/LESSON.md`** — accumulated, verified engineering lessons (Nitro APIs,
337
+ C++↔Kotlin JNI, build traps, payment-safety) — the gotchas already solved.
338
+ - **`PROGRESS.md`** — crash-safe resume state across sessions.
339
+
340
+ The result: less hallucination, fewer footguns, and changes that respect the
341
+ payment-safety invariants by default.
342
+
343
+ ## 📄 License
344
+
345
+ [MIT](https://github.com/padosoft/react-native-ecr17-protocol/blob/main/LICENSE) © [padosoft](https://github.com/padosoft)
346
+
347
+ > **Disclaimer:** independent integration library. "ECR17", "Nexi" and related
348
+ > marks belong to their respective owners and are referenced for interoperability only.
@@ -0,0 +1,41 @@
1
+ project(Ecr17)
2
+ cmake_minimum_required(VERSION 3.9.0)
3
+
4
+ set (PACKAGE_NAME Ecr17)
5
+ set (CMAKE_VERBOSE_MAKEFILE ON)
6
+ set (CMAKE_CXX_STANDARD 20)
7
+
8
+ # Enable Raw Props parsing in react-native (for Nitro Views)
9
+ add_compile_options(-DRN_SERIALIZABLE_STATE=1)
10
+
11
+ # Define C++ library and add all sources
12
+ add_library(${PACKAGE_NAME} SHARED
13
+ src/main/cpp/cpp-adapter.cpp
14
+ ../cpp/Ecr17.cpp
15
+ ../cpp/Ecr17Client/HybridEcr17Client.cpp
16
+ ../cpp/Ecr17Protocol/Ecr17Protocol.cpp
17
+ ../cpp/Ecr17Response/Ecr17Response.cpp
18
+ ../cpp/Lcr/Lcr.cpp
19
+ ../cpp/PacketCodec/PacketCodec.cpp
20
+ ../cpp/Session/Ecr17Session.cpp
21
+ ../cpp/Transport/NativeTransportAdapter.cpp
22
+ )
23
+
24
+ # Add Nitrogen specs :)
25
+ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/Ecr17+autolinking.cmake)
26
+
27
+ # Set up local includes
28
+ include_directories(
29
+ "src/main/cpp"
30
+ "../cpp"
31
+ "../cpp/Ecr17Client" # generated Ecr17OnLoad.cpp does a flat #include "HybridEcr17Client.hpp"
32
+ )
33
+
34
+ find_library(LOG_LIB log)
35
+
36
+ # Link all libraries together
37
+ target_link_libraries(
38
+ ${PACKAGE_NAME}
39
+ ${LOG_LIB}
40
+ android # <-- Android core
41
+ )
@@ -0,0 +1,149 @@
1
+ buildscript {
2
+ repositories {
3
+ google()
4
+ mavenCentral()
5
+ }
6
+
7
+ dependencies {
8
+ classpath "com.android.tools.build:gradle:9.2.1"
9
+ }
10
+ }
11
+
12
+ def reactNativeArchitectures() {
13
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
14
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
15
+ }
16
+
17
+ def isNewArchitectureEnabled() {
18
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
19
+ }
20
+
21
+ apply plugin: "com.android.library"
22
+ apply plugin: 'org.jetbrains.kotlin.android'
23
+ apply from: '../nitrogen/generated/android/Ecr17+autolinking.gradle'
24
+ apply from: "./fix-prefab.gradle"
25
+
26
+ if (isNewArchitectureEnabled()) {
27
+ apply plugin: "com.facebook.react"
28
+ }
29
+
30
+ def getExtOrDefault(name) {
31
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["Ecr17_" + name]
32
+ }
33
+
34
+ def getExtOrIntegerDefault(name) {
35
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["Ecr17_" + name]).toInteger()
36
+ }
37
+
38
+ android {
39
+ namespace "com.padosoft.ecr17"
40
+
41
+ ndkVersion getExtOrDefault("ndkVersion")
42
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
43
+
44
+ defaultConfig {
45
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
46
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
47
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
48
+
49
+ externalNativeBuild {
50
+ cmake {
51
+ cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
52
+ arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
53
+ abiFilters (*reactNativeArchitectures())
54
+
55
+ buildTypes {
56
+ debug {
57
+ cppFlags "-O1 -g"
58
+ }
59
+ release {
60
+ cppFlags "-O2"
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ externalNativeBuild {
68
+ cmake {
69
+ path "CMakeLists.txt"
70
+ }
71
+ }
72
+
73
+ packagingOptions {
74
+ excludes = [
75
+ "META-INF",
76
+ "META-INF/**",
77
+ "**/libc++_shared.so",
78
+ "**/libNitroModules.so",
79
+ "**/libfbjni.so",
80
+ "**/libjsi.so",
81
+ "**/libfolly_json.so",
82
+ "**/libfolly_runtime.so",
83
+ "**/libglog.so",
84
+ "**/libhermes.so",
85
+ "**/libhermes-executor-debug.so",
86
+ "**/libhermes_executor.so",
87
+ "**/libreactnative.so",
88
+ "**/libreactnativejni.so",
89
+ "**/libturbomodulejsijni.so",
90
+ "**/libreact_nativemodule_core.so",
91
+ "**/libjscexecutor.so"
92
+ ]
93
+ }
94
+
95
+ buildFeatures {
96
+ buildConfig true
97
+ prefab true
98
+ }
99
+
100
+ buildTypes {
101
+ release {
102
+ minifyEnabled false
103
+ }
104
+ }
105
+
106
+ lintOptions {
107
+ disable "GradleCompatible"
108
+ }
109
+
110
+ compileOptions {
111
+ sourceCompatibility JavaVersion.VERSION_1_8
112
+ targetCompatibility JavaVersion.VERSION_1_8
113
+ }
114
+
115
+ sourceSets {
116
+ main {
117
+ if (isNewArchitectureEnabled()) {
118
+ java.srcDirs += [
119
+ // React Codegen files
120
+ "${project.buildDir}/generated/source/codegen/java"
121
+ ]
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ repositories {
128
+ mavenCentral()
129
+ google()
130
+ }
131
+
132
+
133
+ dependencies {
134
+ // For < 0.71, this will be from the local maven repo
135
+ // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
136
+ //noinspection GradleDynamicVersion
137
+ implementation "com.facebook.react:react-native:+"
138
+
139
+ // Add a dependency on NitroModules
140
+ implementation project(":react-native-nitro-modules")
141
+ }
142
+
143
+ if (isNewArchitectureEnabled()) {
144
+ react {
145
+ jsRootDir = file("../src/")
146
+ libraryName = "Ecr17"
147
+ codegenJavaPackageName = "com.padosoft.ecr17"
148
+ }
149
+ }
@@ -0,0 +1,51 @@
1
+ tasks.configureEach { task ->
2
+ // Make sure that we generate our prefab publication file only after having built the native library
3
+ // so that not a header publication file, but a full configuration publication will be generated, which
4
+ // will include the .so file
5
+
6
+ def prefabConfigurePattern = ~/^prefab(.+)ConfigurePackage$/
7
+ def matcher = task.name =~ prefabConfigurePattern
8
+ if (matcher.matches()) {
9
+ def variantName = matcher[0][1]
10
+ task.outputs.upToDateWhen { false }
11
+ task.dependsOn("externalNativeBuild${variantName}")
12
+ }
13
+ }
14
+
15
+ afterEvaluate {
16
+ def abis = reactNativeArchitectures()
17
+ rootProject.allprojects.each { proj ->
18
+ if (proj === rootProject) return
19
+
20
+ def dependsOnThisLib = proj.configurations.findAll { it.canBeResolved }.any { config ->
21
+ config.dependencies.any { dep ->
22
+ dep.group == project.group && dep.name == project.name
23
+ }
24
+ }
25
+ if (!dependsOnThisLib && proj != project) return
26
+
27
+ if (!proj.plugins.hasPlugin('com.android.application') && !proj.plugins.hasPlugin('com.android.library')) {
28
+ return
29
+ }
30
+
31
+ def variants = proj.android.hasProperty('applicationVariants') ? proj.android.applicationVariants : proj.android.libraryVariants
32
+ // Touch the prefab_config.json files to ensure that in ExternalNativeJsonGenerator.kt we will re-trigger the prefab CLI to
33
+ // generate a libnameConfig.cmake file that will contain our native library (.so).
34
+ // See this condition: https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/ExternalNativeJsonGenerator.kt;l=207-219?q=createPrefabBuildSystemGlue
35
+ variants.all { variant ->
36
+ def variantName = variant.name
37
+ abis.each { abi ->
38
+ def searchDir = new File(proj.projectDir, ".cxx/${variantName}")
39
+ if (!searchDir.exists()) return
40
+ def matches = []
41
+ searchDir.eachDir { randomDir ->
42
+ def prefabFile = new File(randomDir, "${abi}/prefab_config.json")
43
+ if (prefabFile.exists()) matches << prefabFile
44
+ }
45
+ matches.each { prefabConfig ->
46
+ prefabConfig.setLastModified(System.currentTimeMillis())
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,5 @@
1
+ Ecr17_kotlinVersion=2.1.21
2
+ Ecr17_minSdkVersion=23
3
+ Ecr17_targetSdkVersion=36
4
+ Ecr17_compileSdkVersion=36
5
+ Ecr17_ndkVersion=29.0.14206865
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,9 @@
1
+ #include <jni.h>
2
+ #include <fbjni/fbjni.h>
3
+ #include "Ecr17OnLoad.hpp"
4
+
5
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
6
+ return facebook::jni::initialize(vm, []() {
7
+ margelo::nitro::ecr17::registerAllNatives();
8
+ });
9
+ }