sure_pay_plugin

pub package Platform License: MIT

A Flutter plugin for integrating SurePay POS payment terminals over TCP/IP.

Wraps the official sureposlibrary-release.aar Android SDK (TcpClient) with a clean, typed Dart API using a Kotlin MethodChannel bridge. Supports purchases, reversals, reconciliation, authorization pre-auth workflows, and refunds — all with strongly-typed responses and exceptions.


Table of contents


Features

Operation Method FID
Get SDK version getSdkVersion()
Check terminal ready checkTerminalReady() 0001
Get terminal info getTerminalInfo() 0010
Purchase purchase() 0005
Reversal reverse() 0003
Get last transaction getLastTransaction() 0002
Reconciliation reconciliation() 0008
Authorization authorization() 0011
Authorization void authorizationVoid() 0012
Authorization extension authorizationExtension() 0013
Purchase advice partial purchaseAdvicePartial() 0014
Purchase advice full purchaseAdviceFull() 0015
Refund with password refundWithPassword() 0017
Refund with client ref + password refundClientRefWithPassword() 0018
Cancel cancel()

Platform support: Android only (requires SurePay AAR SDK).


Installation

1 — Add the dependency

dependencies:
  sure_pay_plugin: ^0.0.1

Then run:

flutter pub get

2 — Place the AAR

The sureposlibrary-release.aar must be present at:

<your_project>/android/libs/sureposlibrary-release.aar

The AAR is bundled inside the plugin and resolved automatically via the local Maven repository configured in build.gradle. No extra steps needed.

3 — Internet permission

Add to your app's android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

4 — Minimum SDK

Your app's android/app/build.gradle must set minSdkVersion 21 or higher.


Terminal setup

Linux POS (port 7070)

  1. Press Backspace or F2 → select Admin Menu
  2. Enter admin password: 145636987
  3. Scroll to INTEGRATION SETTINGS → select ENABLE → POS restarts
  4. Repeat steps 1–2, go to INTEGRATION SETTINGSMEDIATCP/IP → POS restarts

Get the terminal IP:

  • Press F1Internet → find the TIP (dynamic IP) field

Port: always 7070


Quick start

import 'package:sure_pay_plugin/sure_pay_plugin.dart';

// 1. Create the repository once (e.g. in your service locator / DI)
final terminal = SurePayRepository(
  ip: '192.168.1.100',   // terminal IP from F1 → Internet → TIP
  port: 7070,
  clientKey: 'MY_APP_KEY',
);

// 2. Make a purchase
try {
  final tx = await terminal.purchase(
    amount: '10.25',
    reference: 'ORDER-001',
  );

  if (tx.isApproved) {
    print('Approved!');
    print('Card   : ${tx.cardNumber}');   // 506968******1986
    print('Scheme : ${tx.schemaName}');   // mada
    print('Auth   : ${tx.authorizeCode}');
    print('RRN    : ${tx.rrn}');
  } else {
    print('Declined: resp=${tx.responseCode}');
  }

} on SurePayTerminalException catch (e) {
  print('Terminal error: ${e.code}');
  // e.g. CANCEL_KRY_PRESSED, INSERT_TIMEOUT, BUSY_TERMINAL

} on SurePayConnectionException catch (e) {
  print('Connection error: ${e.code}');
  // FAILED_SEND or FAILED_RECEIVE
}

API reference

All methods are on SurePayRepository. All return Future and throw typed exceptions.

getSdkVersion()

final String version = await terminal.getSdkVersion();
// "3.0.7"

checkTerminalReady()

final SurePayTerminalInfo info = await terminal.checkTerminalReady();
print(info.modelName);  // "MP200"

getTerminalInfo()

final SurePayTerminalInfo info = await terminal.getTerminalInfo();
print(info.terminalId);   // "2304142404142404"
print(info.serialNumber); // "000315194814449"
print(info.simMnc);       // "-1" (WiFi) or "01" (STC)

purchase()

final SurePayTransaction tx = await terminal.purchase(
  amount: '10.25',       // required — decimal string in SAR
  reference: 'INV-001',  // optional — echoed back in tx.reference
);

reverse()

final SurePayTransaction tx = await terminal.reverse();
// Reverses the last transaction (within terminal timer window)

getLastTransaction()

final SurePayTransaction tx = await terminal.getLastTransaction();

reconciliation()

final SurePayReconciliation r = await terminal.reconciliation();
print(r.isInBalance);  // true / false
print(r.message);      // "In Balance" / "OUT Of Balance"

authorization()

final SurePayTransaction tx = await terminal.authorization(
  amount: '100.00',
);
// Save tx.rrn, tx.authorizeCode, tx.date for later capture/void

authorizationVoid()

