authnet_flutter 1.0.2
authnet_flutter: ^1.0.2 copied to clipboard
Authorize.Net payments for Flutter: Accept.js, Accept Hosted, wallet tokens, gated direct-charge client. Not affiliated with or endorsed by Authorize.Net or Visa.
authnet_flutter #
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.
