Flutter NearPay Odoo Integration - Developer Guide

Flutter NearPay Odoo Integration

Developer Guide for Seamless Payment Processing

Version 1.0
For Flutter Developers
Modules ics_pos_nearpay_integration + ics_account_nearpay_payment
Target Odoo 18

Overview

The NearPay-Odoo integration enables seamless payment processing through a Flutter mobile application communicating with Odoo's POS and Accounting modules.

Key Features

  • POS Payments: Process payments via NearPay terminal with customer display and automatic receipt printing
  • Invoice Payments: Pay invoices from Accounting module with transaction metadata storage
  • Bidirectional Communication: Odoo communicates with Flutter via InAppWebView bridge
  • Real-time Logging: Complete transaction logging and reconciliation

Key Concepts

  • WebView Bridge: Odoo runs in a Flutter InAppWebView and communicates via JavaScript handlers
  • Single Payment Channel: Both POS and Invoice payments use the same payment_request handler
  • Callback Pattern: Flutter returns results via window.paymentComplete()
  • Base64 PDF Printing: Receipts are sent as Base64-encoded PDFs to Flutter for printing

Architecture

Communication Architecture

┌─────────────────────────┐ │ Odoo Backend │ │ (Python/Odoo) │ └──────────┬──────────────┘ │ RPC calls │ ┌──────────▼──────────────┐ │ Odoo WebView UI │ │ (JavaScript/OWL) │ └──────────┬──────────────┘ │ Handler calls │ ┌──────────▼──────────────┐ │ Flutter InAppWebView │ │ Controller │ └──────────┬──────────────┘ │ NearPay SDK calls │ ┌──────────▼──────────────┐ │ NearPay Terminal │ │ (Payment Device) │ └─────────────────────────┘

Communication Channels

Direction From To Method Status
Odoo → Flutter Odoo WebView Flutter InAppWebView callHandler("payment_request", payload) Active
Odoo → Flutter Odoo WebView Flutter InAppWebView callHandler("print_request", data) Active
Odoo → Flutter Odoo WebView Flutter InAppWebView callHandler("customer_display", url) Active
Flutter → Odoo Flutter WebView JavaScript window.paymentComplete(result) Active

Setup & Dependencies

Flutter Dependencies

Add to pubspec.yaml:

dependencies:
  flutter_inappwebview: ^6.0.0
  nearpay_flutter_sdk: ^1.0.0
  json_annotation: ^4.8.0
  
dev_dependencies:
  json_serializable: ^6.7.0

Android Configuration

Update android/app/build.gradle:

android {
    compileSdkVersion 34
    
    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 34
    }
}

iOS Configuration

Update ios/Podfile:

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
    target.build_configurations.each do |config|
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',
        'PERMISSION_NEARPAY=1',
      ]
    end
  end
end

Implementation Guide

1. Basic WebView Setup

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:nearpay_flutter_sdk/nearpay_flutter_sdk.dart';

class OdooNearPayBridge extends StatefulWidget {
  final String odooUrl;
  final String sessionToken;

  const OdooNearPayBridge({
    Key? key,
    required this.odooUrl,
    required this.sessionToken,
  }) : super(key: key);

  @override
  State<OdooNearPayBridge> createState() => _OdooNearPayBridgeState();
}

class _OdooNearPayBridgeState extends State<OdooNearPayBridge> {
  InAppWebViewController? _webViewController;
  NearPay? _nearPay;

  @override
  void initState() {
    super.initState();
    _initializeNearPay();
  }

  Future<void> _initializeNearPay() async {
    _nearPay = await NearPay.instance;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Odoo POS + Invoicing')),
      body: InAppWebView(
        initialUrlRequest: URLRequest(
          url: Uri.parse(widget.odooUrl),
          headers: {'Authorization': 'Bearer ${widget.sessionToken}'},
        ),
        initialSettings: InAppWebViewSettings(
          javaScriptEnabled: true,
          javaScriptCanOpenWindowsAutomatically: true,
        ),
        onWebViewCreated: _onWebViewCreated,
      ),
    );
  }

  void _onWebViewCreated(InAppWebViewController controller) {
    _webViewController = controller;
    _registerPaymentHandlers(controller);
  }
}

