noon_payments 1.2.0
noon_payments: ^1.2.0 copied to clipboard
A Flutter plugin for integrating Noon Payments SDK on Android and iOS.
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:noon_payments/noon_payments.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _paymentResult = "Waiting for action...";
bool _isLoading = false;
/// Whether Apple Pay can be used here. Checked once on startup so the web
/// button is gated WITHOUT awaiting inside the tap (Safari requires the
/// Apple Pay session to be created synchronously in the user gesture).
bool _applePayAvailable = false;
/// Replace these with real data from your backend.
///
/// 🚧 IMPORTANT: Your server-side INITIATE API must have the correct returnUrl:
/// Android: https://localhost/noonappsdkresponse
/// iOS: https://noonpayments.com/sdk/response
final String testOrderId =
'123456789012'; // The order Id that is received in the INITIATE API response
final String testAuthHeader =
"Key YOUR_AUTHORIZED_KEY"; // The authorization header for your business
/// Your backend base URL (web Apple Pay routes the two Noon calls through it).
static const String backendBaseUrl = 'https://your-server.com';
@override
void initState() {
super.initState();
NoonPayments.isApplePayAvailable().then((available) {
if (mounted) setState(() => _applePayAvailable = available);
});
}
/// 🚀 Standard Payment Flow (English)
Future<void> _startStandardPayment() async {
setState(() {
_isLoading = true;
_paymentResult = 'Launching...';
});
try {
final result = await NoonPayments.initiatePayment(
orderId: testOrderId,
authHeader: testAuthHeader,
environment: NoonEnvironment.sandbox,
language: NoonPaymentLanguage.english,
);
_handleResult(result);
} catch (e) {
_handleException(e);
} finally {
setState(() {
_isLoading = false;
});
}
}
/// 🎨 Custom Styled Payment Flow with Logo
Future<void> _startCustomStyledPayment() async {
setState(() {
_isLoading = true;
_paymentResult = 'Launching Styled...';
});
try {
// 1. Optional: Load your company logo from assets if you want it on the payment
final ByteData rawLogo = await rootBundle.load('assets/icons/icon.png');
final Uint8List logoBytes = rawLogo.buffer.asUint8List();
final customStyle = NoonPaymentStyle(
logoBytes: logoBytes,
backgroundColor: "#F8F9FA",
paymentOptionHeadingText: "Secure Checkout",
paymentOptionHeadingForeground: "#2196F3",
iosPaymentOptionHeadingFontSize: 20.0,
paymentOptionBackground: "#FFFFFF",
paymentOptionForeground: "#333333",
iosPaymentOptionFontSize: 16.0,
payNowButtonBackground: "#4CAF50",
payNowButtonForeground: "#FFFFFF",
payNowButtonText: "Secure Pay Now",
iosPayNowButtonRadius: 8.0,
payableAreaBackground: "#FFF9C4",
payableAmountText: "Amount Due",
payableAmountForeground: "#D32F2F",
footerText: "Verified by Noon Payments",
footerForeground: "#9E9E9E",
iosYesButtonBackground: "#4CAF50",
iosNoButtonBackground: "#F44336",
);
final result = await NoonPayments.initiatePayment(
orderId: testOrderId,
authHeader: testAuthHeader,
// environment: NoonEnvironment.sandbox,
environment: NoonEnvironment(
"https://api-test.sa.noonpayments.com/payment/v1/order",
),
language: NoonPaymentLanguage.english,
style: customStyle,
);
_handleResult(result);
} catch (e) {
_handleException(e);
} finally {
setState(() {
_isLoading = false;
});
}
}
/// 🍏 Apple Pay — Direct Integration (iOS only)
///
/// Presents the *native* Apple Pay sheet (not Noon's drop-in sheet) and
/// submits the collected token to Noon's INITIATE API from the device.
///
/// This is **iOS only**. On Flutter Web `payWithApplePay` returns a
/// `USE_SERVER_SIDE` failure (the browser can't call Noon directly — CORS);
/// use [_startApplePayWeb] there instead.
Future<void> _startApplePayDirect() async {
setState(() {
_isLoading = true;
_paymentResult = 'Checking Apple Pay...';
});
try {
if (!await NoonPayments.isApplePayAvailable()) {
setState(
() =>
_paymentResult = '🍏 Apple Pay is not available on this device.',
);
return;
}
final result = await NoonPayments.payWithApplePay(
config: const NoonApplePayConfig(
merchantIdentifier: 'merchant.com.yourcompany.app',
countryCode: 'AE',
currencyCode: 'AED',
summaryItems: [
NoonApplePaySummaryItem(label: 'Your Business', amount: '10'),
],
supportedNetworks: [
ApplePayNetwork.visa,
ApplePayNetwork.masterCard,
ApplePayNetwork.mada,
],
),
order: const NoonOrder(
amount: '10',
currency: 'AED',
name: 'Test Order',
category: 'pay',
reference: 'NPORDTEST0001',
),
authHeader: testAuthHeader,
environment: NoonEnvironment.sandbox,
paymentAction: 'AUTHORIZE,SALE',
);
_handleResult(result);
} catch (e) {
_handleException(e);
} finally {
setState(() {
_isLoading = false;
});
}
}
/// 🌐 Apple Pay on the Web (Flutter Web, server-side flow)
///
/// Presents the browser's Apple Pay (sheet in Safari, cross-device QR in
/// Chrome/Edge) and routes the two Noon calls through YOUR backend via the
/// callbacks — your auth key never reaches the browser.
///
/// 🚧 Safari rule: this MUST be invoked directly from the button tap with
/// **no `await` before** `payWithApplePayServerSide` (the Apple Pay session
/// has to be created inside the user gesture). That's why availability is
/// checked in [initState], not here.
Future<void> _startApplePayWeb() async {
setState(() {
_isLoading = true;
_paymentResult = 'Starting Apple Pay (web)...';
});
// A reference your backend uses to tie the two calls to one order.
const reference = 'NPORDTEST0001';
try {
// ⚠️ First awaited call — nothing is awaited before it in this gesture.
final result = await NoonPayments.payWithApplePayServerSide(
enableLogs: true, // prints "🍏 NoonApplePayWeb: ..." to the browser console
config: const NoonApplePayConfig(
merchantIdentifier: 'merchant.com.yourcompany.app',
countryCode: 'AE',
currencyCode: 'AED',
summaryItems: [
NoonApplePaySummaryItem(label: 'Your Business', amount: '10'),
],
supportedNetworks: [
ApplePayNetwork.visa,
ApplePayNetwork.masterCard,
ApplePayNetwork.mada,
],
),
// 1) Merchant validation: POST Apple's validationUrl to YOUR backend,
// which calls Noon INITIATE and returns
// `result.paymentData.data.validationData` (a JSON string).
onValidateMerchant: (validationUrl) async {
final res = await http.post(
Uri.parse('$backendBaseUrl/apple-pay/validate'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'validationUrl': validationUrl,
'reference': reference,
}),
);
final body = jsonDecode(res.body) as Map<String, dynamic>;
return body['validationData'] as String;
},
// 2) Payment authorized: POST the token to YOUR backend, which calls
// Noon PROCESS_AUTHENTICATION and returns the outcome.
onPaymentAuthorized: (paymentInfo) async {
final res = await http.post(
Uri.parse('$backendBaseUrl/apple-pay/authorize'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'paymentInfo': paymentInfo,
'reference': reference,
}),
);
final body = jsonDecode(res.body) as Map<String, dynamic>;
return body['status'] == 'paid'
? NoonPaymentResult.parse(res.body)
: NoonPaymentResult.failed(
errorMessage: body['message']?.toString() ?? 'Declined',
);
},
);
_handleResult(result);
} catch (e) {
_handleException(e);
} finally {
setState(() {
_isLoading = false;
});
}
}
void _handleResult(NoonPaymentResult result) {
log("NoonPayment Result: $result");
setState(() {
if (result.isSuccess) {
_paymentResult = '✅ Payment Successful!\nResult: ${result.rawResponse}';
} else if (result.isCancelled) {
_paymentResult = '🚫 Payment was cancelled.';
} else {
_paymentResult =
'❌ Payment Failed.\nCode: ${result.errorCode}\nMessage: ${result.errorMessage}';
}
});
}
void _handleException(dynamic e) {
log("Plugin Error: $e");
setState(() {
_paymentResult = '💀 Unexpected Plugin Error:\n$e';
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.amber),
home: Scaffold(
appBar: AppBar(
title: const Text('Noon Payments Example'),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
elevation: 0,
shape: RoundedRectangleBorder(
side: BorderSide(color: Colors.black12),
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
ListTile(
leading: Icon(Icons.shopping_bag_outlined),
title: Text('Order #$testOrderId'),
subtitle: Text('Total Amount: 100.00 AED'),
),
Divider(height: 1),
Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'Scan the item or proceed to secure payment using Noon Payments SDK below.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black54, fontSize: 13),
),
),
],
),
),
),
const Spacer(),
Text(
_paymentResult,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 32),
if (_isLoading)
const Center(child: CircularProgressIndicator())
else ...[
ElevatedButton.icon(
onPressed: _startStandardPayment,
icon: const Icon(Icons.payment),
label: const Text('Standard Payment'),
),
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: _startCustomStyledPayment,
icon: const Icon(Icons.brush),
label: const Text('Custom Styled Payment'),
),
const SizedBox(height: 12),
// Apple Pay — direct (iOS) vs server-side (web).
if (!kIsWeb)
OutlinedButton.icon(
onPressed: _startApplePayDirect,
icon: const Icon(Icons.apple),
label: const Text('Apple Pay (iOS Direct)'),
)
else if (_applePayAvailable)
OutlinedButton.icon(
// Called directly from the tap — no await before it, so the
// Apple Pay session is created inside the user gesture.
onPressed: _startApplePayWeb,
icon: const Icon(Icons.apple),
label: const Text('Apple Pay (Web)'),
),
],
const Spacer(),
],
),
),
),
);
}
}