@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.
- package/Ecr17.podspec +39 -0
- package/README.md +348 -0
- package/android/CMakeLists.txt +41 -0
- package/android/build.gradle +149 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +9 -0
- package/android/src/main/java/com/margelo/nitro/ecr17/HybridEcr17Transport.kt +233 -0
- package/android/src/main/java/com/padosoft/ecr17/Ecr17Package.kt +30 -0
- package/cpp/Ecr17.cpp +1 -0
- package/cpp/Ecr17.hpp +2 -0
- package/cpp/Ecr17Client/HybridEcr17Client.cpp +598 -0
- package/cpp/Ecr17Client/HybridEcr17Client.hpp +85 -0
- package/cpp/Ecr17Protocol/Ecr17Protocol.cpp +277 -0
- package/cpp/Ecr17Protocol/Ecr17Protocol.hpp +103 -0
- package/cpp/Ecr17Response/Ecr17Response.cpp +155 -0
- package/cpp/Ecr17Response/Ecr17Response.hpp +113 -0
- package/cpp/Lcr/Lcr.cpp +42 -0
- package/cpp/Lcr/Lcr.hpp +22 -0
- package/cpp/PacketCodec/PacketCodec.cpp +146 -0
- package/cpp/PacketCodec/PacketCodec.hpp +48 -0
- package/cpp/Session/Ecr17Session.cpp +260 -0
- package/cpp/Session/Ecr17Session.hpp +97 -0
- package/cpp/Session/RetryPolicy.hpp +23 -0
- package/cpp/Transport/FakeTransport.hpp +95 -0
- package/cpp/Transport/NativeTransportAdapter.cpp +42 -0
- package/cpp/Transport/NativeTransportAdapter.hpp +32 -0
- package/cpp/Transport/Transport.hpp +31 -0
- package/cpp/tests/CMakeLists.txt +55 -0
- package/cpp/tests/PosixTcpTransport.hpp +105 -0
- package/cpp/tests/stubs/LrcMode.hpp +25 -0
- package/cpp/tests/test_flows.cpp +148 -0
- package/cpp/tests/test_integration_terminal.cpp +72 -0
- package/cpp/tests/test_lrc.cpp +66 -0
- package/cpp/tests/test_packet_codec.cpp +164 -0
- package/cpp/tests/test_protocol.cpp +102 -0
- package/cpp/tests/test_protocol_commands.cpp +190 -0
- package/cpp/tests/test_response.cpp +164 -0
- package/cpp/tests/test_retry_policy.cpp +28 -0
- package/cpp/tests/test_session.cpp +262 -0
- package/ios/Bridge.h +1 -0
- package/ios/HybridEcr17Transport.swift +103 -0
- package/nitro.json +30 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/Ecr17+autolinking.cmake +82 -0
- package/nitrogen/generated/android/Ecr17+autolinking.gradle +27 -0
- package/nitrogen/generated/android/Ecr17OnLoad.cpp +68 -0
- package/nitrogen/generated/android/Ecr17OnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__shared_ptr_ArrayBuffer_.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridEcr17TransportSpec.cpp +93 -0
- package/nitrogen/generated/android/c++/JHybridEcr17TransportSpec.hpp +68 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Ecr17OnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Func_void.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Func_void_std__shared_ptr_ArrayBuffer_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/HybridEcr17TransportSpec.kt +86 -0
- package/nitrogen/generated/ios/Ecr17+autolinking.rb +62 -0
- package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Bridge.cpp +57 -0
- package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Bridge.hpp +154 -0
- package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Umbrella.hpp +47 -0
- package/nitrogen/generated/ios/Ecr17Autolinking.mm +43 -0
- package/nitrogen/generated/ios/Ecr17Autolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridEcr17TransportSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridEcr17TransportSpecSwift.hpp +119 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridEcr17TransportSpec.swift +60 -0
- package/nitrogen/generated/ios/swift/HybridEcr17TransportSpec_cxx.swift +211 -0
- package/nitrogen/generated/shared/c++/CardType.hpp +84 -0
- package/nitrogen/generated/shared/c++/CardVerificationRequest.hpp +97 -0
- package/nitrogen/generated/shared/c++/CardVerificationResult.hpp +136 -0
- package/nitrogen/generated/shared/c++/CloseSessionResult.hpp +106 -0
- package/nitrogen/generated/shared/c++/ConnectionState.hpp +80 -0
- package/nitrogen/generated/shared/c++/CurrencyExchange.hpp +100 -0
- package/nitrogen/generated/shared/c++/Ecr17Config.hpp +138 -0
- package/nitrogen/generated/shared/c++/HybridEcr17ClientSpec.cpp +42 -0
- package/nitrogen/generated/shared/c++/HybridEcr17ClientSpec.hpp +138 -0
- package/nitrogen/generated/shared/c++/HybridEcr17TransportSpec.cpp +26 -0
- package/nitrogen/generated/shared/c++/HybridEcr17TransportSpec.hpp +70 -0
- package/nitrogen/generated/shared/c++/IncrementalAuthRequest.hpp +96 -0
- package/nitrogen/generated/shared/c++/LrcMode.hpp +84 -0
- package/nitrogen/generated/shared/c++/PaymentCardType.hpp +84 -0
- package/nitrogen/generated/shared/c++/PaymentRequest.hpp +109 -0
- package/nitrogen/generated/shared/c++/PaymentResult.hpp +139 -0
- package/nitrogen/generated/shared/c++/PosStatusResponse.hpp +96 -0
- package/nitrogen/generated/shared/c++/PreAuthClosureRequest.hpp +96 -0
- package/nitrogen/generated/shared/c++/PreAuthRequest.hpp +109 -0
- package/nitrogen/generated/shared/c++/PreAuthResult.hpp +144 -0
- package/nitrogen/generated/shared/c++/ProgressEvent.hpp +83 -0
- package/nitrogen/generated/shared/c++/ReceiptLine.hpp +83 -0
- package/nitrogen/generated/shared/c++/ReversalRequest.hpp +88 -0
- package/nitrogen/generated/shared/c++/ReversalResult.hpp +132 -0
- package/nitrogen/generated/shared/c++/TokenizationRequest.hpp +89 -0
- package/nitrogen/generated/shared/c++/TokenizationService.hpp +76 -0
- package/nitrogen/generated/shared/c++/TotalsResult.hpp +93 -0
- package/nitrogen/generated/shared/c++/TransactionEntryMode.hpp +92 -0
- package/nitrogen/generated/shared/c++/TransactionOutcome.hpp +88 -0
- package/nitrogen/generated/shared/c++/VasResult.hpp +96 -0
- package/package.json +102 -0
- package/react-native.config.js +18 -0
- package/src/index.ts +4 -0
- package/src/specs/client.nitro.ts +102 -0
- package/src/specs/transport.nitro.ts +25 -0
- package/src/types/client.ts +196 -0
- 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