@pagopa/io-react-native-iso18013 0.6.0 → 0.8.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 (37) hide show
  1. package/IoReactNativeIso18013.podspec +1 -1
  2. package/README.md +1 -1
  3. package/android/build.gradle +3 -2
  4. package/android/src/main/AndroidManifest.xml +17 -0
  5. package/android/src/main/AndroidManifestNew.xml +17 -0
  6. package/android/src/main/java/com/ioreactnativeiso18013/Enums.kt +42 -0
  7. package/android/src/main/java/com/ioreactnativeiso18013/IoNfcEngagementService.kt +17 -0
  8. package/android/src/main/java/com/ioreactnativeiso18013/IoReactNativeIso18013Module.kt +329 -137
  9. package/android/src/main/res/values/strings.xml +5 -0
  10. package/android/src/main/res/xml/nfc_engagement_apdu_service.xml +9 -0
  11. package/android/src/test/java/com/ioreactnativeiso18013/IoReactNativeIso18013Test.kt +11 -13
  12. package/ios/IoReactNativeIso18013.mm +8 -9
  13. package/ios/IoReactNativeIso18013.swift +264 -191
  14. package/lib/module/cbor/cbor/README.md +2 -1
  15. package/lib/module/cbor/cose/README.md +6 -4
  16. package/lib/module/iso18013/iso18013-5/README.md +161 -41
  17. package/lib/module/iso18013/iso18013-5/error.js +1 -3
  18. package/lib/module/iso18013/iso18013-5/error.js.map +1 -1
  19. package/lib/module/iso18013/iso18013-5/index.js +1 -1
  20. package/lib/module/iso18013/iso18013-5/index.js.map +1 -1
  21. package/lib/module/iso18013/iso18013-5/proximity.js +29 -19
  22. package/lib/module/iso18013/iso18013-5/proximity.js.map +1 -1
  23. package/lib/module/iso18013/iso18013-7/README.md +1 -1
  24. package/lib/typescript/src/iso18013/iso18013-5/error.d.ts +4 -4
  25. package/lib/typescript/src/iso18013/iso18013-5/error.d.ts.map +1 -1
  26. package/lib/typescript/src/iso18013/iso18013-5/index.d.ts +1 -1
  27. package/lib/typescript/src/iso18013/iso18013-5/index.d.ts.map +1 -1
  28. package/lib/typescript/src/iso18013/iso18013-5/proximity.d.ts +32 -13
  29. package/lib/typescript/src/iso18013/iso18013-5/proximity.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/cbor/cbor/README.md +2 -1
  32. package/src/cbor/cose/README.md +6 -4
  33. package/src/iso18013/iso18013-5/README.md +161 -41
  34. package/src/iso18013/iso18013-5/error.ts +0 -2
  35. package/src/iso18013/iso18013-5/index.ts +3 -2
  36. package/src/iso18013/iso18013-5/proximity.ts +52 -23
  37. package/src/iso18013/iso18013-7/README.md +1 -1
@@ -10,56 +10,132 @@ import com.facebook.react.bridge.ReadableMap
10
10
  import com.facebook.react.bridge.ReadableType
11
11
  import com.facebook.react.bridge.WritableMap
12
12
  import it.pagopa.io.wallet.proximity.OpenID4VP
13
+ import it.pagopa.io.wallet.proximity.ProximityLogger
13
14
  import it.pagopa.io.wallet.proximity.bluetooth.BleRetrievalMethod
15
+ import it.pagopa.io.wallet.proximity.engagement.EngagementListener
16
+ import it.pagopa.io.wallet.proximity.nfc.NfcEngagementEvent
17
+ import it.pagopa.io.wallet.proximity.nfc.NfcEngagementEventBus
18
+ import it.pagopa.io.wallet.proximity.nfc.NfcEngagementService
19
+ import it.pagopa.io.wallet.proximity.nfc.NfcRetrievalMethod
14
20
  import it.pagopa.io.wallet.proximity.qr_code.QrEngagement
15
- import it.pagopa.io.wallet.proximity.qr_code.QrEngagementListener
16
21
  import it.pagopa.io.wallet.proximity.request.DocRequested
17
22
  import it.pagopa.io.wallet.proximity.response.ResponseGenerator
23
+ import it.pagopa.io.wallet.proximity.retrieval.sendErrorResponse
18
24
  import it.pagopa.io.wallet.proximity.session_data.SessionDataStatus
19
25
  import it.pagopa.io.wallet.proximity.wrapper.DeviceRetrievalHelperWrapper
