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.
Files changed (57) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +20 -0
  3. package/README.md +278 -0
  4. package/RNAmwalEcr.podspec +27 -0
  5. package/android/build.gradle +53 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/java/com/amwalecr/AmwalEcrModule.kt +325 -0
  8. package/android/src/main/java/com/amwalecr/AmwalEcrPackage.kt +31 -0
  9. package/android/src/main/java/com/amwalecr/EcrMapping.kt +135 -0
  10. package/docs/compatibility.md +22 -0
  11. package/ios/AmwalEcr.h +4 -0
  12. package/ios/AmwalEcr.mm +121 -0
  13. package/ios/AmwalEcr.swift +424 -0
  14. package/ios/EcrMapping.swift +142 -0
  15. package/lib/module/EcrOperation.js +18 -0
  16. package/lib/module/EcrOperation.js.map +1 -0
  17. package/lib/module/EcrTerminal.js +211 -0
  18. package/lib/module/EcrTerminal.js.map +1 -0
  19. package/lib/module/NativeAmwalEcr.js +5 -0
  20. package/lib/module/NativeAmwalEcr.js.map +1 -0
  21. package/lib/module/index.js +6 -0
  22. package/lib/module/index.js.map +1 -0
  23. package/lib/module/native.js +16 -0
  24. package/lib/module/native.js.map +1 -0
  25. package/lib/module/native.native.js +4 -0
  26. package/lib/module/native.native.js.map +1 -0
  27. package/lib/module/normalize.js +87 -0
  28. package/lib/module/normalize.js.map +1 -0
  29. package/lib/module/package.json +1 -0
  30. package/lib/module/types.js +2 -0
  31. package/lib/module/types.js.map +1 -0
  32. package/lib/typescript/package.json +1 -0
  33. package/lib/typescript/src/EcrOperation.d.ts +10 -0
  34. package/lib/typescript/src/EcrOperation.d.ts.map +1 -0
  35. package/lib/typescript/src/EcrTerminal.d.ts +34 -0
  36. package/lib/typescript/src/EcrTerminal.d.ts.map +1 -0
  37. package/lib/typescript/src/NativeAmwalEcr.d.ts +14 -0
  38. package/lib/typescript/src/NativeAmwalEcr.d.ts.map +1 -0
  39. package/lib/typescript/src/index.d.ts +5 -0
  40. package/lib/typescript/src/index.d.ts.map +1 -0
  41. package/lib/typescript/src/native.d.ts +3 -0
  42. package/lib/typescript/src/native.d.ts.map +1 -0
  43. package/lib/typescript/src/native.native.d.ts +2 -0
  44. package/lib/typescript/src/native.native.d.ts.map +1 -0
  45. package/lib/typescript/src/normalize.d.ts +5 -0
  46. package/lib/typescript/src/normalize.d.ts.map +1 -0
  47. package/lib/typescript/src/types.d.ts +157 -0
  48. package/lib/typescript/src/types.d.ts.map +1 -0
  49. package/package.json +153 -0
  50. package/src/EcrOperation.ts +25 -0
  51. package/src/EcrTerminal.ts +344 -0
  52. package/src/NativeAmwalEcr.ts +28 -0
  53. package/src/index.tsx +34 -0
  54. package/src/native.native.ts +1 -0
  55. package/src/native.ts +18 -0
  56. package/src/normalize.ts +116 -0
  57. package/src/types.ts +189 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Never published before this entry — the two sections below ship together as the
