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.
// Runnable example demonstrating all three authnet_flutter usage modes.
// Needs a real device/simulator to exercise the WebView-based widgets (Mode
// 2); see the package README's confidence caveat on those widgets.
//
// flutter run
//
// This app never talks to a real backend of your own. Mode 2's "charge
// server-side" step and Mode 3's sandbox credentials are both entered by
// you at runtime, so you can see each mode's shape without wiring up a
// throwaway server just to try the SDK.
import 'package:authnet_core/authnet_core.dart';
import 'package:authnet_flutter/authnet_flutter.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'authnet_flutter example',
home: ModePickerPage(),
);
}
}
class ModePickerPage extends StatelessWidget {
const ModePickerPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('authnet_flutter example')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text(
'Not affiliated with, endorsed by, or certified by '
'Authorize.Net or Visa.',
style: TextStyle(fontStyle: FontStyle.italic),
),
const SizedBox(height: 24),
_ModeCard(
title: 'Mode 2a: Accept.js',
subtitle: 'Tokenize a card in-app, charge it from your backend.',
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const AcceptJsDemoPage()),
),
),
_ModeCard(
title: 'Mode 2b: Accept Hosted',
subtitle: "Authorize.Net's own hosted checkout page, embedded.",
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const AcceptHostedDemoPage()),
),
),
_ModeCard(
title: 'Mode 3: Direct charge (opt-in, risky)',
subtitle: 'Charges directly from the app. Read the warning first.',
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const DirectChargeDemoPage()),
),
),
],
),
);
}
}
class _ModeCard extends StatelessWidget {
const _ModeCard(
{required this.title, required this.subtitle, required this.onTap});
final String title;
final String subtitle;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
title: Text(title),
subtitle: Text(subtitle),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
),
);
}
}
/// Mode 2a: tokenize with Accept.js, then hand the nonce to your backend.
///
/// This page owns its own card-entry form (plain `TextField`s, styled
/// however you like) and its own `WebViewController`. authnet_flutter
/// only supplies the HTML to load and the JS call to run; wiring a WebView
/// package to it is this app's job, exactly like it would be in a real app.
class AcceptJsDemoPage extends StatefulWidget {
const AcceptJsDemoPage({super.key});
@override
State<AcceptJsDemoPage> createState() => _AcceptJsDemoPageState();
}
class _AcceptJsDemoPageState extends State<AcceptJsDemoPage> {
final _apiLoginIdController = TextEditingController();
final _publicClientKeyController = TextEditingController();
final _cardNumberController = TextEditingController();
final _monthController = TextEditingController();
final _yearController = TextEditingController();
final _cardCodeController = TextEditingController();
WebViewController? _webViewController;
OpaqueData? _result;
String? _error;
@override
void dispose() {
_apiLoginIdController.dispose();
_publicClientKeyController.dispose();
_cardNumberController.dispose();
_monthController.dispose();
_yearController.dispose();
_cardCodeController.dispose();
super.dispose();
}
void _loadEngine() {
final controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
acceptJsChannelName,
onMessageReceived: (message) {
switch (parseAcceptJsMessage(message.message)) {
case AcceptJsSuccess(:final opaqueData):
setState(() {
_result = opaqueData;
_error = null;
});
case AcceptJsFailure(:final message):
setState(() {
_error = message;
_result = null;
});
}
},
)
..loadHtmlString(
buildAcceptJsHtml(
apiLoginId: _apiLoginIdController.text,
publicClientKey: _publicClientKeyController.text,
sandbox: true,
),
// Accept.js refuses to tokenize on a page with no HTTPS origin.
// loadHtmlString() alone has none, so a baseUrl is required. See
// buildAcceptJsHtml()'s dartdoc.
baseUrl: 'https://localhost',
);
setState(() => _webViewController = controller);
}
void _tokenize() {
_webViewController?.runJavaScript(buildAcceptJsTokenizeCall(
cardNumber: _cardNumberController.text,
month: _monthController.text,
year: _yearController.text,
cardCode: _cardCodeController.text,
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Accept.js')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _apiLoginIdController,
decoration:
const InputDecoration(labelText: 'Sandbox API Login ID'),
),
const SizedBox(height: 8),
TextField(
controller: _publicClientKeyController,
decoration: const InputDecoration(
labelText: 'Public client key',
helperText: 'From AuthNetClient.getMerchantDetails()',
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _apiLoginIdController.text.isEmpty ||
_publicClientKeyController.text.isEmpty
? null
: _loadEngine,
child: const Text('Load the tokenization engine'),
),
if (_webViewController != null) ...[
const SizedBox(height: 16),
TextField(
controller: _cardNumberController,
decoration: const InputDecoration(labelText: 'Card number'),
),
const SizedBox(height: 8),
TextField(
controller: _monthController,
decoration: const InputDecoration(labelText: 'Exp. month'),
),
const SizedBox(height: 8),
TextField(
controller: _yearController,
decoration: const InputDecoration(labelText: 'Exp. year'),
),
const SizedBox(height: 8),
TextField(
controller: _cardCodeController,
decoration: const InputDecoration(labelText: 'CVV'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _tokenize,
child: const Text('Tokenize'),
),
// The WebView here runs Accept.js's JS but renders no UI of
// its own. This tiny box is only so the platform actually
// mounts and runs it. A real app can size this to zero or
// keep it off-screen the same way.
SizedBox(
height: 1,
child: WebViewWidget(controller: _webViewController!),
),
],
if (_result != null)
Text(
'Tokenized. dataDescriptor: ${_result!.dataDescriptor}\n'
'Send this OpaqueData to your backend and charge it with '
"authnet_core's PaymentRequest(method: "
'PaymentMethod.opaqueData, ...).',
),
if (_error != null) Text('Error: $_error'),
],
),
),
);
}
}
/// Mode 2b: Authorize.Net's own hosted checkout, embedded in-app. Needs a
/// token from `AuthNetClient.getHostedPaymentPageToken()`. Get one from
/// your backend and paste it in below (this example has no backend of its
/// own to call).
class AcceptHostedDemoPage extends StatefulWidget {
const AcceptHostedDemoPage({super.key});
@override
State<AcceptHostedDemoPage> createState() => _AcceptHostedDemoPageState();
}
class _AcceptHostedDemoPageState extends State<AcceptHostedDemoPage> {
final _tokenController = TextEditingController();
final _pageOriginController =
TextEditingController(text: 'https://pay.example.com');
WebViewController? _webViewController;
String? _lastMessage;
@override
void dispose() {
_tokenController.dispose();
_pageOriginController.dispose();
super.dispose();
}
void _loadHostedPage() {
const config = AuthNetConfig(apiLoginId: '', transactionKey: '');
final controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
acceptHostedChannelName,
onMessageReceived: (message) => setState(() =>
_lastMessage = parseAcceptHostedMessage(message.message).action),
)
..loadHtmlString(
buildAcceptHostedHtml(
token: _tokenController.text,
formActionUrl: config.hostedFormUrl,
),
baseUrl: _pageOriginController.text,
);
setState(() => _webViewController = controller);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Accept Hosted')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _tokenController,
decoration: const InputDecoration(
labelText: 'Hosted payment page token',
helperText:
'From your backend: AuthNetClient.getHostedPaymentPageToken()',
),
),
const SizedBox(height: 12),
TextField(
controller: _pageOriginController,
decoration: const InputDecoration(
labelText: 'Communicator page origin',
helperText: 'Must match the HTTPS origin of the communicator '
'URL used when requesting this token',
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _tokenController.text.isEmpty ? null : _loadHostedPage,
child: const Text('Load the hosted page'),
),
const SizedBox(height: 16),
if (_webViewController != null)
Expanded(
child: WebViewWidget(controller: _webViewController!),
),
if (_lastMessage != null)
Text('Last message action: $_lastMessage'),
],
),
),
);
}
}
/// Mode 3: charge directly from the app. Read `AuthNetDirectCharge`'s
/// dartdoc before using this in a real app: this demo page's warning
/// banner is not a substitute for it.
class DirectChargeDemoPage extends StatefulWidget {
const DirectChargeDemoPage({super.key});
@override
State<DirectChargeDemoPage> createState() => _DirectChargeDemoPageState();
}
class _DirectChargeDemoPageState extends State<DirectChargeDemoPage> {
final _apiLoginIdController = TextEditingController();
final _transactionKeyController = TextEditingController();
bool _acknowledged = false;
String? _resultMessage;
@override
void dispose() {
_apiLoginIdController.dispose();
_transactionKeyController.dispose();
super.dispose();
}
Future<void> _charge() async {
final direct = AuthNetDirectCharge(
config: AuthNetConfig(
apiLoginId: _apiLoginIdController.text,
transactionKey: _transactionKeyController.text,
),
acknowledgeClientSecretRisk: true,
);
final result = await direct.charge(PaymentRequest(
amount: 1.00,
method: PaymentMethod.creditCard,
billing: BillingDetails(firstName: 'Test', lastName: 'Buyer'),
// Authorize.Net's published sandbox test card (always approves).
card: CardDetails(
number: '4111111111111111',
expMonth: '12',
expYear: '2030',
cvv: '900'),
));
direct.close();
if (!mounted) return;
setState(() => _resultMessage = '${result.status}: ${result.message}');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Direct charge (Mode 3)')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'This mode embeds your Transaction Key in the app. Only '
'use it if you understand and accept that risk; see '
"AuthNetDirectCharge's dartdoc.",
style: TextStyle(color: Colors.red),
),
const SizedBox(height: 16),
TextField(
controller: _apiLoginIdController,
decoration:
const InputDecoration(labelText: 'Sandbox API Login ID'),
),
const SizedBox(height: 8),
TextField(
controller: _transactionKeyController,
decoration:
const InputDecoration(labelText: 'Sandbox Transaction Key'),
obscureText: true,
),
const SizedBox(height: 8),
CheckboxListTile(
value: _acknowledged,
onChanged: (v) => setState(() => _acknowledged = v ?? false),
title: const Text('I understand and accept the risk above.'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _acknowledged &&
_apiLoginIdController.text.isNotEmpty &&
_transactionKeyController.text.isNotEmpty
? _charge
: null,
child: const Text('Charge \$1.00 sandbox test card'),
),
if (_resultMessage != null) Text(_resultMessage!),
],
),
),
);
}
}