@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,598 @@
1
+ #include "HybridEcr17Client.hpp"
2
+
3
+ #include <NitroModules/HybridObjectRegistry.hpp>
4
+
5
+ #include <chrono>
6
+ #include <ctime>
7
+ #include <exception>
8
+ #include <optional>
9
+ #include <stdexcept>
10
+ #include <string>
11
+ #include <type_traits>
12
+ #include <utility>
13
+
14
+ #include "Ecr17Protocol/Ecr17Protocol.hpp"
15
+ #include "Ecr17Response/Ecr17Response.hpp"
16
+ #include "Session/RetryPolicy.hpp"
17
+
18
+ // Commands run on Nitro's C++ thread pool. On Android, those worker threads are
19
+ // NOT attached to the JVM, and — even once attached — JNI `FindClass` on an
20
+ // attached worker thread resolves against the *system* class loader, which can't
21
+ // see app/NitroModules classes (e.g. com.margelo.nitro.core.ArrayBuffer, looked
22
+ // up lazily by the generated transport bridge in `send()` via JArrayBuffer::wrap).
23
+ // That yields "Unable to retrieve jni environment" (no JNIEnv) or
24
+ // "ClassNotFoundException ... DexPathList[... /system/lib64 ...]" (wrong loader).
25
+ #ifdef __ANDROID__
26
+ #include <fbjni/fbjni.h>
27
+ #endif
28
+
29
+ namespace margelo::nitro::ecr17 {
30
+
31
+ namespace {
32
+
33
+ // Runs `fn` on Android under fbjni's ThreadScope::WithClassLoader, which attaches
34
+ // the current thread to the JVM AND installs fbjni's cached app class loader for
35
+ // the duration — so every JNI FindClass inside (including NitroModules' lazy
36
+ // ArrayBuffer lookup) resolves app classes, not the system loader. On iOS (no JVM)
37
+ // `fn` is just called directly. Returns whatever `fn` returns (incl. void) and
38
+ // propagates exceptions, so the caller's try/catch and return-value logic is
39
+ // unchanged. WithClassLoader takes a `std::function<void()>`, so on Android the
40
+ // result is captured in a local and any exception via std::exception_ptr, then
41
+ // rethrown after the scope. `fn` is a lambda, so a `return` inside it returns from
42
+ // the lambda (not the caller) on BOTH platforms — safe in value-returning callers.
43
+ template <typename Fn>
44
+ auto runOnJvmThread(Fn&& fn) -> decltype(fn()) {
45
+ #ifdef __ANDROID__
46
+ using Ret = decltype(fn());
47
+ std::exception_ptr err;
48
+ if constexpr (std::is_void_v<Ret>) {
49
+ ::facebook::jni::ThreadScope::WithClassLoader([&]() {
50
+ try {
51
+ fn();
52
+ } catch (...) {
53
+ err = std::current_exception();
54
+ }
55
+ });
56
+ if (err) std::rethrow_exception(err);
57
+ } else {
58
+ std::optional<Ret> result;
59
+ ::facebook::jni::ThreadScope::WithClassLoader([&]() {
60
+ try {
61
+ result.emplace(fn());
62
+ } catch (...) {
63
+ err = std::current_exception();
64
+ }
65
+ });
66
+ if (err) std::rethrow_exception(err);
67
+ return std::move(*result);
68
+ }
69
+ #else
70
+ return fn();
71
+ #endif
72
+ }
73
+
74
+ } // namespace
75
+
76
+ using margelo::nitro::HybridObjectRegistry;
77
+ using margelo::nitro::Promise;
78
+
79
+ namespace {
80
+
81
+ std::optional<std::string> optStr(const std::string& s) {
82
+ return s.empty() ? std::nullopt : std::optional<std::string>(s);
83
+ }
84
+
85
+ std::optional<double> optNum(const std::string& s) {
86
+ if (s.empty()) {
87
+ return std::nullopt;
88
+ }
89
+ try {
90
+ return std::stod(s);
91
+ } catch (...) {
92
+ return std::nullopt;
93
+ }
94
+ }
95
+
96
+ TransactionOutcome mapOutcome(Outcome o) {
97
+ switch (o) {
98
+ case Outcome::Ok: return TransactionOutcome::OK;
99
+ case Outcome::Ko: return TransactionOutcome::KO;
100
+ case Outcome::CardNotPresent: return TransactionOutcome::CARDNOTPRESENT;
101
+ case Outcome::UnknownTag: return TransactionOutcome::UNKNOWNTAG;
102
+ default: return TransactionOutcome::UNKNOWN;
103
+ }
104
+ }
105
+
106
+ std::optional<CardType> mapCardType(const std::string& raw) {
107
+ if (raw == "1") return CardType::DEBIT;
108
+ if (raw == "2") return CardType::CREDIT;
109
+ if (raw == "3") return CardType::OTHER;
110
+ return std::nullopt;
111
+ }
112
+
113
+ std::optional<TransactionEntryMode> mapEntryMode(const std::string& raw) {
114
+ if (raw == "ICC") return TransactionEntryMode::ICC;
115
+ if (raw == "MAG") return TransactionEntryMode::MAG;
116
+ if (raw == "MAN") return TransactionEntryMode::MANUAL;
117
+ if (raw == "CLM") return TransactionEntryMode::CLESSMAG;
118
+ if (raw == "CLI") return TransactionEntryMode::CLESSICC;
119
+ return std::nullopt;
120
+ }
121
+
122
+ char mapPaymentType(const std::optional<PaymentCardType>& t) {
123
+ if (!t.has_value()) return '0';
124
+ switch (*t) {
125
+ case PaymentCardType::DEBIT: return '1';
126
+ case PaymentCardType::CREDIT: return '2';
127
+ case PaymentCardType::OTHER: return '3';
128
+ default: return '0'; // AUTO
129
+ }
130
+ }
131
+
132
+ PaymentResult mapPayment(const PaymentResponse& p) {
133
+ PaymentResult r;
134
+ r.outcome = mapOutcome(p.outcome);
135
+ r.resultCode = p.resultCode;
136
+ r.pan = optStr(p.pan);
137
+ r.entryMode = mapEntryMode(p.transactionType);
138
+ r.authCode = optStr(p.authCode);
139
+ r.hostDateTime = optStr(p.hostDateTime);
140
+ r.cardType = mapCardType(p.cardType);
141
+ r.acquirerId = optStr(p.acquirerId);
142
+ r.stan = optStr(p.stan);
143
+ r.onlineId = optStr(p.onlineId);
144
+ r.errorDescription = optStr(p.errorDescription);
145
+ if (p.currency.applied) {
146
+ CurrencyExchange ce; // the Nitro-generated struct
147
+ ce.applied = true;
148
+ ce.rate = optNum(p.currency.rate);
149
+ ce.currencyCode = optStr(p.currency.currencyCode);
150
+ ce.amountCents = optNum(p.currency.amount);
151
+ ce.precision = optNum(p.currency.precision);
152
+ r.currencyExchange = ce;
153
+ }
154
+ return r;
155
+ }
156
+
157
+ ReversalResult mapReversal(const PaymentResponse& p) {
158
+ ReversalResult r;
159
+ r.outcome = mapOutcome(p.outcome);
160
+ r.resultCode = p.resultCode;
161
+ r.pan = optStr(p.pan);
162
+ r.entryMode = mapEntryMode(p.transactionType);
163
+ r.hostDateTime = optStr(p.hostDateTime);
164
+ r.cardType = mapCardType(p.cardType);
165
+ r.acquirerId = optStr(p.acquirerId);
166
+ r.stan = optStr(p.stan);
167
+ r.onlineId = optStr(p.onlineId);
168
+ r.errorDescription = optStr(p.errorDescription);
169
+ return r;
170
+ }
171
+
172
+ CardVerificationResult mapCardVerify(const PaymentResponse& p) {
173
+ CardVerificationResult r;
174
+ r.outcome = mapOutcome(p.outcome);
175
+ r.resultCode = p.resultCode;
176
+ r.pan = optStr(p.pan);
177
+ r.entryMode = mapEntryMode(p.transactionType);
178
+ r.authCode = optStr(p.authCode);
179
+ r.hostDateTime = optStr(p.hostDateTime);
180
+ r.cardType = mapCardType(p.cardType);
181
+ r.acquirerId = optStr(p.acquirerId);
182
+ r.stan = optStr(p.stan);
183
+ r.onlineId = optStr(p.onlineId);
184
+ r.errorDescription = optStr(p.errorDescription);
185
+ return r;
186
+ }
187
+
188
+ PreAuthResult mapPreAuth(const PreAuthResponse& p) {
189
+ PreAuthResult r;
190
+ r.outcome = mapOutcome(p.outcome);
191
+ r.resultCode = p.resultCode;
192
+ r.pan = optStr(p.pan);
193
+ r.entryMode = mapEntryMode(p.transactionType);
194
+ r.authCode = optStr(p.authCode);
195
+ r.preAuthorizedAmountCents = optNum(p.preAuthorizedAmount);
196
+ r.preAuthCode = optStr(p.preAuthCode);
197
+ r.actionCode = optStr(p.actionCode);
198
+ r.hostDateTime = optStr(p.hostDateTime);
199
+ r.cardType = mapCardType(p.cardType);
200
+ r.acquirerId = optStr(p.acquirerId);
201
+ r.stan = optStr(p.stan);
202
+ r.onlineId = optStr(p.onlineId);
203
+ r.errorDescription = optStr(p.errorDescription);
204
+ return r;
205
+ }
206
+
207
+ PosStatusResponse mapStatus(const StatusResponse& s) {
208
+ PosStatusResponse r;
209
+ r.terminalId = s.terminalId;
210
+ r.status = static_cast<double>(s.status);
211
+ r.softwareRelease = s.softwareRelease;
212
+ // Parse "DDMMYYhhmm" into a time_point; fall back to epoch on bad input.
213
+ std::chrono::system_clock::time_point tp{};
214
+ if (s.dateTimeRaw.size() >= 10) {
215
+ try {
216
+ std::tm tm{};
217
+ tm.tm_mday = std::stoi(s.dateTimeRaw.substr(0, 2));
218
+ tm.tm_mon = std::stoi(s.dateTimeRaw.substr(2, 2)) - 1;
219
+ tm.tm_year = 100 + std::stoi(s.dateTimeRaw.substr(4, 2)); // 20YY
220
+ tm.tm_hour = std::stoi(s.dateTimeRaw.substr(6, 2));
221
+ tm.tm_min = std::stoi(s.dateTimeRaw.substr(8, 2));
222
+ tm.tm_isdst = -1;
223
+ std::time_t t = std::mktime(&tm);
224
+ if (t != -1) {
225
+ tp = std::chrono::system_clock::from_time_t(t);
226
+ }
227
+ } catch (...) {
228
+ // keep epoch
229
+ }
230
+ }
231
+ r.terminalDateTime = tp;
232
+ return r;
233
+ }
234
+
235
+ TotalsResult mapTotals(const TotalsResponse& t) {
236
+ TotalsResult r;
237
+ r.outcome = mapOutcome(t.outcome);
238
+ r.resultCode = t.resultCode;
239
+ r.posTotalCents = optNum(t.posTotal).value_or(0.0);
240
+ return r;
241
+ }
242
+
243
+ CloseSessionResult mapClose(const CloseResponse& c) {
244
+ CloseSessionResult r;
245
+ r.outcome = mapOutcome(c.outcome);
246
+ r.resultCode = c.resultCode;
247
+ r.posTotalCents = optNum(c.posTotal);
248
+ r.hostTotalCents = optNum(c.hostTotal);
249
+ r.actionCode = optStr(c.actionCode);
250
+ r.errorDescription = optStr(c.errorDescription);
251
+ return r;
252
+ }
253
+
254
+ VasResult mapVas(const VasResponse& v) {
255
+ VasResult r;
256
+ r.responseId = v.responseId;
257
+ r.responseMessage = v.responseMessage;
258
+ r.orderId = optStr(v.orderId);
259
+ r.rawXml = v.rawXml;
260
+ return r;
261
+ }
262
+
263
+ } // namespace
264
+
265
+ void HybridEcr17Client::configure(const Ecr17Config& config) {
266
+ config_ = config;
267
+ // Close any open socket before tearing down the old transport, otherwise the
268
+ // prior native connection leaks until the HybridObject is collected.
269
+ if (transport_) {
270
+ transport_->disconnect();
271
+ }
272
+ // Force re-init so a new configuration rebuilds the session/timeouts.
273
+ session_.reset();
274
+ adapter_.reset();
275
+ transport_.reset();
276
+ // Create the transport HybridObject NOW, on this (JS) thread. createHybridObject
277
+ // does a JNI FindClass for the Kotlin transport, which resolves only against the
278
+ // app class loader — and the JS thread has it. Doing it lazily on a Nitro worker
279
+ // thread (attached via ThreadScope) would use the system class loader and throw
280
+ // ClassNotFoundException. fbjni caches the resolved jclass globally, so later
281
+ // method calls from worker threads work (they only need a JNIEnv, see the guards).
282
+ ensureInit();
283
+ }
284
+
285
+ Ecr17Config HybridEcr17Client::configuration() { return config_; }
286
+
287
+ void HybridEcr17Client::ensureInit() {
288
+ if (session_) {
289
+ return;
290
+ }
291
+ auto obj = HybridObjectRegistry::createHybridObject("Ecr17Transport");
292
+ // HybridObject is a *virtual* base, so static_pointer_cast can't downcast
293
+ // from it — must use dynamic_pointer_cast.
294
+ transport_ = std::dynamic_pointer_cast<HybridEcr17TransportSpec>(obj);
295
+ if (!transport_) {
296
+ throw std::runtime_error("ECR17: registry returned an incompatible Ecr17Transport object");
297
+ }
298
+ adapter_ = std::make_shared<NativeTransportAdapter>(transport_);
299
+
300
+ SessionConfig sc;
301
+ sc.lrcMode = config_.lrcMode.value_or(LrcMode::STD);
302
+ sc.ackTimeoutMs = static_cast<int>(config_.ackTimeoutMs.value_or(2000));
303
+ sc.responseTimeoutMs = static_cast<int>(config_.responseTimeoutMs.value_or(60000));
304
+ sc.retryCount = static_cast<int>(config_.retryCount.value_or(3));
305
+ sc.retryDelayMs = static_cast<int>(config_.retryDelayMs.value_or(200));
306
+ sc.receiptDrainMs = static_cast<int>(config_.receiptDrainMs.value_or(0));
307
+ session_ = std::make_unique<Ecr17Session>(*adapter_, sc);
308
+
309
+ session_->setOnProgress([this](const std::string& message) {
310
+ if (onProgress_) onProgress_(ProgressEvent{message});
311
+ });
312
+ session_->setOnReceiptLine([this](const std::string& line) {
313
+ if (onReceiptLine_) onReceiptLine_(ReceiptLine{line});
314
+ });
315
+ }
316
+
317
+ void HybridEcr17Client::ensureConnected() {
318
+ // Run the transport JNI work under the app class loader (Android); inline on iOS.
319
+ runOnJvmThread([&]() {
320
+ ensureInit();
321
+ // PROACTIVE reconnect: isConnected() performs a synchronous, non-destructive
322
+ // liveness probe (Android: a 1-byte peek-with-pushback that detects a peer
323
+ // FIN without writing to or consuming from the stream) so a peer-closed/
324
+ // half-open socket — common because ECR17/Nexi terminals close TCP between
325
+ // transactions — is detected HERE, before any command is sent. This is what
326
+ // stops a financial command from being sent on a stale socket and then
327
+ // hitting the (correct) money-safety "never replay" path with a FALSE
328
+ // "transport disconnected".
329
+ if (transport_->isConnected()) {
330
+ return; // verified live — returns from this lambda only, no further JNI
331
+ }
332
+ // Auto-connect: block this worker thread until the native transport connects
333
+ // (or throw on failure). keepAlive leaves the socket open for reuse.
334
+ if (onConnectionStateChange_) onConnectionStateChange_(ConnectionState::CONNECTING);
335
+ const double port = config_.port.value_or(10000);
336
+ const double timeout = config_.connectionTimeoutMs.value_or(5000);
337
+ try {
338
+ transport_->connect(config_.host, port, timeout)->await().get();
339
+ } catch (...) {
340
+ // Don't leave listeners stuck on CONNECTING when the connection fails.
341
+ if (onConnectionStateChange_) onConnectionStateChange_(ConnectionState::DISCONNECTED);
342
+ throw;
343
+ }
344
+ if (onConnectionStateChange_) onConnectionStateChange_(ConnectionState::CONNECTED);
345
+ });
346
+ }
347
+
348
+ std::string HybridEcr17Client::cashRegisterIdOr(const std::optional<std::string>& override) const {
349
+ return override.value_or(config_.cashRegisterId);
350
+ }
351
+
352
+ DecodedPacket HybridEcr17Client::runTransaction(
353
+ const std::string& mainPayload, const std::optional<TokenizationRequest>& tokenization,
354
+ bool safeToRetry) {
355
+ std::lock_guard<std::mutex> txLock(txMutex_); // serialize exchanges on the shared session
356
+ auto doExchange = [&]() -> DecodedPacket {
357
+ if (tokenization.has_value()) {
358
+ const bool recurring = tokenization->service == TokenizationService::RECURRING;
359
+ const std::string tag =
360
+ Ecr17Protocol::formatTokenizationTag(recurring, tokenization->contractCode);
361
+ const std::string additional =
362
+ Ecr17Protocol::buildAdditionalTagsMessage(config_.terminalId, tag);
363
+ return session_->exchangeWithAdditionalData(mainPayload, additional);
364
+ }
365
+ return session_->exchange(mainPayload);
366
+ };
367
+
368
+ // All exchange JNI (session_ -> adapter_ -> Kotlin transport, incl. the
369
+ // ArrayBuffer lookup in send()) must run under the app class loader on Android.
370
+ return runOnJvmThread([&]() -> DecodedPacket {
371
+ try {
372
+ return doExchange();
373
+ } catch (const std::exception&) {
374
+ const auto originalError = std::current_exception();
375
+ const bool autoReconnect = config_.autoReconnect.value_or(false);
376
+ const bool dropped = !transport_ || !transport_->isConnected();
377
+ if (autoReconnect && dropped) {
378
+ try {
379
+ ensureConnected(); // restore the socket for subsequent commands
380
+ } catch (...) {
381
+ // Reconnect failed: surface the original exchange error, not the
382
+ // reconnect failure (the former is what the caller needs to see).
383
+ std::rethrow_exception(originalError);
384
+ }
385
+ }
386
+ if (shouldRetryAfterReconnect(autoReconnect, dropped, safeToRetry)) {
387
+ return doExchange(); // only read-only/idempotent ops may be replayed
388
+ }
389
+ throw; // financial op: surface the error (recover via sendLastResult / 'G')
390
+ }
391
+ });
392
+ }
393
+
394
+ void HybridEcr17Client::runAckOnly(const std::string& payload, bool safeToRetry) {
395
+ std::lock_guard<std::mutex> txLock(txMutex_); // serialize exchanges on the shared session
396
+ // Send under the app class loader on Android (ArrayBuffer lookup in send()).
397
+ runOnJvmThread([&]() {
398
+ try {
399
+ session_->sendAckOnly(payload);
400
+ } catch (const std::exception&) {
401
+ const auto originalError = std::current_exception();
402
+ const bool autoReconnect = config_.autoReconnect.value_or(false);
403
+ const bool dropped = !transport_ || !transport_->isConnected();
404
+ if (autoReconnect && dropped) {
405
+ try {
406
+ ensureConnected();
407
+ } catch (...) {
408
+ std::rethrow_exception(originalError); // surface the original error
409
+ }
410
+ }
411
+ if (!shouldRetryAfterReconnect(autoReconnect, dropped, safeToRetry)) {
412
+ throw; // not retryable: surface the original error
413
+ }
414
+ session_->sendAckOnly(payload); // read-only/idempotent op: safe to replay
415
+ }
416
+ });
417
+ }
418
+
419
+ std::shared_ptr<Promise<void>> HybridEcr17Client::connect() {
420
+ // Delegate to ensureConnected so the explicit Connect path emits CONNECTING
421
+ // and then CONNECTED on success (consistent with command auto-connect);
422
+ // returning the raw transport promise would leave listeners stuck on
423
+ // CONNECTING. Runs on a worker thread (ensureConnected blocks until ready).
424
+ return Promise<void>::async([this]() { ensureConnected(); });
425
+ }
426
+
427
+ void HybridEcr17Client::disconnect() {
428
+ if (transport_) {
429
+ transport_->disconnect();
430
+ }
431
+ if (onConnectionStateChange_) onConnectionStateChange_(ConnectionState::DISCONNECTED);
432
+ }
433
+
434
+ bool HybridEcr17Client::isConnected() { return transport_ && transport_->isConnected(); }
435
+
436
+ std::shared_ptr<Promise<PosStatusResponse>> HybridEcr17Client::status() {
437
+ return Promise<PosStatusResponse>::async([this]() -> PosStatusResponse {
438
+ ensureConnected();
439
+ auto pkt = runTransaction(Ecr17Protocol::buildStatusMessage(config_.terminalId),
440
+ std::nullopt, /*safeToRetry=*/true);
441
+ return mapStatus(Ecr17Response::parseStatus(pkt.payload));
442
+ });
443
+ }
444
+
445
+ std::shared_ptr<Promise<PaymentResult>> HybridEcr17Client::pay(const PaymentRequest& request) {
446
+ return Promise<PaymentResult>::async([this, request]() -> PaymentResult {
447
+ ensureConnected();
448
+ const bool tok = request.tokenization.has_value();
449
+ auto payload = Ecr17Protocol::buildPaymentMessage(
450
+ config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
451
+ static_cast<int>(request.amountCents), mapPaymentType(request.paymentType),
452
+ request.cardAlreadyPresent.value_or(false), tok, request.receiptText.value_or(""));
453
+ auto pkt = runTransaction(payload, request.tokenization, false);
454
+ return mapPayment(Ecr17Response::parsePayment(pkt.payload));
455
+ });
456
+ }
457
+
458
+ std::shared_ptr<Promise<PaymentResult>> HybridEcr17Client::payExtended(const PaymentRequest& request) {
459
+ return Promise<PaymentResult>::async([this, request]() -> PaymentResult {
460
+ ensureConnected();
461
+ const bool tok = request.tokenization.has_value();
462
+ auto payload = Ecr17Protocol::buildExtendedPaymentMessage(
463
+ config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
464
+ static_cast<int>(request.amountCents), mapPaymentType(request.paymentType),
465
+ request.cardAlreadyPresent.value_or(false), tok, request.receiptText.value_or(""));
466
+ auto pkt = runTransaction(payload, request.tokenization, false);
467
+ return mapPayment(Ecr17Response::parsePayment(pkt.payload));
468
+ });
469
+ }
470
+
471
+ std::shared_ptr<Promise<ReversalResult>> HybridEcr17Client::reverse(const ReversalRequest& request) {
472
+ return Promise<ReversalResult>::async([this, request]() -> ReversalResult {
473
+ ensureConnected();
474
+ auto payload = Ecr17Protocol::buildReversalMessage(
475
+ config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
476
+ request.stan.value_or("000000"));
477
+ auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
478
+ return mapReversal(Ecr17Response::parsePayment(pkt.payload));
479
+ });
480
+ }
481
+
482
+ std::shared_ptr<Promise<PreAuthResult>> HybridEcr17Client::preAuth(const PreAuthRequest& request) {
483
+ return Promise<PreAuthResult>::async([this, request]() -> PreAuthResult {
484
+ ensureConnected();
485
+ const bool tok = request.tokenization.has_value();
486
+ auto payload = Ecr17Protocol::buildPreAuthMessage(
487
+ config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
488
+ static_cast<int>(request.amountCents), mapPaymentType(request.paymentType),
489
+ request.cardAlreadyPresent.value_or(false), tok, request.receiptText.value_or(""));
490
+ auto pkt = runTransaction(payload, request.tokenization, false);
491
+ return mapPreAuth(Ecr17Response::parsePreAuth(pkt.payload));
492
+ });
493
+ }
494
+
495
+ std::shared_ptr<Promise<PreAuthResult>> HybridEcr17Client::incrementalAuth(
496
+ const IncrementalAuthRequest& request) {
497
+ return Promise<PreAuthResult>::async([this, request]() -> PreAuthResult {
498
+ ensureConnected();
499
+ auto payload = Ecr17Protocol::buildIncrementalMessage(
500
+ config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
501
+ static_cast<int>(request.amountCents), request.originalPreAuthCode, false,
502
+ request.receiptText.value_or(""));
503
+ auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
504
+ return mapPreAuth(Ecr17Response::parsePreAuth(pkt.payload));
505
+ });
506
+ }
507
+
508
+ std::shared_ptr<Promise<PaymentResult>> HybridEcr17Client::preAuthClosure(
509
+ const PreAuthClosureRequest& request) {
510
+ return Promise<PaymentResult>::async([this, request]() -> PaymentResult {
511
+ ensureConnected();
512
+ auto payload = Ecr17Protocol::buildPreAuthClosureMessage(
513
+ config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
514
+ static_cast<int>(request.amountCents), request.originalPreAuthCode, false,
515
+ request.receiptText.value_or(""));
516
+ auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
517
+ return mapPayment(Ecr17Response::parsePayment(pkt.payload));
518
+ });
519
+ }
520
+
521
+ std::shared_ptr<Promise<CardVerificationResult>> HybridEcr17Client::verifyCard(
522
+ const CardVerificationRequest& request) {
523
+ return Promise<CardVerificationResult>::async([this, request]() -> CardVerificationResult {
524
+ ensureConnected();
525
+ const bool tok = request.tokenization.has_value();
526
+ auto payload = Ecr17Protocol::buildCardVerificationMessage(
527
+ config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
528
+ mapPaymentType(request.paymentType), tok);
529
+ auto pkt = runTransaction(payload, request.tokenization, false);
530
+ return mapCardVerify(Ecr17Response::parsePayment(pkt.payload));
531
+ });
532
+ }
533
+
534
+ std::shared_ptr<Promise<CloseSessionResult>> HybridEcr17Client::closeSession() {
535
+ return Promise<CloseSessionResult>::async([this]() -> CloseSessionResult {
536
+ ensureConnected();
537
+ auto payload = Ecr17Protocol::buildCloseSessionMessage(config_.terminalId, config_.cashRegisterId);
538
+ auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
539
+ return mapClose(Ecr17Response::parseClose(pkt.payload));
540
+ });
541
+ }
542
+
543
+ std::shared_ptr<Promise<TotalsResult>> HybridEcr17Client::totals() {
544
+ return Promise<TotalsResult>::async([this]() -> TotalsResult {
545
+ ensureConnected();
546
+ auto payload = Ecr17Protocol::buildTotalsMessage(config_.terminalId, config_.cashRegisterId);
547
+ auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/true);
548
+ return mapTotals(Ecr17Response::parseTotals(pkt.payload));
549
+ });
550
+ }
551
+
552
+ std::shared_ptr<Promise<PaymentResult>> HybridEcr17Client::sendLastResult() {
553
+ return Promise<PaymentResult>::async([this]() -> PaymentResult {
554
+ ensureConnected();
555
+ auto payload = Ecr17Protocol::buildSendLastResultMessage(config_.terminalId, config_.cashRegisterId);
556
+ auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/true);
557
+ return mapPayment(Ecr17Response::parsePayment(pkt.payload));
558
+ });
559
+ }
560
+
561
+ std::shared_ptr<Promise<void>> HybridEcr17Client::enableEcrPrinting(bool enabled) {
562
+ return Promise<void>::async([this, enabled]() {
563
+ ensureConnected();
564
+ runAckOnly(Ecr17Protocol::buildEnableEcrPrintMessage(config_.terminalId, enabled),
565
+ /*safeToRetry=*/true);
566
+ });
567
+ }
568
+
569
+ std::shared_ptr<Promise<void>> HybridEcr17Client::reprint(bool toEcr) {
570
+ return Promise<void>::async([this, toEcr]() {
571
+ ensureConnected();
572
+ runAckOnly(Ecr17Protocol::buildReprintMessage(config_.terminalId, toEcr),
573
+ /*safeToRetry=*/false);
574
+ });
575
+ }
576
+
577
+ std::shared_ptr<Promise<VasResult>> HybridEcr17Client::vas(const std::string& xmlRequest) {
578
+ return Promise<VasResult>::async([this, xmlRequest]() -> VasResult {
579
+ ensureConnected();
580
+ auto payload = Ecr17Protocol::buildVasMessage(config_.terminalId, config_.cashRegisterId, xmlRequest);
581
+ auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
582
+ return mapVas(Ecr17Response::parseVas(pkt.payload));
583
+ });
584
+ }
585
+
586
+ void HybridEcr17Client::setOnProgress(const std::function<void(const ProgressEvent&)>& callback) {
587
+ onProgress_ = callback;
588
+ }
589
+
590
+ void HybridEcr17Client::setOnReceiptLine(const std::function<void(const ReceiptLine&)>& callback) {
591
+ onReceiptLine_ = callback;
592
+ }
593
+
594
+ void HybridEcr17Client::setOnConnectionStateChange(const std::function<void(ConnectionState)>& callback) {
595
+ onConnectionStateChange_ = callback;
596
+ }
597
+
598
+ } // namespace margelo::nitro::ecr17
@@ -0,0 +1,85 @@
1
+ #pragma once
2
+
3
+ #include <NitroModules/Promise.hpp>
4
+
5
+ #include <memory>
6
+ #include <mutex>
7
+
8
+ #include "HybridEcr17ClientSpec.hpp"
9
+ #include "HybridEcr17TransportSpec.hpp"
10
+ #include "Session/Ecr17Session.hpp"
11
+ #include "Transport/NativeTransportAdapter.hpp"
12
+
13
+ namespace margelo::nitro::ecr17 {
14
+
15
+ class HybridEcr17Client : public HybridEcr17ClientSpec {
16
+ public:
17
+ HybridEcr17Client() : HybridObject(TAG) {}
18
+
19
+ // --- Configuration (synchronous) ---
20
+ void configure(const Ecr17Config& config) override;
21
+ Ecr17Config configuration() override;
22
+
23
+ // --- Connection ---
24
+ std::shared_ptr<margelo::nitro::Promise<void>> connect() override;
25
+ void disconnect() override;
26
+ bool isConnected() override;
27
+
28
+ // --- Commands ---
29
+ std::shared_ptr<margelo::nitro::Promise<PosStatusResponse>> status() override;
30
+ std::shared_ptr<margelo::nitro::Promise<PaymentResult>> pay(const PaymentRequest& request) override;
31
+ std::shared_ptr<margelo::nitro::Promise<PaymentResult>> payExtended(const PaymentRequest& request) override;
32
+ std::shared_ptr<margelo::nitro::Promise<ReversalResult>> reverse(const ReversalRequest& request) override;
33
+ std::shared_ptr<margelo::nitro::Promise<PreAuthResult>> preAuth(const PreAuthRequest& request) override;
34
+ std::shared_ptr<margelo::nitro::Promise<PreAuthResult>> incrementalAuth(const IncrementalAuthRequest& request) override;
35
+ std::shared_ptr<margelo::nitro::Promise<PaymentResult>> preAuthClosure(const PreAuthClosureRequest& request) override;
36
+ std::shared_ptr<margelo::nitro::Promise<CardVerificationResult>> verifyCard(const CardVerificationRequest& request) override;
37
+ std::shared_ptr<margelo::nitro::Promise<CloseSessionResult>> closeSession() override;
38
+ std::shared_ptr<margelo::nitro::Promise<TotalsResult>> totals() override;
39
+ std::shared_ptr<margelo::nitro::Promise<PaymentResult>> sendLastResult() override;
40
+ std::shared_ptr<margelo::nitro::Promise<void>> enableEcrPrinting(bool enabled) override;
41
+ std::shared_ptr<margelo::nitro::Promise<void>> reprint(bool toEcr) override;
42
+ std::shared_ptr<margelo::nitro::Promise<VasResult>> vas(const std::string& xmlRequest) override;
43
+
44
+ // --- Events ---
45
+ void setOnProgress(const std::function<void(const ProgressEvent&)>& callback) override;
46
+ void setOnReceiptLine(const std::function<void(const ReceiptLine&)>& callback) override;
47
+ void setOnConnectionStateChange(const std::function<void(ConnectionState)>& callback) override;
48
+
49
+ protected:
50
+ // Lazily creates the native transport (via the Nitro registry), the adapter
51
+ // and the session, and wires session events to the JS callbacks.
52
+ void ensureInit();
53
+ // Ensures an open connection, auto-connecting (and blocking the worker
54
+ // thread until ready) if needed. Throws if the connection fails.
55
+ void ensureConnected();
56
+ std::string cashRegisterIdOr(const std::optional<std::string>& override) const;
57
+ // Runs a transaction, attaching the tokenization 'U' additional-data message
58
+ // when `tokenization` is set (request must be built with withAdditionalData=true).
59
+ // On a mid-command disconnect with autoReconnect enabled, the socket is
60
+ // reconnected; the command is retried ONLY if `safeToRetry` (read-only ops),
61
+ // never for financial ops (a blind retry could double-charge — recover via
62
+ // sendLastResult / 'G' instead).
63
+ DecodedPacket runTransaction(const std::string& mainPayload,
64
+ const std::optional<TokenizationRequest>& tokenization,
65
+ bool safeToRetry);
66
+ void runAckOnly(const std::string& payload, bool safeToRetry);
67
+
68
+ Ecr17Config config_;
69
+
70
+ // Serializes protocol exchanges: every public command runs on a Promise
71
+ // worker thread but they share one session_/transport_ and RX buffer, so
72
+ // concurrent commands must not interleave on the wire (or ACK each other's
73
+ // frames). Held for the duration of a single transaction's exchange.
74
+ std::mutex txMutex_;
75
+
76
+ std::shared_ptr<HybridEcr17TransportSpec> transport_;
77
+ std::shared_ptr<NativeTransportAdapter> adapter_;
78
+ std::unique_ptr<Ecr17Session> session_;
79
+
80
+ std::function<void(const ProgressEvent&)> onProgress_{};
81
+ std::function<void(const ReceiptLine&)> onReceiptLine_{};
82
+ std::function<void(ConnectionState)> onConnectionStateChange_{};
83
+ };
84
+
85
+ } // namespace margelo::nitro::ecr17