26
+ import kotlinx.coroutines.CoroutineScope
27
+ import kotlinx.coroutines.Dispatchers
28
+ import kotlinx.coroutines.Job
29
+ import kotlinx.coroutines.SupervisorJob
30
+ import kotlinx.coroutines.launch
20
31
 
21
32
  class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
22
33
  ReactContextBaseJavaModule(reactContext) {
23
34
 
35
+ private var qrEngagement: QrEngagement? = null
36
+ private var deviceRetrievalHelper: DeviceRetrievalHelperWrapper? = null
37
+
38
+ private var nfcEventJob: Job? = null
39
+ private val nfcScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
40
+
41
+ private var sessionRetrievalMethod: RetrievalMethod? = null
42
+
43
+ // Session transcript received by the NFC engagement, stored for the current session
44
+ private var sessionTranscript: ByteArray? = null
45
+
46
+ init {
47
+ ProximityLogger.enabled = BuildConfig.DEBUG
48
+ }
49
+
24
50
  override fun getName(): String {
25
51
  return NAME
26
52
  }
27
53
 
28
- private var qrEngagement: QrEngagement? = null
29
- private var deviceRetrievalHelper: DeviceRetrievalHelperWrapper? = null
54
+ override fun invalidate() {
55
+ super.invalidate()
56
+ nfcEventJob?.cancel()
57
+ nfcEventJob = null
58
+ nfcScope.coroutineContext[Job]?.cancel()
59
+ currentActivity?.let { NfcEngagementService.disable(it) }
60
+ }
30
61
 
31
62
  /**
32
- * Starts the proximity flow by allocating the necessary resources and initializing the Bluetooth stack.
63
+ * Starts the NFC proximity flow by allocating the necessary resources, enabling NFC
64
+ * engagement/HCE and initializing the Bluetooth stack (if required).
33
65
  * Resolves to true or rejects with an error code defined in [ModuleErrorCodes].
34
66
  * @param peripheralMode whether the device is in peripheral mode. Defaults to true
35
67
  * @param centralClientMode whether the device is in central client mode. Defaults to false
36
68
  * @param clearBleCache whether the BLE cache should be cleared. Defaults to true
37
69
  * @param certificates two-dimensional array of base64 strings representing DER encoded X.509 certificate which are used to authenticate the verifier app
70
+ * @param enagementModes array of strings representing the engagement modes (e.g. QR_CODE, BLE) to be used.
71
+ * @param retrievalMethods array of strings representing the retrieval methods (e.g. NFC, BLE) to be used.
38
72
  * @param promise the promise which will be resolved in case of success or rejected in case of failure.
39
73
  */
40
74
  @ReactMethod
41
- fun start(
75
+ fun startEngagement(
42
76
  peripheralMode: Boolean,
43
77
  centralClientMode: Boolean,
44
78
  clearBleCache: Boolean,
45
79
  certificates: ReadableArray,
80
+ enagementModes: ReadableArray,
81
+ retrievalMethods: ReadableArray,
46
82
  promise: Promise
47
83
  ) {
48
84
  try {
49
- val retrievalMethod = BleRetrievalMethod(
50
- peripheralServerMode = peripheralMode,
51
- centralClientMode = centralClientMode,
52
- clearBleCache = clearBleCache
53
- )
85
+ val activity = checkNotNull(currentActivity) { "No activity available" }
54
86
 
55
87
  val certificatesList = parseCertificates(certificates)
56
- qrEngagement = QrEngagement.build(reactApplicationContext, listOf(retrievalMethod)).apply {
57
- if (certificatesList.isNotEmpty()) {
58
- withReaderTrustStore(certificatesList)
88
+ val parsedEngagementModes = parseEngagementModes(enagementModes)
89
+ val parsedRetrievalMethods = parseRetrievalMethods(retrievalMethods)
90
+
91
+ val methods = buildList {
92
+ if (parsedRetrievalMethods.any { it == RetrievalMethod.BLE }) {
93
+ add(
94
+ BleRetrievalMethod(
95
+ peripheralServerMode = peripheralMode,
96
+ centralClientMode = centralClientMode,
97
+ clearBleCache = clearBleCache
98
+ )
99
+ )
100
+ }
101
+ if (parsedRetrievalMethods.any { it == RetrievalMethod.NFC }) {
102
+ add(NfcRetrievalMethod())
103
+ }
104
+ }
105
+
106
+ // NFC engagement
107
+
108
+ if (parsedEngagementModes.any { it == EngagementMode.NFC }) {
109
+ val readerTrustStore = certificatesList.takeIf { it.isNotEmpty() }
110
+ ?.map { chain -> chain.map<ByteArray, Any> { it } }
111
+
112
+ check(
113
+ NfcEngagementEventBus.setupNfcService(
114
+ retrievalMethods = methods, readerTrustStore = readerTrustStore
115
+ )
116
+ ) { "Unable to setup NFC engagement service" }
117
+
118
+ val status = NfcEngagementService.enable(
119
+ activity = activity, preferredNfcEngSerCls = IoNfcEngagementService::class.java
120
+ )
121
+ check(status.canWork()) { "HCE not available: $status" }
122
+ setupNfcHandler()
123
+ sendEvent("onNfcStarted", null)
124
+ }
125
+
126
+ // QR Code engagement
127
+
128
+ if (parsedEngagementModes.any { it == EngagementMode.QR_CODE }) {
129
+ qrEngagement = QrEngagement.build(reactApplicationContext, methods).apply {
130
+ if (certificatesList.isNotEmpty()) {
131
+ withReaderTrustStore(certificatesList)
132
+ }
59
133
  }
134
+ qrEngagement?.configure()
135
+ setupProximityHandler()
136
+ emitQrCodeString()
60
137
  }
61
- qrEngagement?.configure()
62
- setupProximityHandler()
138
+
63
139
  promise.resolve(true)
64
140
  } catch (e: Exception) {
65
141
  promise.reject(ModuleErrorCodes.START_ERROR, e.message, e)
@@ -67,22 +143,15 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
67
143
  }
68
144
 
69
145
  /**
70
- * Creates a QR code to be scanned in order to initialize the presentation.
71
- * Resolves with a string containing the QR code or rejects with an error code defined in [ModuleErrorCodes].
72
- * @param promise the promise which will be resolved in case of success or rejected in case of failure.
146
+ * Generates and send QR Code string payload to React Native via `RCTEventEmitter`.
73
147
  */
74
- @ReactMethod
75
- fun getQrCodeString(promise: Promise) {
76
- try {
77
- qrEngagement?.let {
78
- val qrCodeString = it.getQrCodeString()
79
- promise.resolve(qrCodeString)
80
- } ?: run {
81
- promise.reject(ModuleErrorCodes.QR_ENGAGEMENT_NOT_DEFINED,NOT_INITIALIZED_ERROR_MESSAGE)
82
- }
83
- } catch (e: Exception) {
84
- promise.reject(ModuleErrorCodes.GET_QR_CODE_ERROR, e.message, e)
85
- }
148
+ private fun emitQrCodeString() {
149
+ val qrCodeString =
150
+ checkNotNull(qrEngagement?.getQrCodeString()) { NOT_INITIALIZED_ERROR_MESSAGE }
151
+
152
+ val data: WritableMap = Arguments.createMap()
153
+ data.putString("data", qrCodeString)
154
+ sendEvent("onQrCodeString", data)
86
155
  }
87
156
 
88
157
  /**
@@ -96,13 +165,21 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
96
165
  fun close(promise: Promise) {
97
166
  try {
98
167
  qrEngagement?.close()
168
+ qrEngagement = null
99
169
  deviceRetrievalHelper?.disconnect()
170
+ deviceRetrievalHelper = null
171
+ sessionRetrievalMethod = null
172
+ sessionTranscript = null
173
+ nfcEventJob?.cancel()
174
+ nfcEventJob = null
175
+ currentActivity?.let { NfcEngagementService.disable(it) }
100
176
  promise.resolve(true)
101
177
  } catch (e: Exception) {
102
- promise.reject(ModuleErrorCodes.CLOSE_ERROR, message=e.message, e)
178
+ promise.reject(ModuleErrorCodes.CLOSE_ERROR, message = e.message, e)
103
179
  }
104
180
  }
105
181
 
182
+
106
183
  /**
107
184
  * Sends an error response during the presentation according to the SessionData status codes
108
185
  * defined in table 20 of the ISO18013-5 standard.
@@ -118,19 +195,20 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
118
195
  @ReactMethod
119
196
  fun sendErrorResponse(code: Double, promise: Promise) {
120
197
  try {
121
- qrEngagement?.let { it ->
122
- val sessionDataStatus = SessionDataStatus.entries.find { it.value == code.toLong() }
123
- if (sessionDataStatus != null) {
124
- it.sendErrorResponse(sessionDataStatus)
125
- promise.resolve(true)
126
- } else {
127
- promise.reject(ModuleErrorCodes.SEND_ERROR_RESPONSE_ERROR, message="Invalid status code")
128
- }
198
+ deviceRetrievalHelper?.let { drh ->
199
+ val sessionDataStatus =
200
+ checkNotNull(SessionDataStatus.entries.find { it.value == code.toLong() }) {
201
+ "Invalid status code"
202
+ }
203
+ drh.sendErrorResponse(sessionDataStatus)
204
+ promise.resolve(true)
129
205
  } ?: run {
130
- promise.reject(ModuleErrorCodes.QR_ENGAGEMENT_NOT_DEFINED, message=NOT_INITIALIZED_ERROR_MESSAGE)
206
+ promise.reject(
207
+ ModuleErrorCodes.DRH_NOT_DEFINED, message = NOT_INITIALIZED_ERROR_MESSAGE
208
+ )
131
209
  }
132
210
  } catch (e: Exception) {
133
- promise.reject(ModuleErrorCodes.SEND_ERROR_RESPONSE_ERROR, message=e.message, e)
211
+ promise.reject(ModuleErrorCodes.SEND_ERROR_RESPONSE_ERROR, message = e.message, e)
134
212
  }
135
213
  }
136
214
 
@@ -162,34 +240,48 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
162
240
  */
163
241
  @ReactMethod
164
242
  fun generateResponse(
165
- documents: ReadableArray,
166
- acceptedFields: ReadableMap,
167
- promise: Promise
243
+ documents: ReadableArray, acceptedFields: ReadableMap, promise: Promise
168
244
  ) {
169
245
  try {
170
- deviceRetrievalHelper?.let { devHelper ->
171
- // Get the DocRequested list and if it's empty then reject the promise and return
172
- val docRequestedList = parseDocRequested(documents)
173
-
174
- val sessionTranscript = devHelper.sessionTranscript()
175
- val responseGenerator = ResponseGenerator(sessionTranscript)
176
- val parsedAcceptedFields = parseAcceptedFields(acceptedFields)
177
- responseGenerator.createResponse(docRequestedList,
178
- parsedAcceptedFields,
179
- object : ResponseGenerator.Response {
180
- override fun onResponseGenerated(response: ByteArray) {
181
- promise.resolve(Base64Utils.encodeBase64(response))
182
- }
246
+ when (sessionRetrievalMethod) {
247
+
248
+ // For NFC retrievals we use the session transcript received during the device connection event
249
+ RetrievalMethod.NFC -> {
250
+ val sessionTranscript = checkNotNull(sessionTranscript) { "Session transcript is null" }
251
+ createAndResolveResponse(
252
+ sessionTranscript = sessionTranscript,
253
+ documents = documents,
254
+ acceptedFields = acceptedFields,
255
+ errorCode = ModuleErrorCodes.GENERATE_RESPONSE_ERROR,
256
+ promise = promise
257
+ )
258
+ }
183
259
 
184
- override fun onError(message: String) {
185
- promise.reject(ModuleErrorCodes.GENERATE_RESPONSE_ERROR, message)
186
- }
187
- })
188
- } ?: run {
189
- promise.reject(ModuleErrorCodes.DRH_NOT_DEFINED, message=NOT_INITIALIZED_ERROR_MESSAGE)
260
+ // For BLE retrievals we use the session transcript obtained from the DeviceRetrievalHelper
261
+ RetrievalMethod.BLE -> {
262
+ deviceRetrievalHelper?.let { drh ->
263
+ createAndResolveResponse(
264
+ sessionTranscript = drh.sessionTranscript(),
265
+ documents = documents,
266
+ acceptedFields = acceptedFields,
267
+ errorCode = ModuleErrorCodes.GENERATE_RESPONSE_ERROR,
268
+ promise = promise
269
+ )
270
+ } ?: run {
271
+ promise.reject(
272
+ ModuleErrorCodes.DRH_NOT_DEFINED, message = NOT_INITIALIZED_ERROR_MESSAGE
273
+ )
274
+ }
275
+ }
276
+
277
+ else -> {
278
+ promise.reject(
279
+ ModuleErrorCodes.GENERATE_RESPONSE_ERROR, message = "Invalid retrieval method"
280
+ )
281
+ }
190
282
  }
191
283
  } catch (e: Exception) {
192
- promise.reject(ModuleErrorCodes.GENERATE_RESPONSE_ERROR, message=e.message, e)
284
+ promise.reject(ModuleErrorCodes.GENERATE_RESPONSE_ERROR, message = e.message, e)
193
285
  }
194
286
  }
195
287
 
@@ -203,14 +295,37 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
203
295
  @ReactMethod
204
296
  fun sendResponse(response: String, promise: Promise) {
205
297
  try {
206
- qrEngagement?.let { qrEng ->
207
- val responseBytes = Base64Utils.decodeBase64(response)
208
- qrEng.sendResponse(responseBytes)
209
- promise.resolve(true)
210
- }
211
- ?: run {
212
- promise.reject(ModuleErrorCodes.QR_ENGAGEMENT_NOT_DEFINED, message=NOT_INITIALIZED_ERROR_MESSAGE)
298
+ val responseBytes = Base64Utils.decodeBase64(response)
299
+
300
+ when (sessionRetrievalMethod) {
301
+
302
+ // For NFC retrievals we use NfcEngagementEventBus to send the response over NFC
303
+ RetrievalMethod.NFC -> {
304
+ check(
305
+ NfcEngagementEventBus.sendDocumentResponse(response = responseBytes)
306
+ ) { "Unable to send response over NFC" }
307
+ promise.resolve(true)
213
308
  }
309
+
310
+ // For BLE retrievals we use the DeviceRetrievalHelper to send the response over BLE
311
+ RetrievalMethod.BLE -> {
312
+ deviceRetrievalHelper?.let { drh ->
313
+ drh.sendResponse(responseBytes, 0L)
314
+ promise.resolve(true)
315
+ } ?: run {
316
+ promise.reject(
317
+ ModuleErrorCodes.DRH_NOT_DEFINED, message = NOT_INITIALIZED_ERROR_MESSAGE
318
+ )
319
+ }
320
+ }
321
+
322
+ else -> {
323
+ promise.reject(
324
+ ModuleErrorCodes.SEND_RESPONSE_ERROR, message = "Invalid retrieval method"
325
+ )
326
+ }
327
+ }
328
+
214
329
  } catch (e: Exception) {
215
330
  promise.reject(ModuleErrorCodes.SEND_RESPONSE_ERROR, e.message, e)
216
331
  }
@@ -246,37 +361,25 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
246
361
  */
247
362
  @ReactMethod
248
363
  fun generateOID4VPDeviceResponse(
249
- clientId: String, responseUri: String, authorizationRequestNonce: String,
250
- mdocGeneratedNonce: String, documents: ReadableArray,
251
- acceptedFields: ReadableMap, promise: Promise
364
+ clientId: String,
365
+ responseUri: String,
366
+ authorizationRequestNonce: String,
367
+ mdocGeneratedNonce: String,
368
+ documents: ReadableArray,
369
+ acceptedFields: ReadableMap,
370
+ promise: Promise
252
371
  ) {
253
372
  try {
254
- val sessionTranscript =
255
- OpenID4VP(
256
- clientId,
257
- responseUri,
258
- authorizationRequestNonce,
259
- mdocGeneratedNonce
260
- ).createSessionTranscript()
261
-
262
- val documentsParsed =
263
- parseDocRequested(documents)
264
-
265
- val parsedAcceptedFields = parseAcceptedFields(acceptedFields)
266
-
267
- val responseGenerator = ResponseGenerator(sessionTranscript)
268
- responseGenerator.createResponse(
269
- documentsParsed,
270
- parsedAcceptedFields,
271
- object : ResponseGenerator.Response {
272
- override fun onResponseGenerated(response: ByteArray) {
273
- promise.resolve(Base64Utils.encodeBase64(response))
274
- }
275
-
276
- override fun onError(message: String) {
277
- promise.reject(ModuleErrorCodes.GENERATE_OID4VP_RESPONSE_ERROR, message)
278
- }
279
- }
373
+ val sessionTranscript = OpenID4VP(
374
+ clientId, responseUri, authorizationRequestNonce, mdocGeneratedNonce
375
+ ).createSessionTranscript()
376
+
377
+ createAndResolveResponse(
378
+ sessionTranscript = sessionTranscript,
379
+ documents = documents,
380
+ acceptedFields = acceptedFields,
381
+ errorCode = ModuleErrorCodes.GENERATE_OID4VP_RESPONSE_ERROR,
382
+ promise = promise
280
383
  )
281
384
  } catch (e: Exception) {
282
385
  promise.reject(ModuleErrorCodes.GENERATE_OID4VP_RESPONSE_ERROR, e.message, e)
@@ -293,12 +396,12 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
293
396
  * onError: Emitted when an error occurs. Carries a payload containing the error data.
294
397
  */
295
398
  private fun setupProximityHandler() {
296
- qrEngagement?.withListener(object : QrEngagementListener {
399
+ qrEngagement?.withListener(object : EngagementListener {
297
400
  /**
298
401
  * This event currently doesn't get called due to an issue with the underlying native library.
299
402
  */
300
403
  override fun onDeviceConnecting() {
301
- sendEvent("onDeviceConnecting", "")
404
+ sendEvent("onDeviceConnecting", null)
302
405
  }
303
406
 
304
407
  override fun onDeviceConnected(deviceRetrievalHelper: DeviceRetrievalHelperWrapper) {
@@ -313,8 +416,11 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
313
416
  }
314
417
 
315
418
  override fun onDocumentRequestReceived(request: String?, sessionsTranscript: ByteArray) {
419
+ sessionRetrievalMethod = RetrievalMethod.BLE
420
+
316
421
  val data: WritableMap = Arguments.createMap()
317
- data.putString("data", request)
422
+ data.putString("data", request ?: "")
423
+ data.putString("retrievalMethod", sessionRetrievalMethod?.bridgeValue)
318
424
  sendEvent("onDocumentRequestReceived", data)
319
425
  }
320
426
 
@@ -324,6 +430,90 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
324
430
  })
325
431
  }
326
432
 
433
+ /**
434
+ * Sets the NFC handler along with the possible dispatched events and their callbacks.
435
+ * The events are then sent to React Native via `RCTEventEmitter`.
436
+ * onDeviceConnecting: Emitted when the device is connecting to the verifier app.
437
+ * onDeviceConnected: Emitted when the device is connected to the verifier app.
438
+ * onDocumentRequestReceived: Emitted when a document request is received from the verifier app. Carries a payload containing the request data.
439
+ * onDeviceDisconnected: Emitted when the device is disconnected from the verifier app.
440
+ * onError: Emitted when an error occurs. Carries a payload containing the error data.
441
+ * onNfcStopped: Emitted when the NFC handler is stopped.
442
+ */
443
+ private fun setupNfcHandler() {
444
+ nfcEventJob?.cancel()
445
+ nfcEventJob = nfcScope.launch {
446
+ NfcEngagementEventBus.events.collect { event ->
447
+ when (event) {
448
+ is NfcEngagementEvent.Connecting -> {
449
+ sendEvent("onDeviceConnecting", null)
450
+ }
451
+
452
+ is NfcEngagementEvent.Connected -> {
453
+ deviceRetrievalHelper = event.device
454
+ sendEvent("onDeviceConnected", null)
455
+ }
456
+
457
+ is NfcEngagementEvent.Error -> {
458
+ val data: WritableMap = Arguments.createMap()
459
+ data.putString("error", event.error.message ?: "")
460
+ sendEvent("onError", data)
461
+ }
462
+
463
+ is NfcEngagementEvent.Disconnected -> sendEvent(
464
+ "onDeviceDisconnected", null
465
+ )
466
+
467
+ is NfcEngagementEvent.DocumentRequestReceived -> {
468
+ sessionRetrievalMethod = if (event.onlyNfc) RetrievalMethod.NFC else RetrievalMethod.BLE
469
+ sessionTranscript = event.sessionTranscript
470
+
471
+ val data: WritableMap = Arguments.createMap()
472
+ data.putString("data", event.request)
473
+ data.putString("retrievalMethod", sessionRetrievalMethod?.bridgeValue)
474
+ sendEvent("onDocumentRequestReceived", data)
475
+ }
476
+
477
+ is NfcEngagementEvent.NotSupported -> sendEvent("onNfcStopped", null)
478
+
479
+ is NfcEngagementEvent.NfcOnlyEventListener -> { /* NOT HANDLED */
480
+ }
481
+
482
+ is NfcEngagementEvent.DocumentSent -> sendEvent("onDeviceDisconnected", null)
483
+ }
484
+ }
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Utility function that generates the response using the ResponseGeneration from a session
490
+ * transcript
491
+ * @param sessionTranscript the session transcript to be used to generate the response
492
+ * @param documents the documents to be included in the response
493
+ * @param acceptedFields the accepted fields to be presented
494
+ * @param errorCode the error code to be used in case of error
495
+ * @param promise the promise which will be resolved in case of success or rejected in case of failure.
496
+ */
497
+ private fun createAndResolveResponse(
498
+ sessionTranscript: ByteArray,
499
+ documents: ReadableArray,
500
+ acceptedFields: ReadableMap,
501
+ errorCode: String,
502
+ promise: Promise
503
+ ) {
504
+ val docRequestedList = parseDocRequested(documents)
505
+ val parsedAcceptedFields = parseAcceptedFields(acceptedFields)
506
+ ResponseGenerator(sessionTranscript).createResponse(
507
+ docRequestedList, parsedAcceptedFields, object : ResponseGenerator.Response {
508
+ override fun onResponseGenerated(response: ByteArray) =
509
+ promise.resolve(Base64Utils.encodeBase64(response))
510
+
511
+ override fun onError(message: String) {
512
+ promise.reject(errorCode, message)
513
+ }
514
+ })
515
+ }
516
+
327
517
  /**
328
518
  * Wrapper function to send an event via `RCTEventEmitter`
329
519
  * @param eventName - The event name
@@ -334,31 +524,30 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
334
524
  .emit(eventName, data)
335
525
  }
336
526
 
527
+ /**
528
+ * Keep: Required for RN built in Event Emitter Calls.
529
+ * This fixes the warning: `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method
530
+ */
337
531
  @ReactMethod
338
- fun addListener(eventName: String?) {
339
- /* Keep: Required for RN built in Event Emitter Calls.
340
- This fixes the warning: `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method
341
- */
342
- }
532
+ fun addListener(eventName: String?) = Unit
343
533
 
534
+ /**
535
+ * Keep: Required for RN built in Event Emitter Calls.
536
+ * This fixes the warning: `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method
537
+ */
344
538
  @ReactMethod
345
- fun removeListeners(count: Int?) {
346
- /*Keep: Required for RN built in Event Emitter Calls.
347
- This fixes the warning: `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method
348
- */
349
- }
539
+ fun removeListeners(count: Int?) = Unit
350
540
 
351
541
  companion object {
352
542
  const val NAME = "IoReactNativeIso18013"
353
- const val NOT_INITIALIZED_ERROR_MESSAGE = "Resources not initialized properly, call the start method before this one."
543
+ const val NOT_INITIALIZED_ERROR_MESSAGE =
544
+ "Resources not initialized properly, call the start method before this one."
354
545
 
355
546
  // Errors which this module uses to reject a promise
356
547
  private object ModuleErrorCodes {
357
548
  // ISO18013-5 related errors
358
549
  const val DRH_NOT_DEFINED = "DRH_NOT_DEFINED"
359
- const val QR_ENGAGEMENT_NOT_DEFINED = "QR_ENGAGEMENT_NOT_DEFINED"
360
550
  const val START_ERROR = "START_ERROR"
361
- const val GET_QR_CODE_ERROR = "GET_QR_CODE_ERROR"
362
551
  const val GENERATE_RESPONSE_ERROR = "GENERATE_RESPONSE_ERROR"
363
552
  const val SEND_RESPONSE_ERROR = "SEND_RESPONSE_ERROR"
364
553
  const val SEND_ERROR_RESPONSE_ERROR = "SEND_ERROR_RESPONSE_ERROR"
@@ -386,10 +575,11 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
386
575
  * },
387
576
  * {...}
388
577
  * }
389
- * @throw IllegalArgumentException if the ReadableMap is not consistent or contains an invalid value
578
+ * @throw IllegalArgumentException if the value provided is not a ReadableMap which contains
579
+ * at least a credential type. If a namespace doesn't contain at least one value.
390
580
  * @returns String representation of [acceptedFields]
391
581
  */
392
- fun parseAcceptedFields(acceptedFields: ReadableMap): String {
582
+ internal fun parseAcceptedFields(acceptedFields: ReadableMap): String {
393
583
  try {
394
584
  // Loop for each credential and throw if something different than map is found
395
585
  acceptedFields.entryIterator.forEach { credentialEntry ->
@@ -399,10 +589,7 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
399
589
  throw IllegalArgumentException("Credential '$credentialName' must be a map")
400
590
  }
401
591
 
402
- // If no namespace is found then throw
403
- if(!credentialValue.entryIterator.hasNext()){
404
- throw IllegalArgumentException("Credential '$credentialName' must define at least one namespace")
405
- }
592
+ // We don't check for a namespace, if there's none that's a valid structure and thus should pass the validation
406
593
 
407
594
  // Loop for each namespace in credential and throw if something different than map is found
408
595
  credentialValue.entryIterator.forEach { namespaceEntry ->
@@ -413,7 +600,7 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
413
600
  }
414
601
 
415
602
  // If no field is found then throw
416
- if(!namespaceValue.entryIterator.hasNext()){
603
+ if (!namespaceValue.entryIterator.hasNext()) {
417
604
  throw IllegalArgumentException("Credential '$credentialName' with namespace `$namespaceName` must define at least one field")
418
605
  }
419
606
 
@@ -434,6 +621,7 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
434
621
  }
435
622
  }
436
623
 
624
+
437
625
  /**
438
626
  * Utility function which extracts the document shape we expect to receive from the bridge
439
627
  * in the one expected by {DocRequested}.
@@ -453,18 +641,15 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
453
641
  val issuerSignedContentStr = entry.getString("issuerSignedContent")
454
642
  val docType = entry.getString("docType")
455
643
 
456
- if (
457
- alias == null || entry.getType("alias") != ReadableType.String ||
458
- issuerSignedContentStr == null || entry.getType("issuerSignedContent") != ReadableType.String ||
459
- docType == null || entry.getType("docType") != ReadableType.String
644
+ if (alias == null || entry.getType("alias") != ReadableType.String || issuerSignedContentStr == null || entry.getType(
645
+ "issuerSignedContent"
646
+ ) != ReadableType.String || docType == null || entry.getType("docType") != ReadableType.String
460
647
  ) throw IllegalArgumentException("Unable to decode the provided documents at index $i")
461
648
 
462
649
  val issuerSignedContent = Base64Utils.decodeBase64AndBase64Url(issuerSignedContentStr)
463
650
 
464
651
  DocRequested(
465
- issuerSignedContent,
466
- alias,
467
- docType
652
+ issuerSignedContent, alias, docType
468
653
  )
469
654
  }.toTypedArray()
470
655
  } catch (e: Exception) {
@@ -484,18 +669,22 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
484
669
  * can throw and if it does rethrow an exception with information on its position
485
670
  */
486
671
  (0 until certificates.size()).mapNotNull { chainIndex ->
487
- val chain = runCatching { certificates.getArray(chainIndex) }
488
- .getOrElse { throw IllegalArgumentException("Certificate chain at $chainIndex is not an array", it) }
489
- ?: throw IllegalArgumentException("Certificate chain at index $chainIndex is null")
672
+ val chain = runCatching { certificates.getArray(chainIndex) }.getOrElse {
673
+ throw IllegalArgumentException(
674
+ "Certificate chain at $chainIndex is not an array", it
675
+ )
676
+ } ?: throw IllegalArgumentException("Certificate chain at index $chainIndex is null")
490
677
 
491
678
  /**
492
679
  * Map each chain certificate and remove null entries. On each certificate call the getString
493
680
  * method which can throw and if it does rethrow an exception with information on its position
494
681
  */
495
682
  (0 until chain.size()).mapNotNull { certIndex ->
496
- val base64 = runCatching { chain.getString(certIndex) }
497
- .getOrElse { throw IllegalArgumentException("Failed to get certificate string at chain $chainIndex, cert $certIndex", it) }
498
- ?: throw java.lang.IllegalArgumentException("Certificate at index $certIndex is null")
683
+ val base64 = runCatching { chain.getString(certIndex) }.getOrElse {
684
+ throw IllegalArgumentException(
685
+ "Failed to get certificate string at chain $chainIndex, cert $certIndex", it
686
+ )
687
+ } ?: throw java.lang.IllegalArgumentException("Certificate at index $certIndex is null")
499
688
 
500
689
  /**
501
690
  * Decode the base64 string for each mapped certificate and if an error occurs rethrow
@@ -504,7 +693,10 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
504
693
  runCatching {
505
694
  Base64Utils.decodeBase64(base64)
506
695
  }.getOrElse {
507
- throw IllegalArgumentException("Certificate at index $certIndex in the chain at index $chainIndex is not a valid base64 string", it)
696
+ throw IllegalArgumentException(
697
+ "Certificate at index $certIndex in the chain at index $chainIndex is not a valid base64 string",
698
+ it
699
+ )
508
700
  }
509
701
  }
510
702
  }
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <string name="nfc_engagement_service_desc">NFC Engagement Service</string>
4
+ <string name="nfc_engagement_aid_group_desc">ISO 18013-5 NFC Engagement</string>
5
+ </resources>