@tapayadot/accept-react-native 1.3.3 → 1.14.1

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 (47) hide show
  1. package/README.md +196 -363
  2. package/android/build.gradle +23 -34
  3. package/android/src/main/java/com/tapaya/acceptsdkreactnative/AcceptSdkReactNativeModule.kt +191 -673
  4. package/android/src/main/java/com/tapaya/acceptsdkreactnative/Errors.kt +81 -0
  5. package/android/src/main/java/com/tapaya/acceptsdkreactnative/Parsing.kt +166 -0
  6. package/android/src/main/java/com/tapaya/acceptsdkreactnative/Serialization.kt +216 -0
  7. package/app.json +2 -2
  8. package/app.plugin.js +1 -1
  9. package/build/Accept.d.ts +255 -0
  10. package/build/Accept.d.ts.map +1 -0
  11. package/build/Accept.js +387 -0
  12. package/build/Accept.js.map +1 -0
  13. package/build/Accept.types.d.ts +352 -0
  14. package/build/Accept.types.d.ts.map +1 -0
  15. package/build/Accept.types.js +9 -0
  16. package/build/Accept.types.js.map +1 -0
  17. package/build/AcceptError.d.ts +53 -0
  18. package/build/AcceptError.d.ts.map +1 -0
  19. package/build/AcceptError.js +107 -0
  20. package/build/AcceptError.js.map +1 -0
  21. package/build/AcceptSdkReactNativeModule.d.ts +67 -26
  22. package/build/AcceptSdkReactNativeModule.d.ts.map +1 -1
  23. package/build/AcceptSdkReactNativeModule.js +53 -3
  24. package/build/AcceptSdkReactNativeModule.js.map +1 -1
  25. package/build/index.d.ts +7 -237
  26. package/build/index.d.ts.map +1 -1
  27. package/build/index.js +7 -254
  28. package/build/index.js.map +1 -1
  29. package/build/utils.d.ts.map +1 -1
  30. package/build/utils.js +2 -2
  31. package/build/utils.js.map +1 -1
  32. package/expo-module.config.json +1 -6
  33. package/package.json +8 -12
  34. package/plugin/withAcceptSdk.js +45 -0
  35. package/build/AcceptSdkReactNative.types.d.ts +0 -259
  36. package/build/AcceptSdkReactNative.types.d.ts.map +0 -1
  37. package/build/AcceptSdkReactNative.types.js +0 -2
  38. package/build/AcceptSdkReactNative.types.js.map +0 -1
  39. package/ios/.xcode.env +0 -11
  40. package/ios/AcceptSdkReactNative.podspec +0 -33
  41. package/ios/AcceptSdkReactNativeAppDelegate.swift +0 -13
  42. package/ios/AcceptSdkReactNativeModule.swift +0 -749
  43. package/ios/Podfile +0 -63
  44. package/ios/Podfile.properties.json +0 -4
  45. package/ios/acceptsdkreactnative.xcodeproj/project.pbxproj +0 -540
  46. package/ios/acceptsdkreactnative.xcodeproj/xcshareddata/xcschemes/acceptsdkreactnative.xcscheme +0 -88
  47. package/plugin/withCustomRepoModule.js +0 -96
@@ -1,744 +1,262 @@
1
1
  package com.tapaya.acceptsdkreactnative
2
2
 
3
3
  import android.util.Base64
4
- import com.google.gson.Gson
5
4
  import com.tapaya.accept.Accept
6
- import com.tapaya.accept.AcceptDemoApi
7
- import com.tapaya.accept.external.IdentityData
8
- import com.tapaya.core.model.external.AcceptEnvironment
9
- import com.tapaya.core.model.external.AcceptUiBehavior
10
- import com.tapaya.core.model.external.BusinessType
11
- import com.tapaya.core.model.external.Currency
12
- import com.tapaya.core.model.external.NfcIndicator
13
- import com.tapaya.core.model.external.NfcPosition
14
- import com.tapaya.core.model.external.NfcZone
15
- import com.tapaya.core.model.external.PaymentMethod
16
- import com.tapaya.core.model.external.RefundReason
17
- import com.tapaya.core.model.external.exception.AcceptException
18
- import com.tapaya.core.model.external.intent.CardChargeIntent
19
- import com.tapaya.core.model.external.intent.CardRefundIntent
20
- import com.tapaya.core.model.external.intent.CertisInstantPaymentIntent
21
- import com.tapaya.core.model.external.intent.SepaInstantPaymentIntent
22
- import com.tapaya.core.model.external.result.AbstractPaymentResult
23
- import com.tapaya.core.model.external.result.CardPaymentResult
24
- import com.tapaya.core.model.external.result.CertisInstantPaymentResult
25
- import com.tapaya.core.model.external.result.SepaInstantPaymentResult
26
- import com.tapaya.core.ui.theme.AcceptColorTheme
27
- import com.tapaya.core.ui.theme.AcceptImageSource
28
- import com.tapaya.core.ui.theme.AcceptImageTheme
29
- import com.tapaya.core.ui.theme.AcceptThemeConfiguration
30
- import com.tapaya.core.ui.theme.AcceptThemeSource
31
- import com.tapaya.core.ui.theme.GradientColors
32
- import expo.modules.interfaces.permissions.PermissionsStatus
5
+ import com.tapaya.accept.AcceptLogLevel
6
+ import com.tapaya.accept.AcceptLogger
7
+ import expo.modules.kotlin.exception.CodedException
8
+ import expo.modules.kotlin.exception.Exceptions
33
9
  import expo.modules.kotlin.functions.Coroutine
34
10
  import expo.modules.kotlin.modules.Module
