Skip to content
logo
  • Products
    • Amwal Checkout
    • Merchant App
    • Merchant Control Panel
  • Pricing
  • Developers
  • About us
  • Contact Us
Edit Content
  • Products
    • Amwal Checkout
    • Merchant App
    • Merchant Control Panel
  • Pricing
  • Developers
  • About us
  • Contact Us
Login
Get Started
Login
Get Started
BA Booking

BA Booking

2
  • BA Booking overview
  • BA Booking Installation
amwalpay woocommerce

Woocommerce

3
  • WooCommerce Subscription & Recurring Payments
  • Overview
  • Woocommerce Installation
CS-Cart

CS Cart

2
  • CS-Cart Overview
  • CS CART Installation
Shopify

Shopify

2
  • Shopify Overview
  • Shopify Installation
odoo

Odoo

2
  • Odoo Overview
  • Odoo Installation

Laravel Package

2
  • Installation
  • Configuration
whmcs

WHMCS

2
  • WHMCS Overview
  • WHMCS Installation
smartbox

SMARTBOX

4
  • Express ApplePay Implementation
  • Offsite Implementation
  • Pre Requisites
  • Implementation
merchant cloud notification

Merchant Cloud Notification

1
  • Merchant Cloud Notification Integration Guide
Magento

Magento

2
  • Magento 2 Overview
  • Magento Installation
secure hash calculation

Secure Hash Calculation

1
  • Secure Hash Calculation
Opencart

Opencart

2
  • OpenCart Overview
  • Opencart Installtion
wp travel engine

Wp Travel Engine

1
  • Wp Travel Engine Installation
ZenCart

ZenCart

2
  • ZenCart Overview
  • Zencart Installation
webhook

Webhooks

12
  • Get Order by Merchant Reference
  • Get Transaction by Merchant Reference
  • Acquiring Session Token
  • Initialize Payment
  • Encryption and Decryption
  • Refund Payment
  • Void Payment
  • Get Transaction by ID
  • Transactions with Statistics
  • Transactions Summary
  • Delete Customer Token
  • Pay by Token
Drupal

Drupal

2
  • Drupal Overview
  • Drupal Installation
PrestaShop

PrestaShop

2
  • PrestaShop Overview
  • PrestaShop Installation
Contact Form 7

Contact Form 7

2
  • Contact Form 7 Overview
  • Contact Form 7 Installation
Joomla

Joomla

2
  • Joomla Overview
  • Joomla Installation
AMWAL INTEGRATED PAYMENT LINK

Integrated Payment Link

1
  • Implementation
Android SDk

Native Android SDK

2
  • Pre Requisites
  • Implementation
ios SDk

Native iOS SDK

3
  • Apple Pay Specific Configuration
  • Pre Requisites
  • Implementation
React SDk

React Native SDK

1
  • Implementation

Flutter SDK

2
  • Implementation
  • Flutter SDK Overview
andriod ecr

POS ECR

6
  • ECR SDK for .NET
  • Flutter ECR SDK
  • ECR SDK for iOS Cocoapods
  • ECR SDK for iOS SPM
  • Android ECR
  • React Native ECR
View Categories
  • Home
  • Amwal Pay Developer Portal
  • POS ECR
  • React Native ECR

React Native ECR

Drive an Amwal POS terminal from a React Native Android or iOS application over Wi-Fi, the Amwal payment host (Web Service), and on Android also a USB cable or the Amwal payment app on the same device. The package is a typed bridge over the same published native providers used by the Amwal Flutter package:

  • Android: com.amwal-pay:ecr-sdk:1.0.6 from Maven Central, or a local ECR-simulator/ecr-sdk module
  • iOS: AmwalECR ~> 0.2.1 from CocoaPods trunk, or a local AmwalECR-iOS-CocoaPods / AmwalECR-iOS-SPM checkout

The bridge contains no payment protocol implementation. Android and iOS use the native Amwal ECR SDKs unchanged and return the same outcomes for sale, void, refund, inquiry, and e-receipt operations.

The one rule #

A failed exchange is not a decline. A timeout or lost connection can happen after the terminal accepted the request and completed the payment. When outcomeIsUnknown is true, do not send the sale again. Inquire using the same merchantReferenceId and reconcile first.

const result = await terminal.sale('1.234', {
  merchantReferenceId: order.id,
});