6
+ first release.
7
+
8
+ - Add the `webService` transport: the terminal is driven through the Amwal
9
+ payment host over REST, selected by `merchantId`, `terminalId` and
10
+ `environment` in the config. No host address is needed; receipt fetch stays
11
+ local-only and answers `unavailable`.
12
+ - Extend `EcrConfig` with `merchantId`, `terminalId` and `environment`
13
+ (`'SIT' | 'UAT' | 'PROD'`), matching `com.amwal-pay:ecr-sdk` 1.0.5 and
14
+ `AmwalECR` 0.2.1, which the native bridges now require. The native session
15
+ path also now refuses an unsigned link (`secureHashKey` is required in
16
+ practice — the terminal refuses what it cannot verify).
17
+ - Add `usbCable` to `EcrTransport`. It names the terminal profile's cable mode
18
+ but is not driven by this library; it answers with a typed `unsupported`
19
+ failure, like `bluetooth`.
20
+ - Report `partialApproval` and `authorizedAmount` on inquiry transactions.
21
+
22
+ And from the 2026-09-02 draft:
23
+
24
+ - Add the React Native TurboModule for Android and iOS.
25
+ - Wrap native ECR sale, void, refund, inquiry, inquiry-by-reference, receipt,
26
+ reachability, and cancellation operations.
27
+ - Preserve approved, declined, failed, recovered, and unknown-outcome semantics
28
+ across both platforms.
29
+ - Add a typed TypeScript API, example application, tests, packaging checks, and
30
+ Codemagic verify/release workflows.
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Amwal Pay
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,278 @@
1
+ # Amwal ECR for React Native
2
+
3
+ Drive an Amwal POS terminal from a React Native Android or iOS application over
4
+ the local network. The package is a typed bridge over the same published native
5
+ providers used by the Amwal Flutter package:
6
+
7
+ - Android: `com.amwal-pay:ecr-sdk:1.0.5` from Maven Central
8
+ - iOS: `AmwalECR ~> 0.2.1` from CocoaPods trunk
9
+
10
+ The bridge contains no payment protocol implementation. Android and iOS use the
11
+ native Amwal ECR SDKs unchanged and return the same outcomes for sale, void,
12
+ refund, inquiry, and e-receipt operations.
13
+
14
+ ## The one rule
15
+
16
+ **A failed exchange is not a decline.** A timeout or lost connection can happen
17
+ after the terminal accepted the request and completed the payment. When
18
+ `outcomeIsUnknown` is `true`, do not send the sale again. Inquire using the same
19
+ `merchantReferenceId` and reconcile first.
20
+
21
+ ```tsx
22
+ const result = await terminal.sale('1.234', {
23
+ merchantReferenceId: order.id,
24
+ });
25
+
26
+ switch (result.outcome) {
27
+ case 'approved':
28
+ completeOrder(result.amount, result.rrn);
29
+ break;
30
+ case 'declined':
31
+ if (result.outcomeIsUnknown) {
32
+ await terminal.inquireByReference(result.merchantReferenceId);
33
+ } else {
34
+ showDecline(result.reason);
35
+ }
36
+ break;
37
+ case 'failed':
38
+ if (result.settled && result.recovered?.outcome === 'found') {
39
+ reconcile(result.recovered.transaction);
40
+ } else if (result.outcomeIsUnknown) {
41
+ await terminal.inquireByReference(result.merchantReferenceId);
42
+ }
43
+ break;
44
+ }
45
+ ```
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ yarn add react-native-amwal-ecr
51
+ ```
52
+
53
+ React Native autolinking installs the Android module. On iOS, install pods after
54
+ adding the package:
55
+
56
+ ```bash
57
+ cd ios
58
+ pod install
59
+ ```
60
+
61
+ The package requires React Native's New Architecture/TurboModules, Java 17,
62
+ Android API 24+, and iOS 15.1+ (the effective minimum of current React Native;
63
+ the native Amwal ECR pod itself supports iOS 12+).
64
+
65
+ ### iOS local-network permission
66
+
67
+ Add a user-facing reason to the application `Info.plist`:
68
+
69
+ ```xml
70
+ <key>NSLocalNetworkUsageDescription</key>
71
+ <string>Connects to the payment terminal to take card payments.</string>
72
+ ```
73
+
74
+ Android's `INTERNET` permission is contributed by the library manifest.
75
+
76
+ ## Create a terminal
77
+
78
+ ```tsx
79
+ import { EcrTerminal } from 'react-native-amwal-ecr';
80
+
81
+ const terminal = new EcrTerminal({
82
+ host: '192.168.1.50',
83
+ serialNumber: 'P653200085189',
84
+ transport: 'wifi',
85
+ config: {
86
+ ecrId: 'TILL-01',
87
+ currencyCode: '512',
88
+ minorUnitDigits: 3,
89
+ port: 9100,
90
+ connectTimeoutMs: 10_000,
91
+ responseTimeoutMs: 120_000,
92
+ probeTimeoutMs: 3_000,
93
+ secureHashKey: secretFromSecureStorage,
94
+ autoInquireOnFailure: true,
95
+ },
96
+ });
97
+ ```
98
+
99
+ The ECR secret is issued by Amwal per terminal. Keep it in the platform keychain
100
+ or secure storage; never place it in source, logs, analytics, or crash reports.
101
+
102
+ Only `ethernet` and `wifi` open a local ECR listener. `webService` drives the
103
+ terminal through the Amwal payment host over REST instead — no host address,
104
+ no local network:
105
+
106
+ ```tsx
107
+ const hostedTerminal = new EcrTerminal({
108
+ serialNumber: 'P653200085189',
109
+ transport: 'webService',
110
+ config: {
111
+ merchantId: '13593',
112
+ terminalId: '742001',
113
+ environment: 'UAT', // 'SIT' | 'UAT' | 'PROD'
114
+ secureHashKey: webServiceSecretFromSecureStorage,
115
+ },
116
+ });
117
+ ```
118
+
119
+ The Web Service path signs with its own secret (different from the LAN key);
120
+ both are issued by Amwal and both travel through `secureHashKey`. Receipt fetch
121
+ is only available on the local transports, and `isReachable()` resolves `false`
122
+ for `webService` — there is no local listener to probe. `bluetooth` and
123
+ `usbCable` return a typed `unsupported` failure without sending anything.
124
+
125
+ ## Operations
126
+
127
+ Amounts are plain decimal strings in major units. Never pass a JavaScript
128
+ number: binary floating point cannot exactly represent OMR amounts such as
129
+ `1.234`.
130
+
131
+ ```tsx
132
+ await terminal.isReachable();
133
+
134
+ await terminal.sale('1.234', {
135
+ merchantReferenceId: 'ORDER-1001',
136
+ });
137
+
138
+ await terminal.voidTransaction('123', {
139
+ originalTerminalId: 'TILL-01',
140
+ merchantReferenceId: 'VOID-1001',
141
+ });
142
+
143
+ await terminal.refund('0.500', {
144
+ receiptNumber: '123',
145
+ transactionDate: '20260902',
146
+ originalTerminalId: 'TILL-01',
147
+ merchantReferenceId: 'REFUND-1001',
148
+ });
149
+
150
+ await terminal.inquire({
151
+ receiptNumber: '123',
152
+ transactionDate: '20260902',
153
+ });
154
+
155
+ await terminal.inquireByReference('ORDER-1001');
156
+
157
+ await terminal.receipt({
158
+ receiptNumber: '123',
159
+ transactionDate: '20260902',
160
+ });
161
+ ```
162
+
163
+ `inquire`, `inquireByReference`, and `receipt` are read-only and safe to repeat.
164
+ The native SDK also follows an unknown money-moving failure with one inquiry by
165
+ reference when `autoInquireOnFailure` is enabled.
166
+
167
+ ## Cancellation
168
+
169
+ Every operation has a `start...` form that returns an `EcrOperation`:
170
+
171
+ ```tsx
172
+ const operation = terminal.startSale('1.234', {
173
+ merchantReferenceId: 'ORDER-1001',
174
+ });
175
+
176
+ cancelButton.onPress = () => operation.cancel();
177
+ const result = await operation.result;
178
+ ```
179
+
180
+ Cancellation stops this application waiting; it does not reverse or stop the
181
+ terminal. A cancelled money-moving request returns a `failed` result with an
182
+ unknown outcome. Inquire before retrying.
183
+
184
+ The cancellable forms are `startSale`, `startVoid`, `startRefund`,
185
+ `startInquire`, `startInquireByReference`, and `startReceipt`.
186
+
187
+ ## Result model
188
+
189
+ `EcrResult` is a discriminated union:
190
+
191
+ - `approved`: money moved; includes amount, RRN, authorisation code, masked PAN,
192
+ and partial-approval fields.
193
+ - `declined`: the terminal answered and refused; code `91` remains unknown and
194
+ requests an inquiry.
195
+ - `failed`: the exchange did not produce a trustworthy answer; includes a typed
196
+ failure and an optional recovered inquiry.
197
+
198
+ Failure kinds are `unreachable`, `timeout`, `malformed`, `connectionLost`,
199
+ `unauthenticated`, `cancelled`, and `unsupported`. The public
200
+ `outcomeIsUnknown` flag is the decision field callers should use.
201
+
202
+ ## Development
203
+
204
+ ```bash
205
+ corepack yarn install
206
+ corepack yarn verify
207
+
208
+ cd example/android
209
+ ./gradlew :app:assembleDebug
210
+
211
+ cd ../ios
212
+ pod install
213
+ xcodebuild -workspace AmwalEcrExample.xcworkspace \
214
+ -scheme AmwalEcrExample -sdk iphonesimulator \
215
+ -configuration Debug CODE_SIGNING_ALLOWED=NO build
216
+ ```
217
+
218
+ The example intentionally does not persist terminal details or secrets.
219
+
220
+ ## Continuous integration
221
+
222
+ [`codemagic.yaml`](codemagic.yaml) runs on Codemagic. Every push and pull
223
+ request runs `rn-verify`: TypeScript, lint, tests with coverage, the package
224
+ build and archive, codegen, the Android example (which compiles the Kotlin
225
+ bridge against `com.amwal-pay:ecr-sdk` from Maven Central) and the iOS example
226
+ for the simulator (which compiles the Swift bridge against `AmwalECR` from
227
+ CocoaPods trunk).
228
+
229
+ ### Releasing to npm
230
+
231
+ Versions follow semantic versioning. A `vX.Y.Z` tag runs `npm-publish`, which
232
+ refuses unless the tag, `package.json` and the top `CHANGELOG.md` entry agree
233
+ and the exact native providers this bridge pins (`android/build.gradle` and
234
+ `RNAmwalEcr.podspec`) are live on Maven Central and trunk. A version already on
235
+ npm is skipped, never re-pushed, and the run ends by reading the version back
236
+ from the registry.
237
+
238
+ ```bash
239
+ git tag -a v0.1.1 -m "react-native-amwal-ecr 0.1.1" && git push origin v0.1.1
240
+ ```
241
+
242
+ Codemagic needs an environment group `npm_credentials` holding `NPM_TOKEN`, an
243
+ npm granular access token with publish rights on `react-native-amwal-ecr`. The
244
+ token lives only there — never in this repository.
245
+
246
+ ### Distributing the example
247
+
248
+ The example (`example/`, bundle `com.amwalpay.amwalEcrRnExample`, package
249
+ `com.amwalpay.amwal_ecr_rn_example`, shown as "Amwal ECR RN Example") can be
250
+ handed to testers without a workstation:
251
+
252
+ | Tag | Workflow | Goes to |
253
+ |---|---|---|
254
+ | `example-ios-<n>` | `example-ios-testflight` | TestFlight, internal group `Testers` |
255
+ | `example-android-<n>` | `example-android-firebase` | Firebase App Distribution, group `tester` |
256
+
257
+ ```bash
258
+ git tag example-ios-1 && git push origin example-ios-1
259
+ git tag example-android-1 && git push origin example-android-1
260
+ ```
261
+
262
+ Both can also be started by hand from Codemagic. The build number is
263
+ Codemagic's `PROJECT_BUILD_NUMBER`, so every build installs over the previous
264
+ one. What has to exist first:
265
+
266
+ - **iOS**: the App Store Connect integration named `Amwal CodeMagic`, and an app
267
+ record in App Store Connect for `com.amwalpay.amwalEcrRnExample`. Signing is
268
+ automatic — the integration's API key fetches or creates the App Store
269
+ provisioning profile; no profile or certificate is uploaded by hand.
270
+ - **Android**: environment group `A` with `FIREBASE_SERVICE_ACCOUNT` (the
271
+ Firebase project's service-account JSON) and `FIREBASE_ANDROID_APP_ID` (the
272
+ Android app registered in Firebase for `com.amwalpay.amwal_ecr_rn_example`).
273
+ The release APK is signed with the template's debug keystore, which App
274
+ Distribution accepts; a store listing would need a real upload keystore.
275
+
276
+ ## License
277
+
278
+ MIT
@@ -0,0 +1,27 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ # CocoaPods output names are case-insensitive. Keep the wrapper product
7
+ # distinct from the native provider's `AmwalECR` product.
8
+ s.name = "RNAmwalEcr"
9
+ s.version = package["version"]
10
+ s.summary = package["description"]
11
+ s.homepage = package["homepage"]
12
+ s.license = package["license"]
13
+ s.authors = package["author"]
14
+
15
+ s.platforms = { :ios => min_ios_version_supported }
16
+ s.source = { :git => "https://github.com/amwal-pay/amwal-ecr-rn.git", :tag => "v#{s.version}" }
17
+
18
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
19
+ s.private_header_files = "ios/**/*.h"
20
+
21
+ # Keep this on the same native provider line as the Flutter bridge. A change
22
+ # in how an outcome is reported is a payment-contract change, so upgrades are
23
+ # deliberate and contract-tested before this range moves.
24
+ s.dependency "AmwalECR", "~> 0.2.1"
25
+
26
+ install_modules_dependencies(s)
27
+ end
@@ -0,0 +1,53 @@
1
+ buildscript {
2
+ ext.AmwalEcr = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36
6
+ ]
7
+
8
+ ext.getExtOrDefault = { prop ->
9
+ if (rootProject.ext.has(prop)) {
10
+ return rootProject.ext.get(prop)
11
+ }
12
+
13
+ return AmwalEcr[prop]
14
+ }
15
+
16
+ repositories {
17
+ google()
18
+ mavenCentral()
19
+ }
20
+
21
+ dependencies {
22
+ classpath "com.android.tools.build:gradle:8.7.2"
23
+ // noinspection DifferentKotlinGradleVersion
24
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
25
+ }
26
+ }
27
+
28
+
29
+ apply plugin: "com.android.library"
30
+ apply plugin: "kotlin-android"
31
+
32
+ apply plugin: "com.facebook.react"
33
+
34
+ android {
35
+ namespace "com.amwalecr"
36
+
37
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
38
+
39
+ defaultConfig {
40
+ minSdkVersion getExtOrDefault("minSdkVersion")
41
+ }
42
+
43
+ compileOptions {
44
+ sourceCompatibility JavaVersion.VERSION_17
45
+ targetCompatibility JavaVersion.VERSION_17
46
+ }
47
+ }
48
+
49
+ dependencies {
50
+ implementation "com.facebook.react:react-android"
51
+ implementation "com.amwal-pay:ecr-sdk:1.0.5"
52
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
53
+ }
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.INTERNET" />
3
+ </manifest>