2. Register Payment Handlers

void _registerPaymentHandlers(InAppWebViewController controller) {
  // Payment processor (POS + Invoices)
  controller.addJavaScriptHandler(
    handlerName: 'payment_request',
    callback: (args) => _handlePaymentRequest(args, controller),
  );

  // Receipt printing
  controller.addJavaScriptHandler(
    handlerName: 'print_request',
    callback: (args) => _handlePrintRequest(args),
  );

  // Customer display
  controller.addJavaScriptHandler(
    handlerName: 'customer_display',
    callback: (args) => _handleCustomerDisplay(args),
  );
}

3. Payment Request Handler

Future<Map<String, dynamic>> _handlePaymentRequest(
  List<dynamic> args,
  InAppWebViewController controller,
) async {
  if (args.isEmpty) {
    return {'success': false, 'error': 'Invalid payload'};
  }

  try {
    final payload = args[0] as Map<String, dynamic>;
    
    final amount = payload['amount'] as num?;
    final currency = payload['currency'] as String? ?? 'SAR';

    if (amount == null || amount <= 0) {
      throw Exception('Invalid amount: $amount');
    }

    // Call NearPay SDK
    final result = await _nearPay?.pay(
      amount: amount.toDouble(),
      currency: currency,
    );

    if (result == null) {
      throw Exception('No result from NearPay');
    }

    // Send result back to Odoo
    await controller.evaluateJavascript(
      source: 'window.paymentComplete(${jsonEncode(result)});',
    );

    return {'success': true};
  } catch (e) {
    await controller.evaluateJavascript(
      source: '''
        window.paymentComplete({
          "error": "${e.toString().replaceAll('"', '\\"')}",
          "is_approved": false
        });
      ''',
    );

    return {'success': false, 'error': e.toString()};
  }
}

4. Print Handler

Future<Map<String, dynamic>> _handlePrintRequest(List<dynamic> args) async {
  try {
    final payload = args[0] as Map<String, dynamic>;
    final base64File = payload['file'] as String?;

    if (base64File == null || base64File.isEmpty) {
      return {'success': false, 'error': 'No file provided'};
    }

    final bytes = base64Decode(base64File);

    // TODO: Implement your printing logic here
    // Options:
    // - Bluetooth printer package
    // - Android Print framework
    // - Cloud print service

    return {'success': true, 'message': 'Receipt sent to printer'};
  } catch (e) {
    return {'success': false, 'error': e.toString()};
  }
}

5. Customer Display Handler

Future<Map<String, dynamic>> _handleCustomerDisplay(
  List<dynamic> args,
) async {
  try {
    final displayPath = args[0] as String?;

    if (displayPath == null) {
      return {'success': false, 'error': 'No display path'};
    }

    if (displayPath.contains('payment')) {
      _showPaymentDisplay();
    } else if (displayPath.contains('thanks')) {
      _showThanksDisplay();
    } else if (displayPath.contains('welcome')) {
      _showWelcomeDisplay();
    }

    return {'success': true};
  } catch (e) {
    return {'success': false, 'error': e.toString()};
  }
}

void _showPaymentDisplay() {
  // Implement customer-facing payment screen
}

void _showThanksDisplay() {
  // Show thank you and transaction details
}

void _showWelcomeDisplay() {
  // Show welcome/standby screen
}

Handler Reference

payment_request

Called By: Odoo POS (payment screen) and Accounting (invoice payment wizard)

Example Payload

{ "amount": 190.50, "currency": "SAR", "invoice_ids": [123, 456], "invoice_numbers": ["INV/2024/001", "INV/2024/002"], "partner_name": "Customer Name", "payment_type": "inbound" }

Expected Response (Success)

{ "receipts": [{ "receipt_id": "abc123", "transaction_uuid": "tx-uuid-789", "is_approved": true, "amount_authorized": { "value": "190.50" }, "currency": { "english": "SAR" }, "status_message": { "english": "Approved" }, "card_scheme": { "name": { "english": "mada" } }, "pan": "4323 28** **** 2272", "approval_code": { "value": "181422" }, "verification_method": { "english": "NO VERIFICATION REQUIRED" }, "entry_mode": "CONTACTLESS" }] }