final SurePayTransaction tx = await terminal.authorizationVoid(
  amount: '100.00',
  rrn: tx.rrn!,
  dateTime: '12102022',  // DDMMYYYY from original auth
  authCode: tx.authorizeCode!,
);

authorizationExtension()

final SurePayTransaction tx = await terminal.authorizationExtension(
  rrn: '123456789123',
  dateTime: '12102022',  // DDMMYYYY
  authCode: '097635',
);

purchaseAdvicePartial()

final SurePayTransaction tx = await terminal.purchaseAdvicePartial(
  amount: '75.00',        // actual charged amount (less than auth)
  rrn: '123456789123',
  dateTime: '12102022',
  authCode: '097635',
);

purchaseAdviceFull()

final SurePayTransaction tx = await terminal.purchaseAdviceFull(
  amount: '100.00',       // full authorized amount
  rrn: '123456789123',
  dateTime: '12102022',
  authCode: '097635',
);

refundWithPassword()

final SurePayTransaction tx = await terminal.refundWithPassword(
  amount: '5.00',
  rrn: '162657000078',
  date: '20221107',  // YYYYMMDD
  password: '14789', // 5-digit manager password
);

refundClientRefWithPassword()

final SurePayTransaction tx = await terminal.refundClientRefWithPassword(
  amount: '5.00',
  clientRef: 'SUREPAY-000123456789',  // max 20 chars
  rrn: '162657000078',
  date: '20221107',
  password: '14789',
);

cancel()

await terminal.cancel();
// Cancels any pending in-progress request

Response models

SurePayTransaction

Field Type Description
flag TransactionFlag approved / declined / voided
isApproved bool true when flag=approved AND responseCode="000"
cardNumber String? Masked PAN e.g. 506968******1986
schemaName String? Card scheme: mada, VISA, MASTER
date String? YYMMDDHHmmss e.g. 220328162707
authorizeCode String? Auth code. Empty if declined.
responseCode String? "000" = approved
rrn String? Reference number e.g. 162657000078
merchantId String? Merchant ID
amount String? Decimal string e.g. "0.01"
terminalId String? Terminal ID
reference String? Echo of your client reference

SurePayReconciliation

Field Type Description
isInBalance bool true when result = "003"
result String? "003" = In Balance, "004" = Out Of Balance
message String? "In Balance" / "OUT Of Balance"

SurePayTerminalInfo

Field Type Description
modelName String? MP200, VEGA3000, UPT1000
terminalId String? Terminal ID
serialNumber String? Serial number
simMnc String? -1=WiFi/Eth, 01=STC, 03=Mobily, 04=Zain, 06=Lebara
raw String? Original pipe-delimited string

Error handling

try {
  final tx = await terminal.purchase(amount: '10.25');
} on SurePayTerminalException catch (e) {
  // Terminal returned an error — see TerminalErrorCode constants
  switch (e.code) {
    case TerminalErrorCode.cancelKeyPressed:
      showMessage('Customer cancelled');
      break;
    case TerminalErrorCode.insertTimeout:
      showMessage('Card not presented in time');
      break;
    case TerminalErrorCode.busyTerminal:
      showMessage('Terminal is busy, please wait');
      break;
    default:
      showMessage('Terminal error: ${e.code}');
  }
} on SurePayConnectionException catch (e) {
  // TCP/IP communication failure
  showMessage('Cannot reach terminal: ${e.code}');
} on SurePayArgumentException catch (e) {
  // Programming error — a required argument was missing
  showMessage('Invalid request: ${e.message}');
}

Error code reference

Terminal errors (SurePayTerminalException.code)

Use the TerminalErrorCode constants:

Constant Value Description
TerminalErrorCode.pinCanceled PIN_CANCELED Customer cancelled PIN entry
TerminalErrorCode.amountNotFound AMOUNT_NOT_FOUND Amount field missing
TerminalErrorCode.insertTimeout INSERT_TIMEOUT Card not inserted in time
TerminalErrorCode.insertCanceled INSERT_CANCELED Card insertion cancelled
TerminalErrorCode.cancelKeyPressed CANCEL_KRY_PRESSED Cancel key pressed on terminal
TerminalErrorCode.cancelConfirmAmount CANCEL_CONFIRM_AMOUNT Amount confirmation cancelled
TerminalErrorCode.iccTransactionFailed ICC_TRANSACTION_FAILED Chip card transaction failed
TerminalErrorCode.transactionNotAllowed TRANSACTION_NOT_ALLOWED Transaction type not allowed
TerminalErrorCode.manualEntryNotAllowed MANUAL_INTER_NOT_ALLOWED Manual card entry disabled
TerminalErrorCode.functionNotSupported FUNCTION_NOT_SUPPORTED Function not supported by terminal
TerminalErrorCode.noIncomeRequest NO_INCOME_REQUEST No incoming request available
TerminalErrorCode.noRecords NO_RECORDS No transaction records found
TerminalErrorCode.notSupported NOT_SUPPORTED Not supported
TerminalErrorCode.checkConnection CHECK_CONNECTION Terminal not reachable
TerminalErrorCode.noReverse NO_REVERSE No reversal available
TerminalErrorCode.reversalTimerExpired REVERSAL_TIMER_EXPIRED Reversal window expired
TerminalErrorCode.busyTerminal BUSY_TERMINAL Terminal is processing another request
TerminalErrorCode.unknownError UNKNOWN_ERROR Unclassified error
TerminalErrorCode.msgFormatError MSG_FORMAT_ERROR Malformed message
TerminalErrorCode.invalidIp INTEGRATION_ERR_INVALID_IP Invalid IP address
TerminalErrorCode.invalidClientKey INTEGRATION_ERR_INVALID_CLIENT_KEY Invalid client key