35
11
  import expo.modules.kotlin.modules.ModuleDefinition
36
- import expo.modules.kotlin.records.Field
37
- import expo.modules.kotlin.records.Record
38
- import kotlinx.coroutines.suspendCancellableCoroutine
39
- import kotlin.coroutines.resume
40
- import kotlin.coroutines.resumeWithException
41
-
42
- //region-start
43
- class CardPaymentIntentReactNative : Record {
44
- @Field
45
- var paymentIntentId: String = ""
46
-
47
- @Field
48
- var amount: Long = 0L
49
-
50
- @Field
51
- var requestedCurrency: String = ""
52
-
53
- @Field
54
- var settlementCurrency: String = ""
55
-
56
- fun toCardInstantPaymentIntent(): CardChargeIntent {
57
- val requested = Currency.valueOf(requestedCurrency)
58
- return CardChargeIntent(
59
- paymentIntentId = paymentIntentId,
60
- amount = amount,
61
- requestedCurrency = requested,
62
- settlementCurrency = if (settlementCurrency.isEmpty()) requested else Currency.valueOf(settlementCurrency)
63
- )
64
- }
65
- }
66
-
67
- class CardPaymentResultReactNative(
68
- @Field
69
- var status: String = "",
70
- @Field
71
- var readerStatus: String = "",
72
- @Field
73
- var last4: String = "",
74
- @Field
75
- var companyName: String = "",
76
- @Field
77
- var companyAddress1: String = "",
78
- @Field
79
- var companyAddress2: String = "",
80
- @Field
81
- var companyCity: String = "",
82
- @Field
83
- var companyCountry: String = "",
84
- @Field
85
- var terminalId: String = "",
86
- @Field
87
- var authorizationCode: String = "",
88
- @Field
89
- var brand: String = "",
90
- @Field
91
- var reason: String = "",
92
- @Field
93
- var receiptUrl: String? = null,
94
- @Field
95
- var refundToken: String? = null,
96
- @Field
97
- var type: String = "",
98
- ) : Record
99
-
100
- fun CardPaymentResult.toReactNative(): CardPaymentResultReactNative {
101
- return CardPaymentResultReactNative(
102
- status = status.name.lowercase(),
103
- readerStatus = readerStatus?.name?.lowercase() ?: "",
104
- last4 = last4 ?: "",
105
- companyName = companyName ?: "",
106
- companyAddress1 = companyAddress1 ?: "",
107
- companyAddress2 = companyAddress2 ?: "",
108
- companyCity = companyCity ?: "",
109
- companyCountry = companyCountry.toCountryAlpha2(),
110
- terminalId = terminalId ?: "",
111
- authorizationCode = authorizationCode ?: "",
112
- brand = brand?.name ?: "",
113
- reason = reason?.name ?: "",
114
- receiptUrl = receiptUrl,
115
- refundToken = refundToken,
116
- type = "card"
117
- )
118
- }
119
-
120
- class SepaPaymentIntentReactNative : Record {
121
- @Field
122
- var paymentIntentId: String = ""
123
-
124
- @Field
125
- var amount: Long = 0L
126
-
127
- @Field
128
- var requestedCurrency: String = ""
129
-
130
- @Field
131
- var settlementCurrency: String = ""
132
-
133
- fun toSepaInstantPaymentIntent(): SepaInstantPaymentIntent {
134
- val requested = Currency.valueOf(requestedCurrency)
135
- return SepaInstantPaymentIntent(
136
- paymentIntentId = paymentIntentId,
137
- amount = amount,
138
- requestedCurrency = requested,
139
- settlementCurrency = if (settlementCurrency.isEmpty()) requested else Currency.valueOf(settlementCurrency)
140
- )
141
- }
142
- }
143
-
144
- class SepaPaymentInstantResultReactNative(
145
- @Field
146
- var status: String = "",
147
- @Field
148
- var postponed: Boolean = false,
149
- @Field
150
- var receivedAt: String = "",
151
- @Field
152
- var receivedFromIban: String = "",
153
- @Field
154
- var receiptUrl: String? = null,
155
- @Field
156
- var type: String = "",
157
- ) : Record
158
-
159
- fun SepaInstantPaymentResult.toReactNative(): SepaPaymentInstantResultReactNative {
160
- return SepaPaymentInstantResultReactNative(
161
- status = status.name.lowercase(),
162
- postponed = postponed,
163
- receivedAt = receivedAt.toString(),
164
- receivedFromIban = receivedFromIban ?: "",
165
- receiptUrl = receiptUrl,
166
- type = "sepa"
167
- )
168
- }
12
+ import java.util.concurrent.ConcurrentHashMap
13
+ import kotlin.time.Duration.Companion.milliseconds
14
+ import kotlinx.coroutines.CoroutineScope
15
+ import kotlinx.coroutines.Dispatchers
16
+ import kotlinx.coroutines.Job
17
+ import kotlinx.coroutines.SupervisorJob
18
+ import kotlinx.coroutines.cancel
19
+ import kotlinx.coroutines.launch
20
+
21
+ /**
22
+ * Expo module wrapping the Accept SDK for Android.
23
+ *
24
+ * The bridge is deliberately flat: every SDK surface is exposed as a `<surface><Method>` function
25
+ * and the namespaced `Accept.auth.*` / `Accept.payments.*` shape is reassembled in TypeScript. The
26
+ * Kotlin SDK's two non-request/response surfaces — `Accept.state` and the `Flow<PaymentEvent>`
27
+ * returned by `pay()` — become the `onStateChange` and `onPaymentEvent` events.
28
+ */
29
+ class AcceptSdkReactNativeModule : Module() {
169
30
 
170
- class CertisPaymentIntentReactNative : Record {
171
- @Field
172
- var paymentIntentId: String = ""
31
+ /**
32
+ * Scope for the SDK's flow collection. Main-immediate because the payment flow it collects
33
+ * launches activities, and cancelled wholesale when the module goes away so an in-flight
34
+ * payment's collector can't outlive the JavaScript context it reports to.
35
+ */
36
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
173
37
 
174
- @Field
175
- var amount: Long = 0L
38
+ /** Live `pay()` collectors, keyed by the request id the caller correlates its events with. */
39
+ private val paymentJobs = ConcurrentHashMap<String, Job>()
176
40
 
177
- @Field
178
- var requestedCurrency: String = ""
41
+ private var stateJob: Job? = null
179
42
 
180
- @Field
181
- var settlementCurrency: String = ""
43
+ override fun definition() = ModuleDefinition {
44
+ Name("AcceptSdkReactNative")
182
45
 
183
- fun toCertisInstantPaymentIntent(): CertisInstantPaymentIntent {
184
- val requested = Currency.valueOf(requestedCurrency)
185
- return CertisInstantPaymentIntent(
186
- paymentIntentId = paymentIntentId,
187
- amount = amount,
188
- requestedCurrency = requested,
189
- settlementCurrency = if (settlementCurrency.isEmpty()) requested else Currency.valueOf(settlementCurrency)
190
- )
191
- }
192
- }
46
+ Events(EVENT_STATE_CHANGE, EVENT_PAYMENT, EVENT_LOG)
193
47
 
194
- class CertisPaymentInstantResultReactNative(
195
- @Field
196
- var status: String = "",
197
- @Field
198
- var postponed: Boolean = false,
199
- @Field
200
- var receivedAt: String = "",
201
- @Field
202
- var receivedFromIban: String = "",
203
- @Field
204
- var receiptUrl: String? = null,
205
- @Field
206
- var type: String = "",
207
- ) : Record
208
-
209
- fun CertisInstantPaymentResult.toReactNative(): CertisPaymentInstantResultReactNative {
210
- return CertisPaymentInstantResultReactNative(
211
- status = status.name.lowercase(),
212
- postponed = postponed,
213
- receivedAt = receivedAt.toString(),
214
- receivedFromIban = receivedFromIban ?: "",
215
- receiptUrl = receiptUrl,
216
- type = "certis"
217
- )
218
- }
48
+ OnCreate {
49
+ stateJob = scope.launch {
50
+ Accept.state.collect { sendEvent(EVENT_STATE_CHANGE, mapOf("state" to it.serialize())) }
51
+ }
52
+ }
219
53
 
220
- class RefundIntentReactNative : Record {
221
- @Field
222
- var paymentToken: String = ""
54
+ OnDestroy {
55
+ stateJob = null
56
+ paymentJobs.clear()
57
+ scope.cancel()
58
+ }
223
59
 
224
- @Field
225
- var refundToken: String? = null
60
+ // ── Lifecycle ───────────────────────────────────────────────────────────
226
61
 
227
- @Field
228
- var reason: String = "customerRequest"
229
- }
62
+ Function("initialize") { isProduction: Boolean ->
63
+ Accept.initialize(appContext.reactContext ?: throw Exceptions.ReactContextLost(), isProduction)
64
+ }
230
65
 
231
- class RefundResultReactNative(
232
- @Field
233
- var refundToken: String? = null,
234
- @Field
235
- var status: String = "",
236
- @Field
237
- var refundReason: String = "",
238
- ) : Record
66
+ Function("getState") { Accept.state.value.serialize() }
239
67
 
68
+ AsyncFunction("clear") Coroutine { -> sdkCall { Accept.clear() } }
240
69
 
241
- class GradientColorsRecord : Record {
242
- @Field var start: String = ""
243
- @Field var end: String = ""
244
- }
70
+ // ── Diagnostics ─────────────────────────────────────────────────────────
245
71
 
246
- class AcceptColorThemeRecord : Record {
247
- @Field var accent: String = ""
248
- @Field var error: String = ""
249
- @Field var warning: String = ""
250
- @Field var success: String = ""
251
- @Field var brandGradientColors: GradientColorsRecord? = null
252
- @Field var brandSubtleGradientColors: GradientColorsRecord? = null
253
-
254
- fun toAcceptColorTheme(): AcceptColorTheme = AcceptColorTheme(
255
- accent = accent.toComposeColor(),
256
- error = error.toComposeColor(),
257
- warning = warning.toComposeColor(),
258
- success = success.toComposeColor(),
259
- brandGradientColors = brandGradientColors?.let { GradientColors(it.start.toComposeColor(), it.end.toComposeColor()) },
260
- brandSubtleGradientColors = brandSubtleGradientColors?.let { GradientColors(it.start.toComposeColor(), it.end.toComposeColor()) },
261
- )
262
- }
72
+ Function("setDebugLoggingEnabled") { enabled: Boolean -> Accept.setDebugLoggingEnabled(enabled) }
263
73
 
264
- class AcceptImageThemeRecord : Record {
265
- @Field var brandLogo: String = ""
266
- @Field var toolbarLogo: String = ""
267
- @Field var solidLogo: String = ""
268
-
269
- fun toAcceptImageTheme(): AcceptImageTheme = try {
270
- AcceptImageTheme(
271
- brandLogo = AcceptImageSource.Data(Base64.decode(brandLogo, Base64.DEFAULT)),
272
- toolbarLogo = AcceptImageSource.Data(Base64.decode(toolbarLogo, Base64.DEFAULT)),
273
- solidLogo = AcceptImageSource.Data(Base64.decode(solidLogo, Base64.DEFAULT)),
274
- )
275
- } catch (e: IllegalArgumentException) {
276
- throw Exception("ThemeError: invalid base64 image data — ${e.message}")
277
- }
278
- }
74
+ Function("setLogLevel") { level: String -> Accept.setLogLevel(parseLogLevel(level)) }
279
75
 
280
- class AcceptThemeConfigurationRecord : Record {
281
- @Field var themeId: String? = null
282
- @Field var colors: AcceptColorThemeRecord? = null
283
- @Field var images: AcceptImageThemeRecord? = null
284
-
285
- @OptIn(AcceptDemoApi::class)
286
- fun toAcceptThemeConfiguration(): AcceptThemeConfiguration {
287
- themeId?.let { id ->
288
- return AcceptThemeConfiguration(
289
- colors = AcceptThemeSource.ById(id),
290
- images = AcceptThemeSource.ById(id),
291
- )
76
+ /**
77
+ * Mirrors the SDK's logs onto [EVENT_LOG] so JavaScript can route them into its own
78
+ * logging stack. Disabling restores the SDK's default Logcat sink.
79
+ */
80
+ Function("setLogForwardingEnabled") { enabled: Boolean ->
81
+ Accept.setLogger(if (enabled) EventLogger() else null)
292
82
  }
293
- return AcceptThemeConfiguration(
294
- colors = colors?.let { AcceptThemeSource.Local(it.toAcceptColorTheme()) } ?: AcceptThemeSource.Default,
295
- images = images?.let { AcceptThemeSource.Local(it.toAcceptImageTheme()) } ?: AcceptThemeSource.Default,
296
- )
297
- }
298
- }
299
83
 
300
- private fun String.toComposeColor(): androidx.compose.ui.graphics.Color = try {
301
- androidx.compose.ui.graphics.Color(android.graphics.Color.parseColor(this))
302
- } catch (e: IllegalArgumentException) {
303
- throw Exception("ThemeError: invalid color '$this'. Expected #RRGGBB or #AARRGGBB — ${e.message}")
304
- }
84
+ // ── Theming ─────────────────────────────────────────────────────────────
305
85
 
306
- private fun Int?.toCountryAlpha2(): String = when (this) {
307
- 8 -> "AL"
308
- 20 -> "AD"
309
- 40 -> "AT"
310
- 51 -> "AM"
311
- 56 -> "BE"
312
- 70 -> "BA"
313
- 100 -> "BG"
314
- 112 -> "BY"
315
- 191 -> "HR"
316
- 196 -> "CY"
317
- 203 -> "CZ"
318
- 208 -> "DK"
319
- 233 -> "EE"
320
- 234 -> "FO"
321
- 246 -> "FI"
322
- 250 -> "FR"
323
- 268 -> "GE"
324
- 276 -> "DE"
325
- 292 -> "GI"
326
- 300 -> "GR"
327
- 304 -> "GL"
328
- 336 -> "VA"
329
- 348 -> "HU"
330
- 352 -> "IS"
331
- 372 -> "IE"
332
- 380 -> "IT"
333
- 398 -> "KZ"
334
- 417 -> "KG"
335
- 428 -> "LV"
336
- 438 -> "LI"
337
- 440 -> "LT"
338
- 442 -> "LU"
339
- 470 -> "MT"
340
- 492 -> "MC"
341
- 498 -> "MD"
342
- 499 -> "ME"
343
- 528 -> "NL"
344
- 578 -> "NO"
345
- 616 -> "PL"
346
- 620 -> "PT"
347
- 642 -> "RO"
348
- 643 -> "RU"
349
- 674 -> "SM"
350
- 688 -> "RS"
351
- 703 -> "SK"
352
- 705 -> "SI"
353
- 724 -> "ES"
354
- 752 -> "SE"
355
- 756 -> "CH"
356
- 792 -> "TR"
357
- 804 -> "UA"
358
- 807 -> "MK"
359
- 826 -> "GB"
360
- else -> ""
361
- }
86
+ Function("setTheme") { theme: Map<String, Any?>? -> Accept.setTheme(theme?.let(::parseTheme)) }
362
87
 
363
- private fun String.toPaymentMethod(): PaymentMethod = when (this) {
364
- "CARD" -> PaymentMethod.CARD
365
- "SEPA" -> PaymentMethod.SEPA_INST
366
- "CERTIS" -> PaymentMethod.CERTIS
367
- "CRYPTO" -> PaymentMethod.CRYPTO
368
- else -> throw IllegalArgumentException("Unknown payment method: $this")
369
- }
88
+ Function("getTheme") { Accept.getTheme()?.serialize() }
370
89
 
371
- private fun PaymentMethod.toJsString(): String = when (this) {
372
- PaymentMethod.CARD -> "CARD"
373
- PaymentMethod.SEPA_INST -> "SEPA"
374
- PaymentMethod.CERTIS -> "CERTIS"
375
- PaymentMethod.CRYPTO -> "CRYPTO"
376
- }
90
+ Function("createLocalThemeImageUri") { base64: String, fileExtension: String ->
91
+ Accept.createLocalThemeImageUri(Base64.decode(base64, Base64.DEFAULT), fileExtension)
92
+ }
377
93
 
378
- private fun String.toRefundReason(): RefundReason = when (this) {
379
- "customerRequest" -> RefundReason.CUSTOMER_REQUEST
380
- "duplicate" -> RefundReason.DUPLICATE
381
- "fraudulent" -> RefundReason.FRAUDULENT
382
- "expiredCharge" -> RefundReason.EXPIRED_CHARGE
383
- else -> throw IllegalArgumentException("Unknown refund reason: $this")
384
- }
94
+ // ── Auth ────────────────────────────────────────────────────────────────
385
95
 
386
- class NfcPositionRecord : Record {
387
- @Field var xBias: Float = 0f
388
- @Field var yBias: Float = 0f
389
- }
390
-
391
- class NfcZoneRecord : Record {
392
- @Field var indicator: String = "DEFAULT"
393
- @Field var position: NfcPositionRecord? = null
96
+ AsyncFunction("authenticate") Coroutine { merchantToken: String ->
97
+ sdkCall { Accept.auth.authenticate(merchantToken) }
98
+ }
394
99
 
395
- fun toNfcZone(): NfcZone {
396
- val pos = position?.let { NfcPosition.Custom(it.xBias, it.yBias) } ?: NfcPosition.Default
397
- return NfcZone(indicator = indicator.toNfcIndicator(), position = pos)
398
- }
399
- }
100
+ // ── SDK ─────────────────────────────────────────────────────────────────
400
101
 
401
- class AcceptUiBehaviorRecord : Record {
402
- @Field var resultScreenDurationSeconds: Int = 10
403
- @Field var showResultScreen: Boolean = true
404
- @Field var nfcZone: NfcZoneRecord? = null
102
+ Function("getSdkVersion") { Accept.sdk.version }
405
103
 
406
- fun toAcceptUiBehavior(): AcceptUiBehavior = AcceptUiBehavior(
407
- resultScreenDurationSeconds = resultScreenDurationSeconds,
408
- showResultScreen = showResultScreen,
409
- nfcZone = nfcZone?.toNfcZone(),
410
- )
411
- }
104
+ Function("isProduction") { sdkCallSync { Accept.sdk.isProduction } }
412
105
 
413
- private fun String.toNfcIndicator(): NfcIndicator = when (this) {
414
- "DEFAULT" -> NfcIndicator.DEFAULT
415
- "ABOVE" -> NfcIndicator.ABOVE
416
- "BELOW" -> NfcIndicator.BELOW
417
- "FRONT" -> NfcIndicator.FRONT
418
- "BEHIND" -> NfcIndicator.BEHIND
419
- else -> throw IllegalArgumentException("Unknown NFC indicator: $this")
420
- }
106
+ AsyncFunction("getDeviceId") Coroutine { -> sdkCall { Accept.sdk.deviceId() } }
421
107
 
422
- class KybPrefillDataRecord : Record {
423
- @Field var businessType: String? = null
424
- @Field var countryCode: String? = null
425
- @Field var registrationNumber: String? = null
426
- @Field var vatNumber: String? = null
427
- @Field var legalName: String? = null
428
- @Field var addressLine1: String? = null
429
- @Field var addressLine2: String? = null
430
- @Field var city: String? = null
431
- @Field var postalCode: String? = null
432
- @Field var merchantCategoryCode: Int? = null
433
- @Field var businessEmail: String? = null
434
- @Field var bankAccountIban: String? = null
435
- @Field var businessUrl: String? = null
436
- @Field var productDescription: String? = null
437
- @Field var supportPhone: String? = null
438
-
439
- fun toIdentityData(): IdentityData = IdentityData(
440
- businessType = businessType?.let { BusinessType.valueOf(it) },
441
- countryCode = countryCode,
442
- registrationNumber = registrationNumber,
443
- vatNumber = vatNumber,
444
- legalName = legalName,
445
- addressLine1 = addressLine1,
446
- addressLine2 = addressLine2,
447
- city = city,
448
- postalCode = postalCode,
449
- merchantCategoryCode = merchantCategoryCode,
450
- businessEmail = businessEmail,
451
- bankAccountIban = bankAccountIban,
452
- businessUrl = businessUrl,
453
- productDescription = productDescription,
454
- supportPhone = supportPhone,
455
- )
456
- }
108
+ AsyncFunction("getMinimumAmounts") Coroutine { ->
109
+ sdkCall { Accept.sdk.minimumAmounts().map { it.serialize() } }
110
+ }
457
111
 
458
- //endregion-end
112
+ // ── Merchant ────────────────────────────────────────────────────────────
459
113
 
460
- class AcceptSdkReactNativeModule : Module() {
461
- override fun definition() = ModuleDefinition {
462
- Name("AcceptSdkReactNative")
114
+ AsyncFunction("getMerchantInfo") Coroutine { -> sdkCall { Accept.merchant.info()?.serialize() } }
463
115
 
464
- Events(
465
- "onCardPaymentStatusChange",
466
- "onSepaPaymentStatusChange",
467
- "onCertisPaymentStatusChange",
468
- "onCardPaymentException",
469
- "onSepaPaymentException",
470
- "onCertisPaymentException",
471
- )
472
-
473
- AsyncFunction("initialize") Coroutine { demo: Boolean, theme: AcceptThemeConfigurationRecord? ->
474
- val context = appContext.reactContext ?: throw Exception("Context not available")
475
- val environment = if (demo) AcceptEnvironment.SANDBOX else AcceptEnvironment.PRODUCTION
476
- Accept.initialize(context, environment, theme?.toAcceptThemeConfiguration() ?: AcceptThemeConfiguration.Default)
477
- }
116
+ AsyncFunction("getMerchantConfig") Coroutine { -> sdkCall { Accept.merchant.config().serialize() } }
478
117
 
479
- AsyncFunction("setTheme") Coroutine { theme: AcceptThemeConfigurationRecord ->
480
- Accept.setThemeConfiguration(theme.toAcceptThemeConfiguration())
481
- return@Coroutine
118
+ AsyncFunction("getOnboardingStatus") Coroutine { ->
119
+ sdkCall { Accept.merchant.onboardingStatus().serialize() }
482
120
  }
483
121
 
484
- AsyncFunction("setUiBehavior") { behavior: AcceptUiBehaviorRecord ->
485
- Accept.setUiBehavior(behavior.toAcceptUiBehavior())
122
+ AsyncFunction("getAvailableCurrencies") Coroutine { -> sdkCall { Accept.merchant.availableCurrencies() } }
123
+
124
+ // ── Payments ────────────────────────────────────────────────────────────
125
+
126
+ /**
127
+ * Sets the session-wide payment defaults. `setOptions` keeps every option the caller
128
+ * doesn't name, so each key present in [options] is applied by its own call — that is what
129
+ * lets a caller set one default without disturbing the others, and it keeps an absent key
130
+ * distinct from an explicit `null` timeout.
131
+ */
132
+ Function("setPaymentOptions") { options: Map<String, Any?> ->
133
+ options.receiptConfig()?.let { Accept.payments.setOptions(receiptConfig = it) }
134
+ options.nfcPosition()?.let { Accept.payments.setOptions(nfcPosition = it) }
135
+ options.display()?.let { Accept.payments.setOptions(display = it) }
136
+ if (options.containsKey("transactionTimeoutMs")) {
137
+ Accept.payments.setOptions(transactionTimeout = options.transactionTimeout())
138
+ }
486
139
  }
487
140
 
488
- AsyncFunction("authenticate") Coroutine { token: String ->
489
- return@Coroutine suspendCancellableCoroutine<Boolean> { continuation ->
490
- try {
491
- Accept.authenticate(
492
- token,
493
- onSuccess = { continuation.resume(true) },
494
- onError = { error -> (error as? Throwable)?.let { continuation.resumeWithException(it) } }
495
- )
496
- } catch (e: Exception) {
497
- continuation.resumeWithException(e)
141
+ /**
142
+ * Starts a payment and streams its `Flow<PaymentEvent>` onto [EVENT_PAYMENT], tagged with
143
+ * [requestId]. Returns as soon as the collector is running; the caller learns the outcome
144
+ * from the terminal `result` or `creationFailed` event.
145
+ */
146
+ Function("startPayment") { requestId: String, request: Map<String, Any?> ->
147
+ val job = scope.launch {
148
+ Accept.payments.pay(
149
+ amount = request.amount(),
150
+ currency = request.currency(),
151
+ paymentToken = request["paymentToken"] as? String,
152
+ receiptConfig = request.receiptConfig(),
153
+ nfcPosition = request.nfcPosition(),
154
+ metadata = parseMetadata(request.metadata()),
155
+ tip = request.tip(),
156
+ transactionTimeout = request.transactionTimeout(),
157
+ display = request.display(),
158
+ ).collect { event ->
159
+ sendEvent(EVENT_PAYMENT, mapOf("requestId" to requestId, "event" to event.serialize()))
498
160
  }
161
+ paymentJobs.remove(requestId)
499
162
  }
163
+ paymentJobs[requestId] = job
500
164
  }
501
165
 
502
- Function("getStatus") { ->
503
- try {
504
- return@Function Accept.status.name
505
- } catch (e: Exception) {
506
- throw e
507
- }
166
+ /**
167
+ * Stops reporting events for [requestId]. This detaches the listener only — the plugin's
168
+ * payment UI is already on screen by then, and is dismissed by the user or by
169
+ * `cancelPayment(paymentToken)` against the backend.
170
+ */
171
+ Function("stopPaymentEvents") { requestId: String ->
172
+ paymentJobs.remove(requestId)?.cancel()
508
173
  }
509
174
 
510
- AsyncFunction("getPaymentStatus") Coroutine { paymentIntentId: String ->
511
- return@Coroutine suspendCancellableCoroutine<String> { continuation ->
512
- try {
513
- Accept.getPaymentStatus(
514
- paymentIntentId,
515
- { result: AbstractPaymentResult ->
516
- val mapped = when (result) {
517
- is CardPaymentResult -> result.toReactNative()
518
- is CertisInstantPaymentResult -> result.toReactNative()
519
- is SepaInstantPaymentResult -> result.toReactNative()
520
- else -> throw Exception("Unsupported payment result type")
521
- }
522
-
523
- continuation.resume(Gson().toJson(mapped))
524
- },
525
- { error: AcceptException ->
526
- (error as? Throwable)?.let { continuation.resumeWithException(it) }
527
- }
528
- )
529
- } catch (e: Exception) {
530
- continuation.resumeWithException(e)
531
- }
532
- }
175
+ AsyncFunction("getPaymentStatus") Coroutine { paymentToken: String ->
176
+ sdkCall { Accept.payments.status(paymentToken).serialize() }
533
177
  }
534
178
 
535
- AsyncFunction("requestLocationPermission") Coroutine { ->
536
- return@Coroutine suspendCancellableCoroutine<Boolean> { continuation ->
537
- appContext.permissions?.askForPermissions(
538
- {
539
- val result = it.map { (_, value) ->
540
- value.status == PermissionsStatus.GRANTED
541
- }.reduce { acc, i -> acc.and(i) }
542
- continuation.resume(result)
543
- },
544
- "android.permission.ACCESS_FINE_LOCATION",
545
- "android.permission.ACCESS_COARSE_LOCATION"
546
- )
547
- }
179
+ AsyncFunction("cancelPayment") Coroutine { paymentToken: String ->
180
+ sdkCall { Accept.payments.cancel(paymentToken).name }
548
181
  }
549
182
 
550
- AsyncFunction("logOut") Coroutine { ->
551
- return@Coroutine suspendCancellableCoroutine<Boolean> { continuation ->
552
- try {
553
- Accept.logOut {
554
- continuation.resume(true)
555
- }
556
- } catch (e: Exception) {
557
- continuation.resumeWithException(e)
558
- }
559
- }
183
+ AsyncFunction("refundPayment") Coroutine { paymentToken: String, reason: String ->
184
+ sdkCall { Accept.payments.refund(paymentToken, parseRefundReason(reason)).name }
560
185
  }
561
186
 
562
- AsyncFunction("isPaymentPending") { ->
563
- try {
564
- return@AsyncFunction Accept.isPaymentPending()
565
- } catch (e: Exception) {
566
- throw e
567
- }
568
- }
187
+ // ── Plugin ──────────────────────────────────────────────────────────────
569
188
 
570
- AsyncFunction("presentKyb") Coroutine { prefilling: KybPrefillDataRecord? ->
571
- Accept.identity.presentKyb(prefilling?.toIdentityData())
572
- }
189
+ Function("isPluginInstalled") { sdkCallSync { Accept.plugin.isInstalled() } }
573
190
 
574
- AsyncFunction("getOnboardingStatus") Coroutine { ->
575
- val status = Accept.identity.getOnboardingStatus()
576
- return@Coroutine mapOf(
577
- "isOnboarded" to status.isOnboarded,
578
- "paymentMethods" to status.paymentMethods.map { pm ->
579
- mapOf(
580
- "method" to pm.method.toJsString(),
581
- "status" to pm.status.name,
582
- )
583
- },
584
- )
585
- }
191
+ AsyncFunction("installPlugin") Coroutine { -> sdkCall { Accept.plugin.install() } }
586
192
 
587
- AsyncFunction("getPaymentMethodStatus") Coroutine { method: String ->
588
- val status = Accept.identity.getPaymentMethodStatus(method.toPaymentMethod())
589
- return@Coroutine status.name
590
- }
193
+ AsyncFunction("activateTerminal") Coroutine { -> sdkCall { Accept.plugin.activateTerminal().serialize() } }
591
194
 
592
- AsyncFunction("startCardPayment") Coroutine { paymentIntent: CardPaymentIntentReactNative ->
593
- return@Coroutine suspendCancellableCoroutine<CardPaymentResultReactNative> { continuation ->
594
- try {
595
- Accept.cardTerminal.process(
596
- paymentIntent.toCardInstantPaymentIntent(),
597
- onResult = {
598
- continuation.resume(it.toReactNative())
599
- },
600
- onStatus = {
601
- sendEvent("onCardPaymentStatusChange", mapOf("data" to it.name.lowercase()))
602
- },
603
- onError = {
604
- sendEvent("onCardPaymentException", it.toReactNative())
605
- }
606
- )
607
- } catch (e: Exception) {
608
- continuation.resumeWithException(e)
609
- }
610
- }
611
- }
195
+ AsyncFunction("getPluginStatus") Coroutine { -> sdkCall { Accept.plugin.status().serialize() } }
612
196
 
613
- AsyncFunction("startSepaPayment") Coroutine { paymentIntent: SepaPaymentIntentReactNative ->
614
- return@Coroutine suspendCancellableCoroutine<SepaPaymentInstantResultReactNative> { continuation ->
615
- try {
616
- Accept.sepaTerminal.process(
617
- paymentIntent.toSepaInstantPaymentIntent(),
618
- onResult = {
619
- continuation.resume(it.toReactNative())
620
- },
621
- onStatus = {
622
- sendEvent("onSepaPaymentStatusChange", mapOf("data" to it.name.lowercase()))
623
- },
624
- onError = {
625
- sendEvent("onSepaPaymentException", it.toReactNative())
626
- }
627
- )
628
- } catch (e: Exception) {
629
- continuation.resumeWithException(e)
630
- }
631
- }
632
- }
197
+ AsyncFunction("getPluginUpdateInfo") Coroutine { -> sdkCall { Accept.plugin.updateInfo().serialize() } }
633
198
 
634
- AsyncFunction("startCertisPayment") Coroutine { paymentIntent: CertisPaymentIntentReactNative ->
635
- return@Coroutine suspendCancellableCoroutine<CertisPaymentInstantResultReactNative> { continuation ->
636
- try {
637
- Accept.certisTerminal.process(
638
- paymentIntent.toCertisInstantPaymentIntent(),
639
- onResult = {
640
- continuation.resume(it.toReactNative())
641
- },
642
- onStatus = {
643
- sendEvent("onCertisPaymentStatusChange", mapOf("data" to it.name.lowercase()))
644
- },
645
- onError = {
646
- sendEvent("onCertisPaymentException", it.toReactNative())
647
- }
648
- )
649
- } catch (e: Exception) {
650
- continuation.resumeWithException(e)
651
- }
652
- }
653
- }
199
+ AsyncFunction("logoutPlugin") Coroutine { -> sdkCall { Accept.plugin.logout() } }
654
200
 
655
- AsyncFunction("refund") Coroutine { intent: RefundIntentReactNative ->
656
- return@Coroutine suspendCancellableCoroutine<RefundResultReactNative> { continuation ->
657
- try {
658
- val refundIntent = CardRefundIntent(
659
- sourcePaymentToken = intent.paymentToken,
660
- refundReason = intent.reason.toRefundReason(),
661
- refundToken = intent.refundToken,
662
- )
663
- Accept.cardTerminal.process(
664
- refundIntent,
665
- onResult = { result ->
666
- continuation.resume(
667
- RefundResultReactNative(
668
- refundToken = result.refundToken,
669
- status = result.status.name.lowercase(),
670
- refundReason = intent.reason,
671
- )
672
- )
673
- },
674
- onStatus = { /* refund flow emits no UI status */ },
675
- onError = { error ->
676
- continuation.resumeWithException(error as Throwable)
677
- }
678
- )
679
- } catch (e: Exception) {
680
- continuation.resumeWithException(e)
681
- }
682
- }
683
- }
201
+ // ── Displays ────────────────────────────────────────────────────────────
684
202
 
685
- AsyncFunction("cancelActivePayment") { ->
686
- if (!Accept.isPaymentPending()) {
687
- throw Exception("NoActivePayment")
688
- }
689
- Accept.cardTerminal.reset()
690
- Accept.sepaTerminal.reset()
691
- Accept.certisTerminal.reset()
692
- }
203
+ Function("getDisplays") { Accept.displays.all().map { it.serialize() } }
693
204
 
694
- AsyncFunction("enableDemoMode") { ->
695
- Accept.enableDemoMode()
696
- }
205
+ Function("getCustomerFacingDisplay") { Accept.displays.customerFacing()?.serialize() }
697
206
 
698
- AsyncFunction("disableDemoMode") { ->
699
- Accept.disableDemoMode()
700
- }
207
+ Function("isDualScreen") { Accept.displays.isDualScreen }
208
+ }
701
209
 
702
- AsyncFunction("isDemoMode") { ->
703
- Accept.isDemoMode()
210
+ /** Forwards the SDK's consumer-facing logs to JavaScript as [EVENT_LOG]. */
211
+ private inner class EventLogger : AcceptLogger {
212
+ override fun log(level: AcceptLogLevel, tag: String, message: String, throwable: Throwable?) {
213
+ sendEvent(
214
+ EVENT_LOG,
215
+ mapOf(
216
+ "level" to level.name,
217
+ "tag" to tag,
218
+ "message" to message,
219
+ "throwable" to throwable?.message,
220
+ ),
221
+ )
704
222
  }
223
+ }
705
224
 
706
- AsyncFunction("isCompanionAppInstalled") { ->
707
- return@AsyncFunction Accept.isCompanionAppInstalled()
708
- }
225
+ private companion object {
226
+ const val EVENT_STATE_CHANGE = "onStateChange"
227
+ const val EVENT_PAYMENT = "onPaymentEvent"
228
+ const val EVENT_LOG = "onLog"
229
+ }
230
+ }
709
231
 
710
- AsyncFunction("pairWithCompanionApp") Coroutine { ->
711
- return@Coroutine suspendCancellableCoroutine<Unit> { continuation ->
712
- try {
713
- Accept.pairWithCompanionApp(
714
- onSuccess = { continuation.resume(Unit) },
715
- onError = { error -> continuation.resumeWithException(error as Throwable) }
716
- )
717
- } catch (e: Exception) {
718
- continuation.resumeWithException(e)
719
- }
720
- }
721
- }
232
+ // ── Request map accessors ───────────────────────────────────────────────────────
233
+ //
234
+ // `pay()` and `setOptions()` read the same option names out of two different maps (a per-payment
235
+ // request and the session defaults), so the accessors are shared. Each returns null when the key is
236
+ // absent, which is exactly what the SDK reads as "use the default".
722
237
 
723
- AsyncFunction("presentCompanionAppSheet") Coroutine { ->
724
- return@Coroutine suspendCancellableCoroutine<Unit> { continuation ->
725
- try {
726
- Accept.presentCompanionAppSheet(
727
- onSuccess = { continuation.resume(Unit) },
728
- onError = { error -> continuation.resumeWithException(error as Throwable) }
729
- )
730
- } catch (e: Exception) {
731
- continuation.resumeWithException(e)
732
- }
733
- }
734
- }
238
+ private fun Map<String, Any?>.amount(): Long = (this["amount"] as? Number)?.toLong()
239
+ ?: throw CodedException("ERR_INVALID_ARGUMENT", "amount is required", null)
735
240
 
736
- }
241
+ private fun Map<String, Any?>.currency(): String = this["currency"] as? String
242
+ ?: throw CodedException("ERR_INVALID_ARGUMENT", "currency is required", null)
737
243
 
738
- private fun AcceptException.toReactNative(): Map<String, String> {
739
- return mapOf(
740
- "message" to ((this as Throwable).message ?: ""),
741
- "exception" to ((this as Throwable).cause?.stackTraceToString() ?: "")
742
- )
743
- }
744
- }
244
+ private fun Map<String, Any?>.tip(): Long? = (this["tip"] as? Number)?.toLong()
245
+
246
+ private fun Map<String, Any?>.transactionTimeout() =
247
+ (this["transactionTimeoutMs"] as? Number)?.toLong()?.milliseconds
248
+
249
+ @Suppress("UNCHECKED_CAST")
250
+ private fun Map<String, Any?>.receiptConfig() =
251
+ (this["receiptConfig"] as? Map<String, Any?>)?.let(::parseReceiptConfig)
252
+
253
+ @Suppress("UNCHECKED_CAST")
254
+ private fun Map<String, Any?>.nfcPosition() =
255
+ (this["nfcPosition"] as? Map<String, Any?>)?.let(::parseNfcPosition)
256
+
257
+ @Suppress("UNCHECKED_CAST")
258
+ private fun Map<String, Any?>.display() =
259
+ (this["display"] as? Map<String, Any?>)?.let(::parsePaymentDisplay)
260
+
261
+ @Suppress("UNCHECKED_CAST")
262
+ private fun Map<String, Any?>.metadata() = this["metadata"] as? Map<String, Any?>