authnet_flutter

authnet_dart: typed payments for Dart and Flutter

pub package pub points CI License: MIT Dart SDK

The in-app client layer of the authnet_dart SDK: contracts for tokenizing a card in-app (the safe, recommended path), plus an opt-in direct-charge mode for when you genuinely don't have a backend yet. Built on top of authnet_core.

Not affiliated with, endorsed by, or certified by Authorize.Net or Visa.

Why use it

  • Headless Accept.js and Accept Hosted contracts that work with your existing WebView and design system: no widget or WebView dependency lock-in.
  • Apple Pay and Google Pay opaque-data helpers.
  • Direct charging exists for backend-less prototypes but is deliberately risk-gated instead of presented as the safe default.
  • 160/160 pub points, 35 automated tests, and 100% production-line coverage.

This package is plain Dart: it has no WebView dependency of its own and ships no widgets. Accept.js and Accept Hosted are exposed as functions: build the HTML to load, build the JS calls to run, parse the messages that come back. You bring whichever WebView package your app already uses (webview_flutter, flutter_inappwebview, anything) and whichever UI you want around it. See "Design note: no widgets" below for why.

Mode 2 vs. Mode 3

This SDK supports three ways to charge a card, and this package is where two of them live: the third, charging entirely from a backend with no Flutter app involved at all, is authnet_core alone; see the root README for the full three-way comparison if that might fit you better. Comparing the two modes that do involve a Flutter app:

Mode 2: Tokenize here, charge on your backend Mode 3: Charge directly from the app
Where the Transaction Key lives Your server only Inside your app binary
PCI-DSS burden Accept Hosted can reduce scope substantially; headless Accept.js still handles card values in Dart before tokenization. Confirm your architecture with your acquirer/assessor. Heavier: raw payment data and the Transaction Key are both in the app.
Needs a backend Yes No
Risk if the app is decompiled None: there's no secret in the app Real: the Transaction Key can be extracted and used to charge/refund on your account
What this package gives you HTML/JS builders + message parsers for Accept.js and Accept Hosted AuthNetDirectCharge

Recommendation: Mode 2, if you have any backend at all, even a single serverless function that does nothing but forward a nonce to Authorize.Net. It's a small amount of extra infrastructure for a real reduction in risk and compliance burden. Mode 3 exists for a genuine reason (an MVP or internal tool with no backend, where the team has weighed this risk and accepted it), but it should be a deliberate choice rather than the default, which is why AuthNetDirectCharge refuses to construct unless you explicitly acknowledge the risk. See its dartdoc for the full explanation.

Install

dart pub add authnet_flutter

Or add the packages manually:

dependencies:
  authnet_core: ^1.0.2
  authnet_flutter: ^1.0.2
  # Plus whichever WebView package you're using, e.g.:
  webview_flutter: ^4.7.0

Mode 2a: Accept.js (headless, you build the form)

Get a public client key from your backend first (AuthNetClient.getMerchantDetails().publicClientKey, this key is designed to be used client-side, unlike the Transaction Key). Then, in your own widget, wire a WebViewController to the contract:

final controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted)
  ..addJavaScriptChannel(
    acceptJsChannelName,
    onMessageReceived: (message) {
      switch (parseAcceptJsMessage(message.message)) {
        case AcceptJsSuccess(:final opaqueData):
          // Send opaqueData to YOUR backend, which charges it with authnet_core:
          //   PaymentRequest(method: PaymentMethod.opaqueData, opaqueData: ...)
        case AcceptJsFailure(:final message):
          showErrorSnackbar(message);
      }
    },
  )
  ..loadHtmlString(
    buildAcceptJsHtml(
      apiLoginId: 'your-api-login-id',   // not a secret: safe client-side
      publicClientKey: publicClientKey,   // from getMerchantDetails()
      sandbox: true,                      // match your backend's environment
    ),
    baseUrl: 'https://localhost', // required: see the note below
  );

// Somewhere in your widget tree, so the platform actually mounts/runs it;
// it renders no UI of its own, so size it however small you like:
WebViewWidget(controller: controller)

// When your own card-entry form (built with plain TextFields, styled
// however you want) is submitted:
controller.runJavaScript(buildAcceptJsTokenizeCall(
  cardNumber: cardNumberController.text,
  month: monthController.text,
  year: yearController.text,
  cardCode: cvvController.text,
));

The page buildAcceptJsHtml builds defines no form fields. Card values do, however, originate in your Flutter/Dart UI and pass through the generated runJavaScript call before Accept.js tokenizes them. They do not reach your backend through this flow, and the Transaction Key stays server-side.

baseUrl is not optional: Accept.js refuses to tokenize a page with no HTTPS origin, and loadHtmlString() gives it none unless you pass one. The check is on the scheme, not a real or reachable domain (the error is "A HTTPS connection is required"), so any placeholder https:// value works. If you hit that error with baseUrl already set, this is the first thing to check.

