# nexi_payment

> A Flutter plugin for Nexi's payment gateways: Nexi's checkout on Android and iOS, and an outcome the app can trust.

Case study by Paolo Gianfelici (Full-stack developer & UI/UX designer). https://paologianfelici.com/work/nexi-payment

- **Role:** Author and maintainer: the Dart API, the Android (Java) and iOS (Swift) code, tests, CI and releases
- **Type:** Open-source Flutter plugin, MIT licence, on pub.dev
- **Period:** 2020 – today (2.3.0, July 2026)
- **Platforms:** Android and iOS
- **Stack:** Flutter, Dart, Java, Swift, Platform channels, Nexi XPay SDK, Nexi NPG SDK, Patrol, GitHub Actions
- **pub.dev:** https://pub.dev/packages/nexi_payment
- **GitHub:** https://github.com/PaoloGi/nexi_payment

nexi_payment lets a Flutter app take payments through Nexi. The app hands over the order, Nexi's own checkout collects the card (the app never sees it), and the app gets back what happened: paid, cancelled, or failed and why.

I wrote it in June 2020, while adding Nexi to the Criluma Viaggi app, and published it on pub.dev. In July 2026 I rewrote it as 2.0: Nexi's newer NPG gateway next to the classic XPay one, one error contract on both platforms, tests that drive the real SDKs, and fixes for the moments when Nexi's native SDKs fall silent or report the wrong thing.

*nexi_payment is on pub.dev and GitHub. The recordings show its example app, a release build, paying in Nexi's public sandbox with Nexi's test card: the payment pages are Nexi's, the merchant is the sandbox's, the cardholder is invented and nothing is charged.*

- 7: stable releases on pub.dev, from 1.0.0 in June 2020 to 2.3.0 in July 2026
- 53: of the 58 commits are mine; the others came as contributors' pull requests
- 2: Nexi gateways behind one Dart API: classic XPay and NPG
- 49: automated tests: 35 unit tests and 14 integration tests that run on a device

## From a button in the app to an outcome it can trust

### One call opens Nexi's checkout

The app passes the order (id, amount, currency, language) and the plugin opens Nexi's Hosted Payment Page through the native SDK: a card, or one of the other methods Nexi offers. The card is typed on Nexi's page, never in the app.

### Summary and 3-D Secure

Nexi shows the summary with the masked card, then hands over to the cardholder's bank for 3-D Secure: in the sandbox, DemoBank, which can be told to pass or to fail.

### An outcome the app can trust

The native SDKs call their "completed" callback for any payment that finished, declined ones included. The plugin reports success only for a final EXECUTED or AUTHORIZED operation; a failed authentication comes back as a failure with Nexi's code. Before 2.0, iOS even reported a denied payment as a plain string; both platforms now raise the same coded errors.

### Every way out ends the call

The back button, Nexi's own cancel link, a sheet dragged away on iOS: each one resolves the payment as cancelled, and the next payment opens normally. Two of those exits used to leave the app waiting for an answer that never came.

## How it was built

### Filling the SDKs' silences

XPaySDK closes its checkout on Android's back button without calling back, and NPGSDK on iOS can let its sheet go without a word. The plugin watches the checkout's lifecycle and resolves with a cancel when it disappears having said nothing (a real outcome always wins), and a guard refuses a second payment while one is open instead of leaving two calls pending.

### When the SDK cannot read its own answer

NPGSDK 1.1.0 declares an operation's additionalData as a map of strings, but the backend nests an object in it, so on Android the SDK can fail to parse the outcome of a payment that went through. The plugin then reads the order back from NPG's orders API and reports what really happened: it never invents a success, and says "outcome unknown" when it cannot tell.

### Packaging, tests and releases

Nexi publishes no Maven artifacts and its iOS pod has no simulator slice: the Android SDKs ship inside the plugin, and the iOS frameworks are fetched from Nexi's release repositories at pod install and checked against pinned SHA-256 checksums. Unit tests pin the channel contract, Patrol tests drive the real SDKs against Nexi's sandbox, CI analyses, tests and builds on every push and pull request, and a version tag publishes to pub.dev.

## Recording a payment plugin: A real checkout, in Nexi's sandbox.

A plugin has no screens of its own, so the recordings show its example app, a release build on an Android emulator, paying for real in Nexi's public sandbox.

- **The published code, built for release.** The repository at the 2.3.0 tag, the example app built in release mode with the sandbox credentials Nexi publishes, installed on an emulator. No private keys: the classic XPay buttons need a merchant's own test terminal and are not shown.
- **Into Nexi's page through DevTools.** The checkout is a web page inside the SDK's WebView. The recording script finds its fields through Chrome DevTools, which also tells where the WebView sits on the screen, and touches them on the emulator's touchscreen like a finger, typing Nexi's test card.
- **Two sandbox quirks.** The sandbox page sometimes leaves its card button without a label, and never enables its pay button for a typed card. The script fixes both from DevTools, the second exactly as the plugin's own device-testing runbook does.
- **Three endings.** A payment that goes through, one whose 3-D Secure fails, and two ways of walking away. Each take starts from a cleared app and a new order.

The payment pages, the sandbox merchant (WWW.CHARTA.IT) and DemoBank are Nexi's public sandbox. The card is Nexi's published test card; the cardholder is invented.

## Recordings

- **A card payment** (103 s): The example app opens Nexi's Hosted Payment Page through the plugin; a test card, the summary, 3-D Secure at the sandbox's DemoBank, and the app gets the outcome back: executed. https://paologianfelici.com/media/nexi-payment/clips/card-payment.mp4?v=a920a2cabc
- **A failed authentication** (15 s): The same payment, but 3-D Secure fails: the SDK calls it "completed", the plugin reads the operation's result and reports a failure with Nexi's code. https://paologianfelici.com/media/nexi-payment/clips/declined.mp4?v=a0fb4adc44
- **Leaving the checkout** (74 s): Two ways out of Nexi's page (the back button and the page's own cancel), and both come back as a cancel; the second payment proves the plugin was not left stuck. https://paologianfelici.com/media/nexi-payment/clips/cancel.mp4?v=e2eb1b3d67