Connection errors (SurePayConnectionException.code)

Value Description
FAILED_SEND Could not send data to terminal
FAILED_RECEIVE Could not receive response from terminal

Wire protocol reference

The SDK handles the TCP/IP framing internally. For reference the raw protocol is:

Request  : 0x02 + ASCII data bytes + 0x03
Response : 0x02 + ASCII data bytes + 0x03

Purchase request (SAR 10.25):

02  30 30 30 35  30 30 30 30 30 30 30 30 30 31 30 32 35  03
STX    "0005"         "000000010250" (in smallest unit)  ETX

Successful response (pipe-delimited):

"0|506968******1986|mada|220328162707|097635|000|162657000078|201075999568|0.01|2386300060647280"
 ^ flag=0 (approved)
                                              ^^^responseCode=000

Error response:

"-2"   →  ERR_CANCEL_KEY_PRESSED
"-59"  →  ERR_NO_CONNECTION

Integration with BLoC

Example Cubit using this plugin:

// payment_cubit.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sure_pay_plugin/sure_pay_plugin.dart';

part 'payment_state.dart';

class PaymentCubit extends Cubit<PaymentState> {
  final SurePayRepository _terminal;

  PaymentCubit(this._terminal) : super(PaymentIdle());

  Future<void> purchase({required String amount, required String reference}) async {
    emit(PaymentLoading());
    try {
      final tx = await _terminal.purchase(amount: amount, reference: reference);
      if (tx.isApproved) {
        emit(PaymentApproved(tx));
      } else {
        emit(PaymentDeclined(tx));
      }
    } on SurePayTerminalException catch (e) {
      emit(PaymentError(e.code));
    } on SurePayConnectionException catch (e) {
      emit(PaymentError(e.code));
    }
  }
}

// payment_state.dart
abstract class PaymentState {}
class PaymentIdle     extends PaymentState {}
class PaymentLoading  extends PaymentState {}
class PaymentApproved extends PaymentState { final SurePayTransaction tx; PaymentApproved(this.tx); }
class PaymentDeclined extends PaymentState { final SurePayTransaction tx; PaymentDeclined(this.tx); }
class PaymentError    extends PaymentState { final String code; PaymentError(this.code); }

Provide it in your widget tree:

BlocProvider(
  create: (_) => PaymentCubit(
    SurePayRepository(ip: '192.168.1.100', port: 7070, clientKey: 'MY_KEY'),
  ),
  child: CheckoutPage(),
)

FAQ

Q: The terminal IP changes on every restart — how do I handle that?

A: On Linux POS, the IP is dynamic. Store it in your app's settings and let the cashier update it. You can also configure a static IP on the terminal's network settings or assign a DHCP reservation by MAC address on your router.

Q: I get FAILED_RECEIVE on every call.

A: Check that:

  1. The terminal and Android POS device are on the same network/VLAN.
  2. TCP/IP integration is enabled on the terminal (see Terminal Setup above).
  3. Port 7070 is not blocked by a firewall.
  4. No other app on the network is already connected to the terminal.

Q: Can I run two concurrent purchases?

A: No. The terminal processes one request at a time. The cancel() method can abort a stuck in-progress request.

Q: Does this work on iOS?

A: No. The plugin uses the SurePay Android AAR which has no iOS equivalent. iOS support would require a separate integration.

Q: What's the clientKey parameter?

A: It is your app's unique identifier sent with every request so the terminal can associate responses with the correct caller. Any non-empty string works — the sample app uses "test_900".

Q: How do I run the example app?

A:

cd example
flutter run

Enter your terminal's IP and port in the connection bar, then tap any operation button.

Libraries

sure_pay_plugin
SurePay Plugin