Mode 2b: Accept Hosted (Authorize.Net's own checkout page)

Accept Hosted iframe callbacks require Authorize.Net's iframe communicator. Host the output of buildAcceptHostedCommunicatorHtml() at an HTTPS URL on your backend, request the token with that exact URL, and load the generated page using the same origin as its baseUrl.

// On your backend, serve this static HTML at communicatorUrl:
final communicatorHtml = buildAcceptHostedCommunicatorHtml(
  authorizeNetOrigin: Uri.parse(config.hostedFormUrl).origin,
);

// Also on your backend, request a fresh single-use token:
final communicatorUrl = 'https://pay.example.com/accept-hosted-communicator';
final token = await authNetClient.getHostedPaymentPageToken(
  amount: 19.99,
  hostedPaymentSettings: {
    'hostedPaymentIFrameCommunicatorUrl': {'url': communicatorUrl},
    'hostedPaymentReturnOptions': {
      'showReceipt': false,
      'url': 'https://pay.example.com/payment-complete',
      'cancelUrl': 'https://pay.example.com/payment-canceled',
    },
  },
);

// In your Flutter app:
final controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted)
  ..addJavaScriptChannel(
    acceptHostedChannelName,
    onMessageReceived: (message) {
      final parsed = parseAcceptHostedMessage(message.message);
      if (parsed.action == 'transactResponse') {
        // A transaction id may be available at parsed.transactionId;
        // confirm it server-side (getTransactionDetails, or a webhook via
        // authnet_server) rather than trusting this message alone. See
        // AcceptHostedMessage's dartdoc for why.
      }
    },
  )
  ..loadHtmlString(
    buildAcceptHostedHtml(
      token: token,
      formActionUrl: config.hostedFormUrl,
    ),
    baseUrl: Uri.parse(communicatorUrl).origin,
  );

// This one IS the checkout UI (Authorize.Net renders it), so give it
// real space in your widget tree:
WebViewWidget(controller: controller)

Unlike Accept.js, Accept Hosted can complete the entire charge itself when given a transactionRequest: Authorize.Net's own page collects card details and charges them directly, and your app just gets the result back. Treat that result as a lead, and confirm it server-side before you ship the order, the same reconciliation habit authnet_server's webhooks support. The communicator page filters by the expected Authorize.Net origin, but the callback is still client-side data and is not a settlement authority.

Mode 3: Direct charge (read this before using it)

final direct = AuthNetDirectCharge(
  config: AuthNetConfig(apiLoginId: '...', transactionKey: '...'),
  acknowledgeClientSecretRisk: true, // required: see the warning below
);

final result = await direct.charge(PaymentRequest(
  amount: 19.99,
  method: PaymentMethod.creditCard,
  billing: BillingDetails(firstName: 'Jane', lastName: 'Doe'),
  card: CardDetails(number: '...', expMonth: '12', expYear: '2030', cvv: '900'),
));

direct.close();

This embeds your Transaction Key in your app binary. Anyone who decompiles your app (a routine, low-effort task for a determined attacker) can extract it and make charges or refunds against your merchant account as if they were you. Your app also now handles raw payment data directly, which substantially increases PCI-DSS scope. AuthNetDirectCharge won't construct without acknowledgeClientSecretRisk: true, so a human has to read this and decide it's an acceptable tradeoff before it can happen. If you're not sure, use Mode 2 instead.

Wallets (Apple Pay / Google Pay)

Authorize.Net accepts a wallet's own payment token directly, no decryption needed on your end:

// After getting a result from your Apple Pay / Google Pay plugin of choice:
final opaqueData = applePayOpaqueData(base64PaymentData);
// or: googlePayOpaqueData(googlePayTokenJson);

// Charge it the same way as an Accept.js nonce, server-side:
//   PaymentRequest(method: PaymentMethod.opaqueData, opaqueData: opaqueData)

Design note: no widgets

Earlier drafts of this package shipped AcceptJsPaymentForm and AcceptHostedPaymentPage as ready-made widgets, each owning its own WebViewController and hard-depending on webview_flutter. That turned out to be the wrong default for a library: it forced every consumer onto one specific WebView package (even if their app already used a different one), and, for Accept.js specifically, baked in a fixed HTML card form with fixed styling that couldn't be made to look like the rest of the app. Neither of those is actually required to use Accept.js/Accept Hosted safely; they were just the easiest thing to ship first. This package now stops at the contract layer (HTML builders, JS-call builders, and typed message parsers) and leaves the WebView and the UI to you. See the example app for one complete way to wire it up.

The HTML/JS builders and message parsers are unit-tested directly. The JS bridge itself has also been run end-to-end against the real sandbox Accept.js in a headless browser: loading the script, calling authnetTokenize(), and round-tripping a real response through parseAcceptJsMessage() all work as expected (that's also how the baseUrl requirement above was found). A headless browser isn't a mobile WebView, though, and this SDK's automated tests can't cover that gap: test with a sandbox card on a real device or simulator before shipping.

License

MIT: see LICENSE. This library does not certify PCI-DSS compliance; Mode 3 in particular has real compliance implications you're responsible for understanding.

Libraries

authnet_flutter
The in-app client layer for the Authorize.Net Dart SDK: Accept.js and Accept Hosted contracts (Mode 2, the recommended client-side path: HTML builders + typed message parsers, no widgets or WebView dependency of its own, so bring whatever WebView package your app already uses), plus an opt-in gated direct-charge mode (Mode 3).