@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
@@ -0,0 +1,233 @@
1
+ package com.margelo.nitro.ecr17
2
+
3
+ import com.margelo.nitro.core.ArrayBuffer
4
+ import com.margelo.nitro.core.Promise
5
+ import java.io.IOException
6
+ import java.io.PushbackInputStream
7
+ import java.net.InetSocketAddress
8
+ import java.net.Socket
9
+ import java.net.SocketTimeoutException
10
+ import java.util.concurrent.atomic.AtomicBoolean
11
+ import kotlin.concurrent.thread
12
+
13
+ /**
14
+ * Android (Kotlin) implementation of the ECR17 LAN transport. Plain TCP socket
15
+ * with a background reader thread that forwards received bytes to the C++ core
16
+ * via the [onDataCallback]. Nitro generates the C++<->Kotlin JNI bridge for this
17
+ * HybridObject, so no manual JNI is needed.
18
+ */
19
+ class HybridEcr17Transport : HybridEcr17TransportSpec() {
20
+ private var socket: Socket? = null
21
+ private var readerThread: Thread? = null
22
+
23
+ // The reader and the liveness probe ([isConnected]) share one input stream;
24
+ // PushbackInputStream lets the probe peek a byte (looking only for EOF) and push
25
+ // it back so it is never consumed from the protocol stream.
26
+ private var input: PushbackInputStream? = null
27
+
28
+ @Volatile private var running = false
29
+
30
+ // True while a caller-initiated disconnect is in progress, so the reader's
31
+ // finally block does not emit a spurious onDisconnect for an intentional close.
32
+ @Volatile private var intentionalDisconnect = false
33
+
34
+ // Single-shot guard so a given drop fires onDisconnect exactly once, no matter
35
+ // whether the reader thread or the liveness probe observes it first.
36
+ private val disconnectEmitted = AtomicBoolean(false)
37
+
38
+ // Serializes access to the shared input stream between the reader thread and the
39
+ // liveness probe so the two never `read()` concurrently.
40
+ private val ioLock = Any()
41
+
42
+ private var onDataCallback: ((ArrayBuffer) -> Unit)? = null
43
+ private var onDisconnectCallback: (() -> Unit)? = null
44
+
45
+ override fun connect(host: String, port: Double, timeoutMs: Double): Promise<Unit> {
46
+ return Promise.parallel {
47
+ // Tear down any previous connection (and join its reader) before reconnecting.
48
+ closeCurrent()
49
+ intentionalDisconnect = false
50
+ disconnectEmitted.set(false)
51
+
52
+ val s = Socket()
53
+ s.tcpNoDelay = true
54
+ s.connect(InetSocketAddress(host, port.toInt()), timeoutMs.toInt())
55
+ // A short read timeout lets the reader loop release `ioLock` periodically so
56
+ // the liveness probe ([isConnected]) can run between reads; a timeout is not
57
+ // an error, just "no data yet".
58
+ s.soTimeout = READ_TIMEOUT_MS
59
+ socket = s
60
+ input = PushbackInputStream(s.getInputStream(), 1)
61
+ running = true
62
+ startReader()
63
+ Unit
64
+ }
65
+ }
66
+
67
+ private fun startReader() {
68
+ readerThread =
69
+ thread(name = "ecr17-reader", isDaemon = true) {
70
+ val buffer = ByteArray(4096)
71
+ try {
72
+ while (running) {
73
+ val read =
74
+ synchronized(ioLock) {
75
+ if (!running) return@synchronized SENTINEL_STOP
76
+ try {
77
+ input?.read(buffer) ?: -1 // null stream -> treat as EOF
78
+ } catch (_: SocketTimeoutException) {
79
+ SENTINEL_TIMEOUT // no data within READ_TIMEOUT_MS — keep looping
80
+ }
81
+ }
82
+ when {
83
+ read == SENTINEL_STOP -> break
84
+ read == SENTINEL_TIMEOUT -> continue
85
+ read < 0 -> break // EOF: peer closed the connection
86
+ read > 0 ->
87
+ onDataCallback?.invoke(ArrayBuffer.copy(buffer.copyOfRange(0, read)))
88
+ }
89
+ }
90
+ } catch (_: IOException) {
91
+ // Expected on socket close / read error. Other throwables (Error, unexpected
92
+ // RuntimeException) intentionally propagate so they're visible in dev; the
93
+ // finally still fires the drop notification before the thread unwinds.
94
+ } finally {
95
+ // Promptly close the socket so `isConnected()`'s `isClosed` check is an
96
+ // immediate, reliable signal of the drop (no reliance on a write probe).
97
+ markDropped()
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Non-destructive liveness probe used to detect a peer-closed / half-open socket
104
+ * BEFORE a command is sent. ECR17/Nexi terminals routinely close the TCP socket
105
+ * between transactions; `Socket.isConnected` stays `true` for such a half-open
106
+ * socket, and the reader thread may not have observed the EOF yet — so without a
107
+ * probe a command could be sent on a dead socket, fail MID-exchange, and (because
108
+ * the money-safety RetryPolicy correctly refuses to replay a financial command)
109
+ * surface a FALSE "transport disconnected" error.
110
+ *
111
+ * The probe peeks ONE byte with a tiny read timeout and immediately pushes it
112
+ * back, so it NEVER consumes a protocol byte and NEVER writes anything to the
113
+ * peer (unlike `sendUrgentData`, which can place an inline 0xFF before the next
114
+ * STX frame on terminals with SO_OOBINLINE and corrupt a financial command). A
115
+ * returned `-1` means the peer closed; a read timeout means the (idle) socket is
116
+ * alive. It runs under `ioLock` so it never races the reader's `read()`.
117
+ *
118
+ * Money-safety is unchanged: this only removes the FALSE drop from a stale
119
+ * pre-send socket; a genuine mid-exchange drop still surfaces and is recovered
120
+ * via sendLastResult ('G').
121
+ */
122
+ override fun isConnected(): Boolean {
123
+ val s = socket
124
+ val pin = input
125
+ if (!running || s == null || pin == null || !s.isConnected || s.isClosed) {
126
+ return false
127
+ }
128
+ return synchronized(ioLock) {
129
+ if (!running || s.isClosed) return@synchronized false
130
+ // Use a tiny probe timeout instead of the reader's READ_TIMEOUT_MS so a healthy
131
+ // IDLE socket (the normal between-transactions case) returns "alive" almost
132
+ // immediately rather than blocking every pre-send check for the full read
133
+ // timeout. Safe to retune soTimeout here: we hold ioLock, so the reader thread
134
+ // is not mid-read; we restore READ_TIMEOUT_MS before releasing the lock.
135
+ try {
136
+ s.soTimeout = PROBE_TIMEOUT_MS
137
+ val b = pin.read() // EOF (-1) if peer closed; SocketTimeoutException if idle+alive
138
+ if (b < 0) {
139
+ markDropped()
140
+ false
141
+ } else {
142
+ pin.unread(b) // push the (unexpected) byte back — never consumed/dropped
143
+ true
144
+ }
145
+ } catch (_: SocketTimeoutException) {
146
+ true // idle but alive (no FIN received within the probe timeout)
147
+ } catch (_: IOException) {
148
+ markDropped() // genuine I/O error: treat as dropped (other throwables propagate)
149
+ false
150
+ } finally {
151
+ try {
152
+ s.soTimeout = READ_TIMEOUT_MS // restore the reader loop's timeout
153
+ } catch (_: IOException) {
154
+ // socket already closed by markDropped(); nothing to restore
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ /** Closes the current socket and joins its reader thread (intentional close). */
161
+ private fun closeCurrent() {
162
+ intentionalDisconnect = true
163
+ running = false
164
+ try {
165
+ socket?.close()
166
+ } catch (_: Throwable) {
167
+ // ignore
168
+ }
169
+ socket = null
170
+ input = null
171
+ readerThread?.let { t ->
172
+ try {
173
+ t.join(1000)
174
+ } catch (_: Throwable) {
175
+ // ignore
176
+ }
177
+ }
178
+ readerThread = null
179
+ }
180
+
181
+ /**
182
+ * Marks the connection as dropped, closes the socket so the drop is synchronously
183
+ * observable, and fires onDisconnect exactly once. Safe to call from both the
184
+ * reader thread and the liveness probe (single-shot via [disconnectEmitted]).
185
+ */
186
+ private fun markDropped() {
187
+ running = false
188
+ try {
189
+ socket?.close()
190
+ } catch (_: Throwable) {
191
+ // ignore
192
+ }
193
+ // Only signal disconnect for unexpected drops, not caller-initiated closes, and
194
+ // only once per drop.
195
+ if (!intentionalDisconnect && disconnectEmitted.compareAndSet(false, true)) {
196
+ onDisconnectCallback?.invoke()
197
+ }
198
+ }
199
+
200
+ override fun disconnect() {
201
+ closeCurrent()
202
+ }
203
+
204
+ override fun send(bytes: ArrayBuffer) {
205
+ val s = socket ?: throw IllegalStateException("ECR17 transport is not connected")
206
+ val out = s.getOutputStream()
207
+ out.write(bytes.toByteArray())
208
+ out.flush()
209
+ }
210
+
211
+ override fun setOnData(callback: (bytes: ArrayBuffer) -> Unit) {
212
+ onDataCallback = callback
213
+ }
214
+
215
+ override fun setOnDisconnect(callback: () -> Unit) {
216
+ onDisconnectCallback = callback
217
+ }
218
+
219
+ private companion object {
220
+ // Reader-loop read timeout: short enough that the liveness probe never waits
221
+ // long for `ioLock`, long enough to avoid busy-spinning.
222
+ private const val READ_TIMEOUT_MS = 100
223
+
224
+ // Probe read timeout used by isConnected(): tiny so a healthy idle socket reports
225
+ // "alive" near-instantly instead of paying READ_TIMEOUT_MS on every pre-send check.
226
+ private const val PROBE_TIMEOUT_MS = 1
227
+
228
+ // Sentinels returned by the synchronized read block; kept below the real EOF
229
+ // value (-1) so `read < 0` still catches a genuine EOF after these are handled.
230
+ private const val SENTINEL_TIMEOUT = -2
231
+ private const val SENTINEL_STOP = -3
232
+ }
233
+ }
@@ -0,0 +1,30 @@
1
+ package com.padosoft.ecr17
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+ import com.margelo.nitro.ecr17.Ecr17OnLoad
8
+
9
+ /**
10
+ * Autolinked ReactPackage for the ECR17 Nitro module.
11
+ *
12
+ * It exposes no TurboModules (Nitro registers its HybridObjects via the C++
13
+ * registry, not the RN module system). Its sole job is to load the native
14
+ * "Ecr17" C++ library when the package is constructed by React Native's
15
+ * autolinking, so the HybridObjects are registered before JS creates them.
16
+ */
17
+ class Ecr17Package : BaseReactPackage() {
18
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
19
+
20
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
21
+ ReactModuleInfoProvider { emptyMap() }
22
+
23
+ companion object {
24
+ init {
25
+ // Idempotent: loads libEcr17.so (System.loadLibrary), which runs
26
+ // JNI_OnLoad -> registerAllNatives() and registers the HybridObjects.
27
+ Ecr17OnLoad.initializeNative()
28
+ }
29
+ }
30
+ }
package/cpp/Ecr17.cpp ADDED
@@ -0,0 +1 @@
1
+ #include "Ecr17.hpp"
package/cpp/Ecr17.hpp ADDED
@@ -0,0 +1,2 @@
1
+ #pragma once
2
+ #include "Ecr17Client/HybridEcr17Client.hpp"