react-native-amwal-ecr 0.1.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/CHANGELOG.md +30 -0
- package/LICENSE +20 -0
- package/README.md +278 -0
- package/RNAmwalEcr.podspec +27 -0
- package/android/build.gradle +53 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/amwalecr/AmwalEcrModule.kt +325 -0
- package/android/src/main/java/com/amwalecr/AmwalEcrPackage.kt +31 -0
- package/android/src/main/java/com/amwalecr/EcrMapping.kt +135 -0
- package/docs/compatibility.md +22 -0
- package/ios/AmwalEcr.h +4 -0
- package/ios/AmwalEcr.mm +121 -0
- package/ios/AmwalEcr.swift +424 -0
- package/ios/EcrMapping.swift +142 -0
- package/lib/module/EcrOperation.js +18 -0
- package/lib/module/EcrOperation.js.map +1 -0
- package/lib/module/EcrTerminal.js +211 -0
- package/lib/module/EcrTerminal.js.map +1 -0
- package/lib/module/NativeAmwalEcr.js +5 -0
- package/lib/module/NativeAmwalEcr.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native.js +16 -0
- package/lib/module/native.js.map +1 -0
- package/lib/module/native.native.js +4 -0
- package/lib/module/native.native.js.map +1 -0
- package/lib/module/normalize.js +87 -0
- package/lib/module/normalize.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/EcrOperation.d.ts +10 -0
- package/lib/typescript/src/EcrOperation.d.ts.map +1 -0
- package/lib/typescript/src/EcrTerminal.d.ts +34 -0
- package/lib/typescript/src/EcrTerminal.d.ts.map +1 -0
- package/lib/typescript/src/NativeAmwalEcr.d.ts +14 -0
- package/lib/typescript/src/NativeAmwalEcr.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/native.d.ts +3 -0
- package/lib/typescript/src/native.d.ts.map +1 -0
- package/lib/typescript/src/native.native.d.ts +2 -0
- package/lib/typescript/src/native.native.d.ts.map +1 -0
- package/lib/typescript/src/normalize.d.ts +5 -0
- package/lib/typescript/src/normalize.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +157 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +153 -0
- package/src/EcrOperation.ts +25 -0
- package/src/EcrTerminal.ts +344 -0
- package/src/NativeAmwalEcr.ts +28 -0
- package/src/index.tsx +34 -0
- package/src/native.native.ts +1 -0
- package/src/native.ts +18 -0
- package/src/normalize.ts +116 -0
- package/src/types.ts +189 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
package com.amwalecr
|
|
2
|
+
|
|
3
|
+
import com.amwalpay.ecr.EcrConfig
|
|
4
|
+
import com.amwalpay.ecr.EcrEnvironment
|
|
5
|
+
import com.amwalpay.ecr.EcrLink
|
|
6
|
+
import com.amwalpay.ecr.EcrLogger
|
|
7
|
+
import com.amwalpay.ecr.EcrOpenedSession
|
|
8
|
+
import com.amwalpay.ecr.EcrSessions
|
|
9
|
+
import com.facebook.react.bridge.Promise
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
|
+
import com.facebook.react.bridge.ReadableMap
|
|
12
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
13
|
+
import java.math.BigDecimal
|
|
14
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
15
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
16
|
+
import kotlin.time.Duration.Companion.milliseconds
|
|
17
|
+
import kotlinx.coroutines.CoroutineScope
|
|
18
|
+
import kotlinx.coroutines.CoroutineStart
|
|
19
|
+
import kotlinx.coroutines.Dispatchers
|
|
20
|
+
import kotlinx.coroutines.Job
|
|
21
|
+
import kotlinx.coroutines.SupervisorJob
|
|
22
|
+
import kotlinx.coroutines.cancel
|
|
23
|
+
import kotlinx.coroutines.launch
|
|
24
|
+
|
|
25
|
+
@ReactModule(name = AmwalEcrModule.NAME)
|
|
26
|
+
class AmwalEcrModule(reactContext: ReactApplicationContext) :
|
|
27
|
+
NativeAmwalEcrSpec(reactContext) {
|
|
28
|
+
|
|
29
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
|
30
|
+
private val running = ConcurrentHashMap<String, RunningOperation>()
|
|
31
|
+
|
|
32
|
+
override fun isReachable(request: ReadableMap, promise: Promise) {
|
|
33
|
+
val call = parse(request, promise) ?: return
|
|
34
|
+
if (!call.isIpTransport) {
|
|
35
|
+
promise.resolve(false)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
scope.launch {
|
|
39
|
+
try {
|
|
40
|
+
promise.resolve(call.session.probeReachability()?.reachable == true)
|
|
41
|
+
} catch (error: IllegalArgumentException) {
|
|
42
|
+
promise.reject(ECR_INVALID_ARGUMENT, error.message ?: "Invalid ECR arguments", error)
|
|
43
|
+
} catch (error: Exception) {
|
|
44
|
+
promise.reject(ECR_INTERNAL, error.message ?: "Reachability probe failed", error)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override fun sale(request: ReadableMap, promise: Promise) {
|
|
50
|
+
run(request, promise) { call ->
|
|
51
|
+
EcrMapping.result(
|
|
52
|
+
call.session.sale(
|
|
53
|
+
amount = call.requiredAmount(),
|
|
54
|
+
merchantReference = call.merchantReferenceId,
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override fun voidTransaction(request: ReadableMap, promise: Promise) {
|
|
61
|
+
run(request, promise) { call ->
|
|
62
|
+
EcrMapping.result(
|
|
63
|
+
call.session.void(
|
|
64
|
+
receiptNumber = call.requiredReceiptNumber("A void"),
|
|
65
|
+
originalTerminalId = call.originalTerminalId,
|
|
66
|
+
merchantReference = call.merchantReferenceId,
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override fun refund(request: ReadableMap, promise: Promise) {
|
|
73
|
+
run(request, promise) { call ->
|
|
74
|
+
EcrMapping.result(
|
|
75
|
+
call.session.refund(
|
|
76
|
+
amount = call.requiredAmount(),
|
|
77
|
+
receiptNumber = call.requiredReceiptNumber("A refund"),
|
|
78
|
+
transactionDate = call.transactionDate,
|
|
79
|
+
originalTerminalId = call.originalTerminalId,
|
|
80
|
+
merchantReference = call.merchantReferenceId,
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override fun inquire(request: ReadableMap, promise: Promise) {
|
|
87
|
+
run(request, promise) { call ->
|
|
88
|
+
EcrMapping.inquiry(
|
|
89
|
+
call.session.inquire(
|
|
90
|
+
receiptNumber = call.requiredReceiptNumber("An inquiry"),
|
|
91
|
+
transactionDate = call.transactionDate,
|
|
92
|
+
originalTerminalId = call.originalTerminalId,
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override fun inquireByReference(request: ReadableMap, promise: Promise) {
|
|
99
|
+
run(request, promise) { call ->
|
|
100
|
+
EcrMapping.inquiry(
|
|
101
|
+
call.session.inquireByReference(
|
|
102
|
+
originalReference = call.requiredOriginalReference(),
|
|
103
|
+
transactionDate = call.transactionDate,
|
|
104
|
+
originalTerminalId = call.originalTerminalId,
|
|
105
|
+
merchantReference = call.merchantReferenceId,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
override fun receipt(request: ReadableMap, promise: Promise) {
|
|
112
|
+
run(request, promise) { call ->
|
|
113
|
+
EcrMapping.receipt(
|
|
114
|
+
call.session.receipt(
|
|
115
|
+
receiptNumber = call.requiredReceiptNumber("A receipt"),
|
|
116
|
+
transactionDate = call.transactionDate,
|
|
117
|
+
originalTerminalId = call.originalTerminalId,
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
override fun cancel(operationId: String, promise: Promise) {
|
|
124
|
+
val operation = running.remove(operationId)
|
|
125
|
+
if (operation == null) {
|
|
126
|
+
promise.resolve(false)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
operation.reply.resolve(
|
|
131
|
+
EcrMapping.failedResult(
|
|
132
|
+
merchantReferenceId = operation.merchantReferenceId,
|
|
133
|
+
kind = EcrMapping.CANCELLED,
|
|
134
|
+
message = CANCELLED_MESSAGE,
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
operation.job.cancel()
|
|
138
|
+
promise.resolve(true)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
override fun invalidate() {
|
|
142
|
+
running.values.forEach { operation ->
|
|
143
|
+
operation.reply.resolve(
|
|
144
|
+
EcrMapping.failedResult(
|
|
145
|
+
merchantReferenceId = operation.merchantReferenceId,
|
|
146
|
+
kind = EcrMapping.CANCELLED,
|
|
147
|
+
message = "The React Native host was invalidated while the terminal had the request. " +
|
|
148
|
+
"The transaction may still have completed — inquire before retrying.",
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
operation.job.cancel()
|
|
152
|
+
}
|
|
153
|
+
running.clear()
|
|
154
|
+
scope.cancel()
|
|
155
|
+
super.invalidate()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private fun run(
|
|
159
|
+
request: ReadableMap,
|
|
160
|
+
promise: Promise,
|
|
161
|
+
block: suspend (Call) -> Any,
|
|
162
|
+
) {
|
|
163
|
+
val call = parse(request, promise) ?: return
|
|
164
|
+
val reply = OneShotPromise(promise)
|
|
165
|
+
|
|
166
|
+
if (!call.isSupportedTransport) {
|
|
167
|
+
reply.resolve(
|
|
168
|
+
EcrMapping.failedResult(
|
|
169
|
+
merchantReferenceId = "",
|
|
170
|
+
kind = EcrMapping.UNSUPPORTED,
|
|
171
|
+
message = "This library drives a terminal over ethernet, wifi, or the Web Service " +
|
|
172
|
+
"REST path. Transport '${call.transport}' is not handled, so nothing was sent.",
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
lateinit var operation: RunningOperation
|
|
179
|
+
val job = scope.launch(start = CoroutineStart.LAZY) {
|
|
180
|
+
try {
|
|
181
|
+
reply.resolve(block(call))
|
|
182
|
+
} catch (error: IllegalArgumentException) {
|
|
183
|
+
reply.reject(ECR_INVALID_ARGUMENT, error.message ?: "Invalid ECR arguments", error)
|
|
184
|
+
} catch (error: Exception) {
|
|
185
|
+
reply.reject(ECR_INTERNAL, error.message ?: "Native ECR operation failed", error)
|
|
186
|
+
} finally {
|
|
187
|
+
running.remove(call.operationId, operation)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
operation = RunningOperation(job, reply, call.merchantReferenceId)
|
|
191
|
+
|
|
192
|
+
if (running.putIfAbsent(call.operationId, operation) != null) {
|
|
193
|
+
reply.reject(
|
|
194
|
+
ECR_INVALID_ARGUMENT,
|
|
195
|
+
"operationId '${call.operationId}' is already running",
|
|
196
|
+
null,
|
|
197
|
+
)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
job.start()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private fun parse(request: ReadableMap, promise: Promise): Call? = try {
|
|
204
|
+
Call(request)
|
|
205
|
+
} catch (error: IllegalArgumentException) {
|
|
206
|
+
promise.reject(ECR_INVALID_ARGUMENT, error.message ?: "Invalid ECR arguments", error)
|
|
207
|
+
null
|
|
208
|
+
} catch (error: Exception) {
|
|
209
|
+
promise.reject(ECR_INTERNAL, error.message ?: "Could not read ECR arguments", error)
|
|
210
|
+
null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private data class RunningOperation(
|
|
214
|
+
val job: Job,
|
|
215
|
+
val reply: OneShotPromise,
|
|
216
|
+
val merchantReferenceId: String,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
private class OneShotPromise(private val promise: Promise) {
|
|
220
|
+
private val answered = AtomicBoolean(false)
|
|
221
|
+
|
|
222
|
+
fun resolve(value: Any?) {
|
|
223
|
+
if (answered.compareAndSet(false, true)) promise.resolve(value)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fun reject(code: String, message: String, error: Throwable?) {
|
|
227
|
+
if (answered.compareAndSet(false, true)) promise.reject(code, message, error)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private class Call(private val raw: ReadableMap) {
|
|
232
|
+
val operationId = raw.requiredString("operationId")
|
|
233
|
+
val host = raw.optionalString("host")
|
|
234
|
+
val serialNumber = raw.optionalString("serialNumber")
|
|
235
|
+
val transport = raw.optionalString("transport").ifEmpty { "wifi" }
|
|
236
|
+
val transactionDate = raw.optionalString("transactionDate")
|
|
237
|
+
val originalTerminalId = raw.optionalString("originalTerminalId")
|
|
238
|
+
val merchantReferenceId = raw.optionalString("merchantReferenceId")
|
|
239
|
+
private val config = raw.config()
|
|
240
|
+
|
|
241
|
+
val isIpTransport get() = transport == "ethernet" || transport == "wifi"
|
|
242
|
+
val isWebService get() = transport == "webService" || transport == "web_service"
|
|
243
|
+
val isSupportedTransport get() = isIpTransport || isWebService
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The SDK session for this call's transport: TCP for ethernet/wifi, the
|
|
247
|
+
* hosted REST path for webService. Built lazily so an unsupported
|
|
248
|
+
* transport is answered as a typed failure before any session exists.
|
|
249
|
+
*/
|
|
250
|
+
val session: EcrOpenedSession by lazy {
|
|
251
|
+
val link = if (isWebService) {
|
|
252
|
+
EcrLink.WebService(
|
|
253
|
+
merchantId = config.merchantId,
|
|
254
|
+
terminalId = config.terminalId,
|
|
255
|
+
)
|
|
256
|
+
} else {
|
|
257
|
+
EcrLink.Lan(host = host, port = config.port)
|
|
258
|
+
}
|
|
259
|
+
val plan = EcrSessions.plan(link = link, config = config)
|
|
260
|
+
require(plan.isReady) { plan.issues.joinToString("; ") }
|
|
261
|
+
EcrSessions.open(
|
|
262
|
+
terminalSerial = serialNumber,
|
|
263
|
+
plan = plan,
|
|
264
|
+
logger = EcrLogger.None,
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fun requiredAmount(): BigDecimal {
|
|
269
|
+
val text = raw.requiredString("amount")
|
|
270
|
+
return try {
|
|
271
|
+
BigDecimal(text)
|
|
272
|
+
} catch (error: NumberFormatException) {
|
|
273
|
+
throw IllegalArgumentException("'$text' is not a decimal amount", error)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
fun requiredReceiptNumber(operation: String): String =
|
|
278
|
+
raw.optionalString("receiptNumber").ifBlank {
|
|
279
|
+
throw IllegalArgumentException("$operation needs the original receipt number")
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fun requiredOriginalReference(): String =
|
|
283
|
+
raw.optionalString("originalMerchantReference").ifBlank {
|
|
284
|
+
throw IllegalArgumentException("An inquiry by reference needs the original reference")
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
companion object {
|
|
289
|
+
const val NAME = NativeAmwalEcrSpec.NAME
|
|
290
|
+
private const val ECR_INVALID_ARGUMENT = "ECR_INVALID_ARGUMENT"
|
|
291
|
+
private const val ECR_INTERNAL = "ECR_INTERNAL"
|
|
292
|
+
private const val CANCELLED_MESSAGE =
|
|
293
|
+
"The caller cancelled while the terminal had the request. " +
|
|
294
|
+
"The transaction may still have completed — inquire before retrying."
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private fun ReadableMap.requiredString(key: String): String {
|
|
299
|
+
val value = optionalString(key).trim()
|
|
300
|
+
require(value.isNotEmpty()) { "'$key' is required and was missing" }
|
|
301
|
+
return value
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private fun ReadableMap.optionalString(key: String): String =
|
|
305
|
+
if (hasKey(key) && !isNull(key)) getString(key).orEmpty() else ""
|
|
306
|
+
|
|
307
|
+
private fun ReadableMap.config(): EcrConfig {
|
|
308
|
+
val source = if (hasKey("config") && !isNull("config")) getMap("config") else null
|
|
309
|
+
if (source == null) return EcrConfig()
|
|
310
|
+
|
|
311
|
+
return EcrConfig(
|
|
312
|
+
ecrId = source.optionalString("ecrId").ifEmpty { "ECR01" },
|
|
313
|
+
currencyCode = source.optionalString("currencyCode").ifEmpty { "512" },
|
|
314
|
+
minorUnitDigits = if (source.hasKey("minorUnitDigits")) source.getInt("minorUnitDigits") else 3,
|
|
315
|
+
port = if (source.hasKey("port")) source.getInt("port") else EcrConfig.DEFAULT_PORT,
|
|
316
|
+
connectTimeout = (if (source.hasKey("connectTimeoutMs")) source.getDouble("connectTimeoutMs").toLong() else 10_000L).milliseconds,
|
|
317
|
+
responseTimeout = (if (source.hasKey("responseTimeoutMs")) source.getDouble("responseTimeoutMs").toLong() else 120_000L).milliseconds,
|
|
318
|
+
probeTimeout = (if (source.hasKey("probeTimeoutMs")) source.getDouble("probeTimeoutMs").toLong() else 3_000L).milliseconds,
|
|
319
|
+
secureHashKey = source.optionalString("secureHashKey"),
|
|
320
|
+
autoInquireOnFailure = if (source.hasKey("autoInquireOnFailure")) source.getBoolean("autoInquireOnFailure") else true,
|
|
321
|
+
merchantId = source.optionalString("merchantId"),
|
|
322
|
+
terminalId = source.optionalString("terminalId"),
|
|
323
|
+
environment = EcrEnvironment.fromName(source.optionalString("environment")),
|
|
324
|
+
)
|
|
325
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.amwalecr
|
|
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.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import java.util.HashMap
|
|
9
|
+
|
|
10
|
+
class AmwalEcrPackage : BaseReactPackage() {
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
|
+
return if (name == AmwalEcrModule.NAME) {
|
|
13
|
+
AmwalEcrModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
|
|
20
|
+
mapOf(
|
|
21
|
+
AmwalEcrModule.NAME to ReactModuleInfo(
|
|
22
|
+
name = AmwalEcrModule.NAME,
|
|
23
|
+
className = AmwalEcrModule.NAME,
|
|
24
|
+
canOverrideExistingModule = false,
|
|
25
|
+
needsEagerInit = false,
|
|
26
|
+
isCxxModule = false,
|
|
27
|
+
isTurboModule = true
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
package com.amwalecr
|
|
2
|
+
|
|
3
|
+
import com.amwalpay.ecr.EcrInquiry
|
|
4
|
+
import com.amwalpay.ecr.EcrReceipt
|
|
5
|
+
import com.amwalpay.ecr.EcrResult
|
|
6
|
+
import com.amwalpay.ecr.EcrTransaction
|
|
7
|
+
import com.amwalpay.ecr.Failure
|
|
8
|
+
import com.facebook.react.bridge.Arguments
|
|
9
|
+
import com.facebook.react.bridge.WritableMap
|
|
10
|
+
|
|
11
|
+
internal object EcrMapping {
|
|
12
|
+
const val CANCELLED = "cancelled"
|
|
13
|
+
const val UNSUPPORTED = "unsupported"
|
|
14
|
+
|
|
15
|
+
fun result(result: EcrResult): WritableMap = when (result) {
|
|
16
|
+
is EcrResult.Approved -> Arguments.createMap().apply {
|
|
17
|
+
putString("outcome", "approved")
|
|
18
|
+
putString("merchantReferenceId", result.merchantReference)
|
|
19
|
+
putString("amount", result.amount)
|
|
20
|
+
putString("responseCode", result.responseCode)
|
|
21
|
+
putString("rrn", result.rrn)
|
|
22
|
+
putString("authCode", result.authCode)
|
|
23
|
+
putString("maskedPan", result.maskedPan)
|
|
24
|
+
putBoolean("partialApproval", result.partialApproval)
|
|
25
|
+
putString("requestedAmount", result.requestedAmount)
|
|
26
|
+
putString("raw", result.raw)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
is EcrResult.Declined -> Arguments.createMap().apply {
|
|
30
|
+
putString("outcome", "declined")
|
|
31
|
+
putString("merchantReferenceId", result.merchantReference)
|
|
32
|
+
putString("responseCode", result.responseCode)
|
|
33
|
+
putString("reason", result.reason)
|
|
34
|
+
putString("nextStep", result.nextStep.name)
|
|
35
|
+
putString("raw", result.raw)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
is EcrResult.Failed -> Arguments.createMap().apply {
|
|
39
|
+
putString("outcome", "failed")
|
|
40
|
+
putString("merchantReferenceId", result.merchantReference)
|
|
41
|
+
putMap("failure", failure(result.failure))
|
|
42
|
+
result.recovered?.let { putMap("recovered", inquiry(it)) }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fun inquiry(inquiry: EcrInquiry): WritableMap = when (inquiry) {
|
|
47
|
+
is EcrInquiry.Found -> Arguments.createMap().apply {
|
|
48
|
+
putString("outcome", "found")
|
|
49
|
+
putString("merchantReferenceId", inquiry.merchantReference)
|
|
50
|
+
putMap("transaction", transaction(inquiry.transaction))
|
|
51
|
+
putString("raw", inquiry.raw)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
is EcrInquiry.NotFound -> Arguments.createMap().apply {
|
|
55
|
+
putString("outcome", "notFound")
|
|
56
|
+
putString("merchantReferenceId", inquiry.merchantReference)
|
|
57
|
+
putString("reason", inquiry.reason)
|
|
58
|
+
putString("raw", inquiry.raw)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
is EcrInquiry.Failed -> Arguments.createMap().apply {
|
|
62
|
+
putString("outcome", "failed")
|
|
63
|
+
putString("merchantReferenceId", inquiry.merchantReference)
|
|
64
|
+
putMap("failure", failure(inquiry.failure))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fun receipt(receipt: EcrReceipt): WritableMap = when (receipt) {
|
|
69
|
+
is EcrReceipt.Ready -> Arguments.createMap().apply {
|
|
70
|
+
putString("outcome", "ready")
|
|
71
|
+
putString("merchantReferenceId", receipt.merchantReference)
|
|
72
|
+
putString("url", receipt.url)
|
|
73
|
+
putString("raw", receipt.raw)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
is EcrReceipt.Unavailable -> Arguments.createMap().apply {
|
|
77
|
+
putString("outcome", "unavailable")
|
|
78
|
+
putString("merchantReferenceId", receipt.merchantReference)
|
|
79
|
+
putString("reason", receipt.reason)
|
|
80
|
+
putString("raw", receipt.raw)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
is EcrReceipt.Failed -> Arguments.createMap().apply {
|
|
84
|
+
putString("outcome", "failed")
|
|
85
|
+
putString("merchantReferenceId", receipt.merchantReference)
|
|
86
|
+
putMap("failure", failure(receipt.failure))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fun failedResult(
|
|
91
|
+
merchantReferenceId: String,
|
|
92
|
+
kind: String,
|
|
93
|
+
message: String,
|
|
94
|
+
): WritableMap = Arguments.createMap().apply {
|
|
95
|
+
putString("outcome", "failed")
|
|
96
|
+
putString("merchantReferenceId", merchantReferenceId)
|
|
97
|
+
putMap("failure", Arguments.createMap().apply {
|
|
98
|
+
putString("kind", kind)
|
|
99
|
+
putString("message", message)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private fun transaction(value: EcrTransaction): WritableMap = Arguments.createMap().apply {
|
|
104
|
+
putString("transactionId", value.transactionId)
|
|
105
|
+
putString("stan", value.stan)
|
|
106
|
+
putString("type", value.type)
|
|
107
|
+
putString("status", value.status)
|
|
108
|
+
putString("amount", value.amount)
|
|
109
|
+
putString("totalAmount", value.totalAmount)
|
|
110
|
+
putString("currency", value.currency)
|
|
111
|
+
putString("transactionTime", value.transactionTime)
|
|
112
|
+
putString("maskedPan", value.maskedPan)
|
|
113
|
+
putString("cardHolderName", value.cardHolderName)
|
|
114
|
+
putString("rrn", value.rrn)
|
|
115
|
+
putString("authCode", value.authCode)
|
|
116
|
+
putString("batchId", value.batchId)
|
|
117
|
+
putString("terminalId", value.terminalId)
|
|
118
|
+
putBoolean("isRefunded", value.isRefunded)
|
|
119
|
+
putBoolean("canVoid", value.canVoid)
|
|
120
|
+
putBoolean("canRefund", value.canRefund)
|
|
121
|
+
putBoolean("partialApproval", value.partialApproval)
|
|
122
|
+
putString("authorizedAmount", value.authorizedAmount)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private fun failure(value: Failure): WritableMap = Arguments.createMap().apply {
|
|
126
|
+
putString("kind", when (value) {
|
|
127
|
+
is Failure.Unreachable -> "unreachable"
|
|
128
|
+
is Failure.Timeout -> "timeout"
|
|
129
|
+
is Failure.Malformed -> "malformed"
|
|
130
|
+
is Failure.ConnectionLost -> "connectionLost"
|
|
131
|
+
is Failure.Unauthenticated -> "unauthenticated"
|
|
132
|
+
})
|
|
133
|
+
putString("message", value.message)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Compatibility
|
|
2
|
+
|
|
3
|
+
| Layer | Version or minimum |
|
|
4
|
+
|---|---|
|
|
5
|
+
| `react-native-amwal-ecr` | 0.1.0 |
|
|
6
|
+
| React Native | 0.87.1 development baseline; peer dependency accepts supported host versions |
|
|
7
|
+
| Android provider | `com.amwal-pay:ecr-sdk:1.0.5` exactly |
|
|
8
|
+
| Android | API 24+, Java 17, compile SDK 36 |
|
|
9
|
+
| iOS provider | `AmwalECR ~> 0.2.1` |
|
|
10
|
+
| iOS | 15.1+ with the current React Native baseline |
|
|
11
|
+
| Protocol | ECR protocol version 1 |
|
|
12
|
+
|
|
13
|
+
Android is pinned exactly because changing how a payment outcome is interpreted
|
|
14
|
+
must be deliberate. iOS uses the provider's compatible patch line so CocoaPods
|
|
15
|
+
can resolve a host application that also uses `AmwalECR` directly.
|
|
16
|
+
|
|
17
|
+
The two provider versions must move together and only after their cross-platform
|
|
18
|
+
contract fixtures pass. `ethernet`, `wifi` and `webService` are supported —
|
|
19
|
+
the first two over TCP on the local network, `webService` through the Amwal
|
|
20
|
+
payment host over REST (`merchantId`, `terminalId` and `environment` in the
|
|
21
|
+
config select the terminal; receipt fetch stays local-only). `bluetooth` and
|
|
22
|
+
`usbCable` are reported as unsupported before anything is sent.
|
package/ios/AmwalEcr.h
ADDED
package/ios/AmwalEcr.mm
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#import "AmwalEcr.h"
|
|
2
|
+
|
|
3
|
+
@interface AmwalEcrImpl : NSObject
|
|
4
|
+
- (void)isReachable:(NSDictionary *)request
|
|
5
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
6
|
+
rejecter:(RCTPromiseRejectBlock)reject;
|
|
7
|
+
- (void)sale:(NSDictionary *)request
|
|
8
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
9
|
+
rejecter:(RCTPromiseRejectBlock)reject;
|
|
10
|
+
- (void)voidTransaction:(NSDictionary *)request
|
|
11
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
12
|
+
rejecter:(RCTPromiseRejectBlock)reject;
|
|
13
|
+
- (void)refund:(NSDictionary *)request
|
|
14
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
15
|
+
rejecter:(RCTPromiseRejectBlock)reject;
|
|
16
|
+
- (void)inquire:(NSDictionary *)request
|
|
17
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
18
|
+
rejecter:(RCTPromiseRejectBlock)reject;
|
|
19
|
+
- (void)inquireByReference:(NSDictionary *)request
|
|
20
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
21
|
+
rejecter:(RCTPromiseRejectBlock)reject;
|
|
22
|
+
- (void)receipt:(NSDictionary *)request
|
|
23
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
24
|
+
rejecter:(RCTPromiseRejectBlock)reject;
|
|
25
|
+
- (void)cancel:(NSString *)operationId
|
|
26
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
27
|
+
rejecter:(RCTPromiseRejectBlock)reject;
|
|
28
|
+
- (void)invalidate;
|
|
29
|
+
@end
|
|
30
|
+
|
|
31
|
+
@implementation AmwalEcr {
|
|
32
|
+
AmwalEcrImpl *_implementation;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
- (instancetype)init
|
|
36
|
+
{
|
|
37
|
+
self = [super init];
|
|
38
|
+
if (self) {
|
|
39
|
+
_implementation = [AmwalEcrImpl new];
|
|
40
|
+
}
|
|
41
|
+
return self;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
- (void)isReachable:(NSDictionary *)request
|
|
45
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
46
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
47
|
+
{
|
|
48
|
+
[_implementation isReachable:request resolver:resolve rejecter:reject];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
- (void)sale:(NSDictionary *)request
|
|
52
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
53
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
54
|
+
{
|
|
55
|
+
[_implementation sale:request resolver:resolve rejecter:reject];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
- (void)voidTransaction:(NSDictionary *)request
|
|
59
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
60
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
61
|
+
{
|
|
62
|
+
[_implementation voidTransaction:request resolver:resolve rejecter:reject];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
- (void)refund:(NSDictionary *)request
|
|
66
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
67
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
68
|
+
{
|
|
69
|
+
[_implementation refund:request resolver:resolve rejecter:reject];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
- (void)inquire:(NSDictionary *)request
|
|
73
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
74
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
75
|
+
{
|
|
76
|
+
[_implementation inquire:request resolver:resolve rejecter:reject];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
- (void)inquireByReference:(NSDictionary *)request
|
|
80
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
81
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
82
|
+
{
|
|
83
|
+
[_implementation inquireByReference:request resolver:resolve rejecter:reject];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
- (void)receipt:(NSDictionary *)request
|
|
87
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
88
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
89
|
+
{
|
|
90
|
+
[_implementation receipt:request resolver:resolve rejecter:reject];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
- (void)cancel:(NSString *)operationId
|
|
94
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
95
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
96
|
+
{
|
|
97
|
+
[_implementation cancel:operationId resolver:resolve rejecter:reject];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
- (void)invalidate
|
|
101
|
+
{
|
|
102
|
+
[_implementation invalidate];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
106
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
107
|
+
{
|
|
108
|
+
return std::make_shared<facebook::react::NativeAmwalEcrSpecJSI>(params);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
+ (NSString *)moduleName
|
|
112
|
+
{
|
|
113
|
+
return @"AmwalEcr";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
+ (BOOL)requiresMainQueueSetup
|
|
117
|
+
{
|
|
118
|
+
return NO;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@end
|