switch (result.outcome) {
  case 'approved':
    completeOrder(result.amount, result.rrn);
    break;
  case 'declined':
    if (result.outcomeIsUnknown) {
      await terminal.inquireByReference(result.merchantReferenceId);
    } else {
      showDecline(result.reason);
    }
    break;
  case 'failed':
    if (result.settled && result.recovered?.outcome === 'found') {
      reconcile(result.recovered.transaction);
    } else if (result.outcomeIsUnknown) {
      await terminal.inquireByReference(result.merchantReferenceId);
    }
    break;
}

Install #

yarn add react-native-amwal-ecr

React Native autolinking installs the Android module. On iOS, install pods after adding the package:

cd ios
pod install

The package requires React Native’s New Architecture/TurboModules, Java 17, Android API 24+, and iOS 15.1+ (the effective minimum of current React Native; the native Amwal ECR pod itself supports iOS 12+).

iOS local-network permission #

Add a user-facing reason to the application Info.plist:

<key>NSLocalNetworkUsageDescription</key>
<string>Connects to the payment terminal to take card payments.</string>

Android’s manifest entries are contributed by the library and merge into your app automatically:

  • INTERNET and ACCESS_NETWORK_STATE for Wi-Fi and Web Service.
  • uses-feature android.hardware.usb.host (required="false"), so USB cable mode does not exclude devices without a USB host port.
  • <queries> for the Amwal payment app (com.amwalpay.pos) and its com.amwalpay.pos.ecr.TRANSACTION action, so app to app can see whether the payment app is installed on Android 11+.

Create a terminal #

import { EcrTerminal } from 'react-native-amwal-ecr';

const terminal = new EcrTerminal({
  host: '192.168.1.50',
  serialNumber: 'P653200085189',
  transport: 'wifi',
  config: {
    ecrId: 'TILL-01',
    currencyCode: '512',
    minorUnitDigits: 3,
    port: 9100,
    connectTimeoutMs: 10_000,
    responseTimeoutMs: 120_000,
    probeTimeoutMs: 3_000,
    secureHashKey: secretFromSecureStorage,
    autoInquireOnFailure: true,
  },
});

The ECR secret is issued by Amwal per terminal. Keep it in the platform keychain or secure storage; never place it in source, logs, analytics, or crash reports.

Only ethernet and wifi open a local ECR listener. webService drives the terminal through the Amwal payment host over REST instead — no host address, no local network:

const hostedTerminal = new EcrTerminal({
  serialNumber: 'P653200085189',
  transport: 'webService',
  config: {
    merchantId: '13593',
    terminalId: '742001',
    environment: 'UAT', // 'SIT' | 'UAT' | 'PROD'
    secureHashKey: webServiceSecretFromSecureStorage,
  },
});

The Web Service path signs with its own secret (different from the LAN key); both are issued by Amwal and both travel through secureHashKey. Receipt fetch is only available on the local transports, and isReachable() resolves false for webService — there is no local listener to probe.

Transport support #

Modes match the TMS ecrMode values.

TransportecrModeAndroidiOS
usbCable1Driven over a USB accessory channelunsupported, nothing sent
wifi (and ethernet)2DrivenDriven
bluetooth3Refused before anything is sentRefused before anything is sent
webService4Driven over RESTDriven over REST
appToApp5Hands the request to com.amwalpay.pos on the same deviceunsupported, nothing sent

The iOS SDK (AmwalECR 0.2.2, the same pod the Flutter bridge uses) can describe a USB cable link, but the till has to supply the byte channel and this bridge does not open a cable, matching Flutter on iOS. Wi-Fi, ethernet (as Wi-Fi), and Web Service go through EcrSessions.plan / EcrSessions.open on both platforms.

On Android, usbCable and appToApp need no host. For appToApp the payment app must be installed and a foreground Activity must exist, because the request is an Activity result exchange. The library never launches the payment app just to check for it. isReachable() resolves false for every transport that has no local listener to probe.

Sign-on and close receipt #

signOn() asks the terminal what it is and which transactions it permits. closeReceipt() tells it to leave its receipt and return to idle.

const signOn = await terminal.signOn();
if (signOn.outcome === 'available') {
  const { terminalName, ecrMode, permitted } = signOn.capabilities;
  // permitted: [{ messageType, minAmount, maxAmount }, ...]
}

await terminal.closeReceipt();
TransportsignOn() / closeReceipt()
Wi-FiAndroid and iOS
USB cableAndroid only
App to app, Web Service, BluetoothAnswer without sending anything