print_request

Called By: Odoo POS (receipt screen)

{ "file": "base64-encoded-pdf-content" }

customer_display

Called By: Odoo POS (payment and receipt screens)

Example URLs:
  • /ics_pos/customer_display/welcome
  • /ics_pos/customer_display/payment?amount=190.50
  • /ics_pos/customer_display/order?order_id=POS001
  • /ics_pos/customer_display/thanks?order_id=POS001

Payment Flows

POS Payment Flow

1. Order Creation

Customer adds products → Click "Payment"

2. Payment Request

Odoo POS calls payment_request handler

3. NearPay Processing

Flutter processes payment via NearPay

4. Response Handling

Flutter calls window.paymentComplete(result)

5. Validation

Odoo validates and creates pos.payment record

6. Completion

Receipt printed, customer display updated

Invoice Payment Flow

1. Invoice Open

User opens invoice → Click "Register Payment"

2. Method Selection

Select NearPay payment method

3. Payment Request

Odoo calls payment_request handler

4. NearPay Processing

Flutter processes payment via NearPay

5. Reconciliation

Odoo reconciles invoice with payment

6. Redirect

Odoo redirects to invoice list

Error Handling

Network Errors

Future<void> _handleNetworkError(String error) async {
  int retries = 0;
  const maxRetries = 3;
  const delayMs = 2000;

  while (retries < maxRetries) {
    try {
      await Future.delayed(Duration(milliseconds: delayMs));
      // Retry payment
      break;
    } catch (e) {
      retries++;
      if (retries >= maxRetries) {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('Payment failed after $maxRetries attempts')
            ),
          );
        }
      }
    }
  }
}

NearPay SDK Errors

Future<void> _handleNearPayError(dynamic error) async {
  String errorMessage = 'Unknown error';

  if (error is PlatformException) {
    errorMessage = error.message ?? 'Platform error';
  } else if (error is FormatException) {
    errorMessage = 'Invalid response format';
  } else {
    errorMessage = error.toString();
  }

  await _webViewController?.evaluateJavascript(
    source: '''
      window.paymentComplete({
        "error": "$errorMessage",
        "is_approved": false
      });
    ''',
  );
}

Common Error Scenarios

Timeout Handling

Implement a 120-second timeout for all payment operations. If NearPay doesn't respond, notify the user and allow retry.

Invalid Amount

Always validate that amount is positive and greater than zero before processing payment.

WebView Unavailable

Check if InAppWebViewController is available before calling handlers. This prevents crashes if WebView is closed unexpectedly.

Testing

Console Testing in Odoo

Without a physical NearPay terminal, test using Odoo's built-in helpers:

In Odoo POS Console

// Simulate approved payment
window.testInvoicePayment(true);

// Simulate declined payment
window.testInvoicePayment(false);

Flutter Unit Tests

import 'package:flutter_test/flutter_test.dart';

void main() {
  group('OdooNearPayBridge', () {
    test('Payment request handler processes valid payload', () async {
      final payload = {
        'amount': 190.50,
        'currency': 'SAR',
      };

      final response = {
        'receipts': [
          {
            'transaction_uuid': 'test-tx-123',
            'is_approved': true,
            'amount_authorized': {'value': '190.50'},
          }
        ]
      };

      expect(response['receipts'][0]['is_approved'], true);
    });

    test('Payment handler rejects invalid amount', () async {
      final payload = {'amount': -10, 'currency': 'SAR'};
      expect(payload['amount'], lessThan(0));
    });

    test('Parses Base64 receipt correctly', () {
      final base64String = 'SGVsbG8gV29ybGQ=';
      final decoded = utf8.decode(base64Decode(base64String));
      expect(decoded, equals('Hello World'));
    });
  });
}

Integration Testing

void main() {
  testWidgets('Full payment flow', (WidgetTester tester) async {
    await tester.pumpWidget(
      OdooNearPayBridge(
        odooUrl: 'http://localhost:8069',
        sessionToken: 'test-token',
      ),
    );

    // Verify WebView is created
    expect(find.byType(InAppWebView), findsOneWidget);

    // Simulate payment
    await tester.pumpAndSettle();
  });
}

