scan_to_pay 1.0.1 copy "scan_to_pay: ^1.0.1" to clipboard
scan_to_pay: ^1.0.1 copied to clipboard

Reusable Flutter SDK for scan-to-pay: OCR-based account-number capture, multi-frame confirmation, pluggable bank resolver, and themeable UI.

scan_to_pay #

Reusable Flutter SDK for scan-to-pay: point a camera at a written/printed account number, get back a confirmed ScanResult (account number + bank + confidence) ready for your transfer flow.

This package packages a production-proven scan-to-pay architecture with a mature OCR pipeline, multi-frame confirmation, and fuzzy bank matching — now as a one-widget drop-in any Flutter app can integrate.

  • Zero shared state. The SDK never decides what happens next; it hands you a ScanResult and your app takes it from there.
  • Themeable end-to-end. Accent color, fonts, and full widget slots for the viewfinder and confirmation sheet.
  • Pluggable bank resolver. A built-in Nigerian bank resolver is used by default, or you can inject your own (remote API, wallet switcher, etc.).
  • Analytics hook. Optional ScanToPayAnalyticsDelegate so host apps can pipe frame timings, lock events, and errors into their own telemetry.

Install #

dependencies:
  scan_to_pay: ^1.0.1

Quick start #

import 'package:flutter/material.dart';
import 'package:scan_to_pay/scan_to_pay.dart';

Future<void> openScanner(BuildContext context) async {
  await ScanToPayLauncher.push(
    context,
    config: ScanToPayConfig(
      theme: const ScanToPayTheme(accentColor: Color(0xFFD32F2F)),
      onAccountResolved: (ScanResult result) async {
        await Navigator.of(context).push(
          MaterialPageRoute(
            builder: (_) => TransferConfirmPage(result: result),
          ),
        );
      },
    ),
  );
}

That's it. The SDK wires up the camera, ML Kit OCR, multi-frame voting, the default Nigerian bank resolver, the viewfinder, and the confirmation sheet. When the user taps Continue, your onAccountResolved callback receives a ScanResult and you drive the rest of the flow.

Permissions #

iOS #

Add to ios/Runner/Info.plist:

<key>NSCameraUsageDescription</key>
<string>Used to scan account numbers for payments.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Used to pick an existing photo to scan an account number.</string>

Minimum iOS deployment target: 15.5.

Android #

Add to android/app/src/main/AndroidManifest.xml:

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

Minimum minSdkVersion: 21.

Customisation #

Theming #

ScanToPayConfig(
  theme: ScanToPayTheme(
    accentColor: Colors.teal,
    backgroundColor: Colors.black,
    sheetBackgroundColor: const Color(0xFF0F0F0F),
    fontFamily: 'Kumbh Sans',
    title: 'Scan to Pay',
  ),
  onAccountResolved: ...,
);

Replace the viewfinder or confirmation sheet entirely #

ScanToPayTheme(
  accentColor: Colors.teal,
  viewfinderBuilder: (context, state) => MyCustomViewfinder(state: state),
  accountSheetBuilder: (context, result, {required onConfirm, required onRescan}) {
    return MyCustomSheet(result: result, onConfirm: onConfirm, onRescan: onRescan);
  },
);

Plug in a custom bank resolver #

Default (Nigerian bank list, no network):

ScanToPayConfig(
  // bankResolver is optional; DefaultNigerianBankResolver is used when omitted.
  onAccountResolved: ...,
);

Remote-first, fall back to built-in list:

final resolver = ChainedBankResolver([
  RemoteBankResolver(
    uri: Uri.parse('https://your-api.com/v1/banks'),
    headers: {'Authorization': 'Bearer $token'},
  ),
  DefaultNigerianBankResolver(),
]);

ScanToPayConfig(
  bankResolver: resolver,
  onAccountResolved: ...,
);

Write your own:

class MyBankResolver extends BankResolver {
  @override
  Future<BankInfo?> resolve({
    required String accountNumber,
    required String rawOcrText,
  }) async {
    final bank = await myApi.lookupBank(accountNumber);
    return bank == null ? null : BankInfo(name: bank.name, code: bank.code);
  }

  @override
  List<String> get knownBankNames => myApi.cachedNames;
}

Behaviour knobs #

ScanToPayConfig(
  requiredConfirmations: 3,                       // frames that must agree
  processInterval: Duration(milliseconds: 600),   // frame throttle
  enableGalleryPicker: true,                      // show "Scan from photo"
  hintMessages: ['Align the digits', 'Hold steady'],
  focusRefreshInterval: Duration(seconds: 2),
  minAccountLength: 10,                           // NUBAN = 10 on both edges
  maxAccountLength: 10,
  onAccountResolved: ...,
);

Telemetry #

class MyAnalytics extends ScanToPayAnalyticsDelegate {
  @override
  void onAccountLocked(ScanResult result) => track('scan_locked', result);
  @override
  void onError(Object error, StackTrace stack) => track('scan_error', error);
}

ScanToPayConfig(
  analyticsDelegate: MyAnalytics(),
  onAccountResolved: ...,
);

Architecture at a glance #

┌──────────────────────────────────────────────────────────────┐
│ Host app (your Flutter app)                                  │
│ Provides: ScanToPayTheme · onAccountResolved · bankResolver  │
└───────────────────────┬──────────────────────────────────────┘
                        │
┌───────────────────────▼──────────────────────────────────────┐
│ ScanToPayLauncher.push(context, config: ...)                 │
└───────────────────────┬──────────────────────────────────────┘
                        │
         ┌──────────────┼──────────────┐
         ▼              ▼              ▼
   CameraEngine     OcrEngine    AccountNumberExtractor
   (camera)         (ML Kit)     + HandwritingValidator
         │              │              │
         └──────────────▼──────────────┘
                 ScanOrchestrator
        (vote bus · post-lock buffer · session state)
                        │
                        ▼
                  BankResolver
                        │
                        ▼
                   ScanResult  ──►  onAccountResolved (host)

The orchestrator is pure Dart (no BuildContext, no setState). Its state stream is what ScanToPayView and any custom UI you build consume.

Why not just copy the code? #

Because every project that adds scan-to-pay ends up:

  1. Re-implementing the camera lifecycle + permissions + disposal.
  2. Re-implementing NV21/BGRA8888 + InputImage rotation handling.
  3. Re-implementing the frame throttle + multi-frame vote + post-lock buffer.
  4. Re-inventing the NUBAN regex, digit/letter transliteration, context scoring.
  5. Re-building a viewfinder + account-found sheet + bank picker.

This package ships all of that behind a single ScanToPayLauncher.push(...) call while leaving theming, bank resolution, and the post-scan flow entirely in the host app's hands.

License #

MIT

1
likes
160
points
12
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Reusable Flutter SDK for scan-to-pay: OCR-based account-number capture, multi-frame confirmation, pluggable bank resolver, and themeable UI.

Repository (GitHub)
View/report issues

Topics

#ocr #scan #payments #nuban #bank

License

MIT (license)

Dependencies

camera, flutter, google_mlkit_text_recognition, http, image_picker, meta, permission_handler

More

Packages that depend on scan_to_pay