signOn() returns available, unavailable (the terminal answered but is not ready; capabilities.reason says why), or failed with a typed failure. It has a cancellable startSignOn() form. A declined transaction can carry capabilities when the terminal refused because its profile changed.

Operations #

Amounts are plain decimal strings in major units. Never pass a JavaScript number: binary floating point cannot exactly represent OMR amounts such as 1.234.

await terminal.isReachable();

await terminal.sale('1.234', {
  merchantReferenceId: 'ORDER-1001',
});

await terminal.voidTransaction('123', {
  originalTerminalId: 'TILL-01',
  merchantReferenceId: 'VOID-1001',
});

await terminal.refund('0.500', {
  receiptNumber: '123',
  transactionDate: '20260902',
  originalTerminalId: 'TILL-01',
  merchantReferenceId: 'REFUND-1001',
});

await terminal.inquire({
  receiptNumber: '123',
  transactionDate: '20260902',
});

await terminal.inquireByReference('ORDER-1001');

await terminal.receipt({
  receiptNumber: '123',
  transactionDate: '20260902',
});

inquire, inquireByReference, and receipt are read-only and safe to repeat. The native SDK also follows an unknown money-moving failure with one inquiry by reference when autoInquireOnFailure is enabled.

Cancellation #

Every operation has a start... form that returns an EcrOperation:

const operation = terminal.startSale('1.234', {
  merchantReferenceId: 'ORDER-1001',
});

cancelButton.onPress = () => operation.cancel();
const result = await operation.result;

Cancellation stops this application waiting; it does not reverse or stop the terminal. A cancelled money-moving request returns a failed result with an unknown outcome. Inquire before retrying.

The cancellable forms are startSale, startVoid, startRefund, startInquire, startInquireByReference, and startReceipt.

Result model #

EcrResult is a discriminated union:

  • approved: money moved; includes amount, RRN, authorisation code, masked PAN, and partial-approval fields.
  • declined: the terminal answered and refused; code 91 remains unknown and requests an inquiry.
  • failed: the exchange did not produce a trustworthy answer; includes a typed failure and an optional recovered inquiry.

Failure kinds are unreachable, timeout, malformed, connectionLost, unauthenticated, cancelled, and unsupported. The public outcomeIsUnknown flag is the decision field callers should use.

Local native SDKs #

The example always loads this package. The properties files choose how that package resolves ecr-sdk and AmwalECR, the same way amwal_ecr does.

PlatformPackage configExample override
Androidandroid/gradle.propertiesexample/android/gradle.properties
iOSios/ecr_sdk.propertiesexample/ios/ecr_sdk.properties

The package defaults to the published SDKs (maven / cocoapods). On Android, Gradle reads the library module’s own android/gradle.properties in preference to the example’s, so the example’s ecrSdkDependency=project does not take effect by itself. Pass it on the command line to build the example against the local ECR-simulator:

cd example
npx react-native run-android --extra-params "-PecrSdkDependency=project"
# or: cd android && ./gradlew :app:assembleDebug -PecrSdkDependency=project

The build log states which source was used: “Using ECR SDK from project module :ecr-sdk” or “from Maven”.

Android ecrSdkDependency:

ValueUse when
projectLive :ecr-sdk from sibling ECR-simulator (the example default)
jarcd ../ECR-simulator && ./gradlew :ecr-sdk:jar
mavenPublished com.amwal-pay:ecr-sdk

iOS ecrSdkDependency:

ValueWhat the example Podfile does
projectpod 'AmwalECR', :path => sibling AmwalECR-iOS-CocoaPods
cocoapodsTrunk, from RNAmwalEcr.podspec
spmPrints the sibling AmwalECR-iOS-SPM path to add in Xcode

Paths assume this package, ECR-simulator, AmwalECR-iOS-CocoaPods, and AmwalECR-iOS-SPM sit under the same parent directory. One-off:

ECR_SDK_DEPENDENCY=cocoapods pod install

Development #

corepack yarn install        # or: node .yarn/releases/yarn-4.11.0.cjs install
corepack yarn verify

cd example/android
./gradlew :app:assembleDebug

cd ../ios
pod install
xcodebuild -workspace AmwalEcrExample.xcworkspace \
  -scheme AmwalEcrExample -sdk iphonesimulator \
  -configuration Debug CODE_SIGNING_ALLOWED=NO build

The repository pins Yarn 4.11 (.yarnrc.yml), so the bundled release runs even where yarn and corepack are not installed. Install from the repository root: the example is a workspace and gets its dependencies from there.