Troubleshooting

JavaScript Handler Not Called

Symptom: payment_request handler in Flutter never executes

Solutions:

  • Verify JavaScript is enabled in InAppWebViewSettings
  • Check that handlers are registered in onWebViewCreated
  • Ensure Odoo WebView code is calling window.flutter_inappwebview.callHandler()
  • Check browser console for errors in Odoo

Debug Code

initialSettings: InAppWebViewSettings(
  javaScriptEnabled: true,
  javaScriptCanOpenWindowsAutomatically: true,
),
onConsoleMessage: (ctrl, msg) {
  print('Console: ${msg.message}');
}

paymentComplete Not Executing in Odoo

Symptom: Flutter sends result but Odoo doesn't receive it

Solutions:

  • Verify evaluateJavascript syntax is correct
  • Check that result JSON is valid
  • Ensure WebView context has window.paymentComplete defined
  • Check for JavaScript errors in Odoo console

Debug Code

await _webViewController?.evaluateJavascript(
  source: '''
    console.log('About to call paymentComplete');
    console.log(typeof window.paymentComplete);
    if (window.paymentComplete) {
      window.paymentComplete(${jsonEncode(result)});
    } else {
      console.error('paymentComplete not found');
    }
  ''',
);

NearPay SDK Initialization Fails

Symptom: await NearPay.instance hangs or throws error

Solutions:

  • Check that NearPay plugin is properly installed
  • Verify platform-specific permissions are granted
  • Ensure Android minimum SDK ≥ 21
  • Check NearPay API credentials
  • Review NearPay documentation at https://docs.nearpay.io

Receipt Printing Doesn't Work

Symptom: print_request handler is called but nothing prints

Solutions:

  • Implement actual printer integration (Bluetooth, cloud print, etc.)
  • Test Base64 decoding: base64Decode(base64String)
  • Verify printer is connected and available
  • Log decoded bytes size for debugging

Debug Code

try {
  final bytes = base64Decode(base64File);
  print('Decoded ${bytes.length} bytes');
  // Attempt to print
} catch (e) {
  print('Error: $e');
}

Odoo WebView Not Loading

Symptom: InAppWebView shows blank page or error

Solutions:

  • Verify Odoo URL is correct and accessible
  • Check network connectivity
  • Verify authentication token is valid
  • Check Odoo server logs for errors

Debug Code

onLoadStop: (controller, uri) {
  print('Page loaded: $uri');
},
onLoadError: (controller, url, code, message) {
  print('Load error: $code - $message');
}

Best Practices

Code Quality

  • Input Validation: Always validate amount and payload before processing
  • Error Handling: Use try-catch for all async operations
  • Logging: Log extensively for debugging and auditing
  • Timeouts: Implement 120-second maximum timeout for payments
  • State Management: Track payment processing state to prevent duplicates

Security

  • HTTPS Only: Always use HTTPS for Odoo connections
  • Token Management: Securely store and manage session tokens
  • Input Sanitization: Sanitize all JSON strings before passing to evaluateJavascript
  • Error Messages: Don't expose sensitive information in error messages

User Experience

  • Loading Indicators: Show loading state during payment processing
  • Clear Feedback: Provide clear success/failure messages
  • Retry Mechanism: Allow users to retry failed payments
  • Transaction Details: Display transaction ID and receipt information

Offline Support

  • Queue Payments: Store failed payments locally for retry
  • Sync Later: Implement background sync when connection is restored
  • Offline Indicator: Show user when offline mode is active

Resources & Documentation

Official Documentation

Odoo Module Source Code

  • POS Module: Check ics_pos_nearpay_integration/static/src/js/
  • Invoice Module: Check ics_account_nearpay_payment/static/src/js/

Additional Resources

Support & Contact

iCloud Solutions Team

Getting Help

  • Check the troubleshooting section first
  • Review error logs and console messages
  • Test with a simple payload first
  • Consult the official NearPay documentation
  • Contact support with detailed error information

Flutter NearPay Odoo Integration Developer Guide

Version 1.0 | Created by iCloud Solutions

Specialized in Odoo ERP solutions and mobile integrations

© 2025 iCloud Solutions. All rights reserved.

↑ Back to Top