The example saves its terminals and Wi-Fi / Web Service signing keys on the device with @react-native-async-storage/async-storage. That is convenient for a sample till; a production app should keep the ECR secret in the platform keychain or keystore instead.

Running the example #

cd example
npx react-native start
npx react-native run-android   # or run-ios
  • Android on a USB device: run adb reverse tcp:8081 tcp:8081 so the device can reach Metro. Without it the app shows “Unable to load script”. The mapping is lost when the device reconnects.
  • After adding or updating a native dependency: rebuild the app. A JS reload is not enough. On Android, delete example/android/build/generated/autolinking if Gradle keeps the old module list; on iOS, run pod install again. NativeModule: AsyncStorage is null means the installed app predates that dependency.
  • iOS: the example Podfile raises every pod’s deployment target to the project minimum, because some resource bundles (for example AsyncStorage’s) default to iOS 13.4, which current Xcode rejects.
  • Metro cache: if a newly installed package is still “unable to resolve”, restart Metro with --reset-cache.

Continuous integration #

codemagic.yaml runs on Codemagic. Every push and pull request runs rn-verify: TypeScript, lint, tests with coverage, the package build and archive, codegen, the Android example (which compiles the Kotlin bridge against com.amwal-pay:ecr-sdk from Maven Central) and the iOS example for the simulator (which compiles the Swift bridge against AmwalECR from CocoaPods trunk).

Releasing to npm #

Versions follow semantic versioning. A vX.Y.Z tag runs npm-publish, which refuses unless the tag, package.json and the top CHANGELOG.md entry agree and the exact native providers this bridge pins (android/build.gradle and RNAmwalEcr.podspec) are live on Maven Central and trunk. A version already on npm is skipped, never re-pushed, and the run ends by reading the version back from the registry.

git tag -a v0.1.1 -m "react-native-amwal-ecr 0.1.1" && git push origin v0.1.1

Codemagic needs an environment group npm_credentials holding NPM_TOKEN, an npm granular access token with publish rights on react-native-amwal-ecr. The token lives only there — never in this repository.

Distributing the example #

The example (example/, bundle com.amwalpay.amwalEcrRnExample, package com.amwalpay.amwal_ecr_rn_example, shown as “Amwal ECR RN Example”) can be handed to testers without a workstation:

TagWorkflowGoes to
example-ios-<n>example-ios-testflightTestFlight, internal group Testers
example-android-<n>example-android-firebaseFirebase App Distribution, group tester
git tag example-ios-1 && git push origin example-ios-1
git tag example-android-1 && git push origin example-android-1

Both can also be started by hand from Codemagic. The build number is Codemagic’s PROJECT_BUILD_NUMBER, so every build installs over the previous one. What has to exist first:

  • iOS: the App Store Connect integration named Amwal CodeMagic, and an app record in App Store Connect for com.amwalpay.amwalEcrRnExample. Signing is automatic — the integration’s API key fetches or creates the App Store provisioning profile; no profile or certificate is uploaded by hand.
  • Android: environment group A with FIREBASE_SERVICE_ACCOUNT (the Firebase project’s service-account JSON) and FIREBASE_ANDROID_APP_ID (the Android app registered in Firebase for com.amwalpay.amwal_ecr_rn_example). The release APK is signed with the template’s debug keystore, which App Distribution accepts; a store listing would need a real upload keystore.
Updated on October 15, 2026

What are your Feelings

  • Happy
  • Normal
  • Sad

Share This Article :

  • Facebook
  • X
  • LinkedIn
  • Pinterest
Table of Contents
  • The one rule
  • Install
    • iOS local-network permission
  • Create a terminal
    • Transport support
    • Sign-on and close receipt
  • Operations
  • Cancellation
  • Result model
  • Local native SDKs
  • Development
    • Running the example
  • Continuous integration
    • Releasing to npm
    • Distributing the example

Secure. Seamless – Powering Payments for Every Business.

Sign Up
Support

4th Floor, Majan Tower Building
North Al Ghubrah, P.O. Box 233, P.C 118
Muscat, Sultanate of Oman

: support@amwal-pay.com

📞 : +96824121845

Resources
  • Developers
  • Careers
Company
  • About us
  • Contact Us
  • Contact Sales
  • Partners

2026 © AmwalPay. All Rights Reserved.

  • Terms & Conditions
  • Privacy Policy