Credit card implementation with Flutter and Iyzico
Front end implementation of credit card process with Flutter and Iyzico payment provider and connecting to the server we previously created
Hello again, lately we created a Node Js server with Iyzico package to connect to Iyzico online payment service from Turkey. That blog post can be found here . Now we need to implement a Flutter frontend and service to talk with our server.
First be sure that our server is running
npm run dev
Now lets move to flutter, here is how our project layout will be
We will use the following packages
Our main file look like this
import 'package:flutter/material.dart';
import 'package:iyzico_flutter/ui/credit_card_page.dart';
import 'package:provider/provider.dart';
import './core/providers/iyzico_provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => IyzicoPaymentProvider(),
),
],
child: MaterialApp(
title: 'Iyzico Frontend Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const CreditCardPage(),
),
);
}
}
As you can see we will implement a credit card screen, but before doing this we need to implement the logic. According the documentation have request and respond models. When creating a real world app that accepts credit card, you need to implement both models to inform your client properly with the response you get from endpoint. But a real world app is beyond the scope of this post so we will implement just the request model now and get a response which we will see in our terminal. In the documentation there are required and not required parameters, but as a best practice we will implement all of the parameters. I also like to implement copywith methods in the models to produce models with ease. Below is our model, sorry a bit big but this is normal as payment providers require a lot of information before the transaction.
/// Iyzico request model
/// Parameters are documented in the following link
/// [https://dev.iyzipay.com/en/api/auth]
/// Todo implement iyzico response models
class IyzicoRequestModel {
String? locale;
String? conversationId;
double? price;
double? paidPrice;
String? currency;
int? installment;
String? basketId;
String? paymentChannel;
String? paymentGroup;
PaymentCard? paymentCard;
Buyer? buyer;
ShippingAddress? shippingAddress;
BillingAddress? billingAddress;
List<BasketItems>? basketItems;
IyzicoRequestModel({
this.locale,
this.conversationId,
this.price,
this.paidPrice,
this.currency,
this.installment,
this.basketId,
this.paymentChannel,
this.paymentGroup,
this.paymentCard,
this.buyer,
this.shippingAddress,
this.billingAddress,
this.basketItems,
});
IyzicoRequestModel copyWith({
String? locale,
String? conversationId,
double? price,
double? paidPrice,
String? currency,
int? installment,
String? basketId,
String? paymentChannel,
String? paymentGroup,
PaymentCard? paymentCard,
Buyer? buyer,
ShippingAddress? shippingAddress,
BillingAddress? billingAddress,
List<BasketItems>? basketItems,
}) {
return IyzicoRequestModel(
locale: locale ?? this.locale,
conversationId: conversationId ?? this.conversationId,
price: price ?? this.price,
paidPrice: paidPrice ?? this.paidPrice,
currency: currency ?? this.currency,
installment: installment ?? this.installment,
basketId: basketId ?? this.basketId,
paymentChannel: paymentChannel ?? this.paymentChannel,
paymentGroup: paymentGroup ?? this.paymentGroup,
paymentCard: paymentCard ?? this.paymentCard,
buyer: buyer ?? this.buyer,
shippingAddress: shippingAddress ?? this.shippingAddress,
billingAddress: billingAddress ?? this.billingAddress,
basketItems: basketItems ?? this.basketItems,
);
}
factory IyzicoRequestModel.fromJson(Map<String, dynamic> json) {
return IyzicoRequestModel(
locale: json['locale'],
conversationId: json['conversionId'],
price: json['price'],
paidPrice: json['paidPrice'],
currency: json['currency'],
installment: json['installment'],
basketId: json['basketId'],
paymentChannel: json['paymentChannel'],
paymentGroup: json['paymentGroup'],
paymentCard: PaymentCard.fromJson(json['paymentCard']),
buyer: Buyer.fromJson(json['buyer']),
shippingAddress: ShippingAddress.fromJson(json['shippingAddress']),
billingAddress: BillingAddress.fromJson(json['billingAddress']),
basketItems: List<BasketItems>.from(
json['basketItems'].map((x) => BasketItems.fromJson(x))),
);
}
Map<String, dynamic> toJson() => {
'locale': locale,
'conversationId': conversationId,
'price': price,
'paidPrice': paidPrice,
'currency': currency,
'installment': installment,
'basketId': basketId,
'paymentChannel': paymentChannel,
'paymentGroup': paymentGroup,
'paymentCard': paymentCard?.toJson(),
'buyer': buyer?.toJson(),
'shippingAddress': shippingAddress?.toJson(),
'billingAddress': billingAddress?.toJson(),
'basketItems': List<dynamic>.from(basketItems!.map((x) => x.toJson())),
};
}
class PaymentCard {
String? cardHolderName;
String? cardNumber;
String? expireMonth;
String? expireYear;
String? cvc;
int? registerCard;
PaymentCard(
{this.cardHolderName,
this.cardNumber,
this.expireMonth,
this.expireYear,
this.cvc,
this.registerCard});
PaymentCard copyWith({
String? cardHolderName,
String? cardNumber,
String? expireMonth,
String? expireYear,
String? cvc,
int? registerCard,
}) {
return PaymentCard(
cardHolderName: cardHolderName ?? this.cardHolderName,
cardNumber: cardNumber ?? this.cardNumber,
expireMonth: expireMonth ?? this.expireMonth,
expireYear: expireYear ?? this.expireYear,
cvc: cvc ?? this.cvc,
registerCard: registerCard ?? this.registerCard,
);
}
factory PaymentCard.fromJson(Map<String, dynamic> json) {
return PaymentCard(
cardHolderName: json['cardHolderName'],
cardNumber: json['cardNumber'],
expireMonth: json['expireMonth'],
expireYear: json['expireYear'],
cvc: json['cvc'],
registerCard: json['registerCard'],
);
}
Map<String, dynamic> toJson() => {
'cardHolderName': cardHolderName,
'cardNumber': cardNumber,
'expireMonth': expireMonth,
'expireYear': expireYear,
'cvc': cvc,
'registerCard': registerCard,
};
}
class Buyer {
String? id;
String? name;
String? surname;
String? identityNumber;
String? city;
String? country;
String? email;
String? gsmNumber;
String? ip;
String? registrationAddress;
String? zipCode;
String? registrationDate;
String? lastLoginDate;
Buyer(
{this.id,
this.name,
this.surname,
this.identityNumber,
this.city,
this.country,
this.email,
this.gsmNumber,
this.ip,
this.registrationAddress,
this.zipCode,
this.registrationDate,
this.lastLoginDate});
Buyer copyWith({
String? id,
String? name,
String? surname,
String? identityNumber,
String? city,
String? country,
String? email,
String? gsmNumber,
String? ip,
String? registrationAddress,
String? zipCode,
String? registrationDate,
String? lastLoginDate,
}) {
return Buyer(
id: id ?? this.id,
name: name ?? this.name,
surname: surname ?? this.surname,
identityNumber: identityNumber ?? this.identityNumber,
city: city ?? this.city,
country: country ?? this.country,
email: email ?? this.email,
gsmNumber: gsmNumber ?? this.gsmNumber,
ip: ip ?? this.ip,
registrationAddress: registrationAddress ?? this.registrationAddress,
zipCode: zipCode ?? this.zipCode,
registrationDate: registrationDate ?? this.registrationDate,
lastLoginDate: lastLoginDate ?? this.lastLoginDate,
);
}
factory Buyer.fromJson(Map<String, dynamic> json) {
return Buyer(
id: json['id'],
name: json['name'],
surname: json['surname'],
identityNumber: json['identityNumber'],
city: json['city'],
country: json['country'],
email: json['email'],
gsmNumber: json['gsmNumber'],
ip: json['ip'],
registrationAddress: json['registrationAddress'],
zipCode: json['zipCode'],
registrationDate: json['registrationDate'],
lastLoginDate: json['lastLoginDate'],
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'surname': surname,
'identityNumber': identityNumber,
'city': city,
'country': country,
'email': email,
'gsmNumber': gsmNumber,
'ip': ip,
'registrationAddress': registrationAddress,
'zipCode': zipCode,
'registrationDate': registrationDate,
'lastLoginDate': lastLoginDate,
};
}
class BillingAddress {
String? contactName;
String? city;
String? country;
String? address;
String? zipCode;
BillingAddress(
{this.contactName, this.city, this.country, this.address, this.zipCode});
BillingAddress copyWith({
String? contactName,
String? city,
String? country,
String? address,
String? zipCode,
}) {
return BillingAddress(
contactName: contactName ?? this.contactName,
city: city ?? this.city,
country: country ?? this.country,
address: address ?? this.address,
zipCode: zipCode ?? this.zipCode,
);
}
factory BillingAddress.fromJson(Map<String, dynamic> json) {
return BillingAddress(
contactName: json['contactName'],
city: json['city'],
country: json['country'],
address: json['address'],
zipCode: json['zipCode'],
);
}
Map<String, dynamic> toJson() => {
'contactName': contactName,
'city': city,
'country': country,
'address': address,
'zipCode': zipCode,
};
}
class ShippingAddress {
String? contactName;
String? city;
String? country;
String? address;
String? zipCode;
ShippingAddress(
{this.contactName, this.city, this.country, this.address, this.zipCode});
ShippingAddress copyWith({
String? contactName,
String? city,
String? country,
String? address,
String? zipCode,
}) {
return ShippingAddress(
contactName: contactName ?? this.contactName,
city: city ?? this.city,
country: country ?? this.country,
address: address ?? this.address,
zipCode: zipCode ?? this.zipCode,
);
}
factory ShippingAddress.fromJson(Map<String, dynamic> json) {
return ShippingAddress(
contactName: json['contactName'],
city: json['city'],
country: json['country'],
address: json['address'],
zipCode: json['zipCode'],
);
}
Map<String, dynamic> toJson() => {
'contactName': contactName,
'city': city,
'country': country,
'address': address,
'zipCode': zipCode,
};
}
class BasketItems {
String? id;
String? itemType;
String? name;
String? category1;
String? category2;
double? price;
BasketItems(
{this.id,
this.itemType,
this.name,
this.category1,
this.category2,
this.price});
BasketItems copyWith({
String? id,
String? itemType,
String? name,
String? category1,
String? category2,
double? price,
}) {
return BasketItems(
id: id ?? this.id,
itemType: itemType ?? this.itemType,
name: name ?? this.name,
category1: category1 ?? this.category1,
category2: category2 ?? this.category2,
price: price ?? this.price,
);
}
factory BasketItems.fromJson(Map<String, dynamic> json) {
return BasketItems(
id: json['id'],
itemType: json['itemType'],
name: json['name'],
category1: json['category1'],
category2: json['category2'],
price: json['price'],
);
}
Map<String, dynamic> toJson() => {
'id': id,
'itemType': itemType,
'name': name,
'category1': category1,
'category2': category2,
'price': price,
};
}
Next we will implement our data constants file. These are data that are needed to be given in addition what we get as input from our credit card widget. Imagine a real world app, normally you should have the user selection of products and item details available in state. Also you should have implemented forms and other structures to gather user billing address and shipping address info before the credit card screen and put them to the state as well. In this post, we are skipping those elements, we are just creating dummy variables to represent what normally in state should be.
const String emptyData = '';
const String cityData = 'Istanbul';
const String countryData = 'Turkey';
const String emailData = 'em@em.co';
const String addressData = 'Some street';
const double priceData = 8.0;
const double paidPriceData = 10.0;
const String category1Data = 'Collectibles';
const String category2Data = 'Accessories';
const String itemTypeData = 'PHYSICAL';
const String productNameData = 'Binocular';
const String productIdData = 'BI101';
const String currencyData = 'TRY';
const String localeData = 'tr';
const String identityData = '11111111111';
const int installmentData = 1;
const int registerCardData = 0;
Talking about state, now it is a good time to implement our provider. It will make a service call which we will implement next, but before doing that it needs to check whether the credit card is expired or not.
import 'package:flutter/material.dart';
import '../models/iyzico_request_model.dart';
import '../services/iyzico_service.dart';
class IyzicoPaymentProvider extends ChangeNotifier {
late IyzicoRequestModel _payment;
IyzicoRequestModel get payment => _payment;
set payment(IyzicoRequestModel payment) {
_payment = payment;
notifyListeners();
}
pay() async {
// Check for expiry
var now = DateTime.now();
var expirationDate = DateTime.parse(
'20${payment.paymentCard!.expireYear}-${payment.paymentCard!.expireMonth}-01');
var isExpired = expirationDate.isBefore(now);
if (isExpired) {
print('Card expired');
return;
}
// If there is no expiry send the payment request
IyzicoService().sendPaymentRequest(payment);
}
}
Now we need to implement our service
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:io';
import '../models/iyzico_request_model.dart';
class IyzicoService {
factory IyzicoService() {
return _service;
}
static final IyzicoService _service = IyzicoService._internal();
IyzicoService._internal();
sendPaymentRequest(IyzicoRequestModel payment) async {
var paymentUrl = Uri.parse('http://localhost:3000/api/payment/iyzico');
try {
final response = await http.post(
paymentUrl,
body: jsonEncode(payment.toJson()),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
encoding: Encoding.getByName("utf-8"),
);
if (response.statusCode != 200)
throw HttpException('${response.statusCode}');
if (response.statusCode == 200) print('Connection is successful');
print(response.body.toString());
} on SocketException {
print('No internet connection or server is down');
}
}
}
Lastly we will implement our credit card screen. This screen uses credit card input form package as a visual form for credit card. What on tap happens is calling first _prepareOrderForPayment method, then pay method. The _prepareOrderForMethod is filling state with the data we get from credit card form by executing some string methods to properly extract parameters and with the dummy data we created before.
import 'package:flutter/material.dart';
import 'package:credit_card_input_form/constants/constanst.dart';
import 'package:credit_card_input_form/credit_card_input_form.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import '../core/providers/iyzico_provider.dart';
import '../core/models/iyzico_request_model.dart';
import 'dart:io' show Platform;
import '../core/utils/data_constants.dart';
class CreditCardPage extends StatefulWidget {
const CreditCardPage({Key? key}) : super(key: key);
@override
_CreditCardPageState createState() => _CreditCardPageState();
}
class _CreditCardPageState extends State<CreditCardPage> {
// Style the credit card input form
final Map<String, String> _customCaptions = {
'PREV': 'Prev',
'NEXT': 'Next',
'DONE': 'Done',
'CARD_NUMBER': 'Card Number',
'CARDHOLDER_NAME': 'Cardholder name',
'VALID_THRU': 'Valid thru',
'SECURITY_CODE_CVC': 'Security code (CVC)',
'NAME_SURNAME': 'Name Surname',
'MM_YY': 'MM/YY',
'RESET': 'Reset',
};
final _buttonStyle = BoxDecoration(
borderRadius: BorderRadius.circular(30.0),
gradient: LinearGradient(
colors: [
const Color(0xfffcdf8a),
const Color(0xfff38381),
],
begin: const FractionalOffset(0.0, 0.0),
end: const FractionalOffset(1.0, 0.0),
stops: [0.0, 1.0],
tileMode: TileMode.clamp),
);
final _cardDecoration = BoxDecoration(
boxShadow: <BoxShadow>[
const BoxShadow(
color: Colors.black54, blurRadius: 15.0, offset: Offset(0, 8))
],
gradient: const LinearGradient(
colors: [
Colors.red,
Colors.blue,
],
begin: const FractionalOffset(0.0, 0.0),
end: const FractionalOffset(1.0, 0.0),
stops: [0.0, 1.0],
tileMode: TileMode.clamp),
borderRadius: const BorderRadius.all(Radius.circular(15)));
final _buttonTextStyle = GoogleFonts.raleway(
fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white);
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
var iyzicoPaymentProvider = Provider.of<IyzicoPaymentProvider>(context);
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: ListView(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 300),
child: Stack(
children: [
CreditCardInputForm(
showResetButton: true,
customCaptions: _customCaptions,
frontCardDecoration: _cardDecoration,
backCardDecoration: _cardDecoration,
prevButtonTextStyle: _buttonTextStyle,
nextButtonTextStyle: _buttonTextStyle,
resetButtonTextStyle: _buttonTextStyle,
nextButtonDecoration: _buttonStyle,
prevButtonDecoration: _buttonStyle,
resetButtonDecoration: _buttonStyle,
onStateChange: (currentState, cardInfo) {
if (currentState == InputState.DONE) {
_prepareOrderForPayment(
cardInfo, iyzicoPaymentProvider);
iyzicoPaymentProvider.pay();
}
},
)
],
),
),
],
),
),
);
}
void _prepareOrderForPayment(
cardInfo, IyzicoPaymentProvider iyzicoPaymentProvider) {
// Initialize models
IyzicoRequestModel pay = IyzicoRequestModel();
PaymentCard card = PaymentCard();
Buyer buyer = Buyer();
BillingAddress billingAddress = BillingAddress();
ShippingAddress shippingAddress = ShippingAddress();
BasketItems basket = BasketItems();
// Extract card details from card info string
var infoString = cardInfo.toString();
var cardNumber = infoString
.split(',')[0]
.replaceAll('cardNumber=', '')
.replaceAll(' ', '');
var cardHolderName = infoString.split(',')[1].replaceAll(' name=', '');
var expiryMonth =
infoString.split(',')[2].replaceAll(' validate=', '').split('/')[0];
var expiryYear =
infoString.split(',')[2].replaceAll(' validate=', '').split('/')[1];
var cvcCode = infoString.split(',')[3].replaceAll(' cvv=', '');
// Extract name surname from card info string
String infoName = cardHolderName;
var names = infoName.split(' ');
var name, surname;
if (names.length == 2) {
name = names[0];
surname = names[1];
} else if (names.length > 2) {
name = names[0] + ' ' + names[1];
surname = names[2];
}
// Extract payment channel details
String paymentChannel = '';
if (Platform.isAndroid) paymentChannel = 'MOBILE_ANDROID';
if (Platform.isIOS) paymentChannel = 'MOBILE_IOS';
String paymentGroup = 'PRODUCT';
// Client credit card entries
card = card.copyWith(
cardNumber: cardNumber,
cardHolderName: cardHolderName,
expireMonth: expiryMonth,
expireYear: expiryYear,
cvc: cvcCode,
registerCard: registerCardData);
// Buyer details
buyer = buyer.copyWith(
id: identityData,
name: name,
surname: surname,
identityNumber: identityData,
city: cityData,
country: countryData,
email: emailData,
gsmNumber: emptyData,
registrationAddress: emptyData,
zipCode: emptyData,
lastLoginDate: emptyData,
registrationDate: emptyData,
ip: emptyData,
);
// Billing address details
billingAddress = billingAddress.copyWith(
contactName: cardHolderName,
address: addressData,
city: cityData,
country: countryData,
zipCode: emptyData,
);
// Shipping address details
shippingAddress = shippingAddress.copyWith(
contactName: cardHolderName,
address: addressData,
city: cityData,
country: countryData,
zipCode: emptyData,
);
// Shopping bag details
basket = basket.copyWith(
price: priceData,
category1: category1Data,
category2: category2Data,
itemType: itemTypeData,
name: productNameData,
id: productIdData);
// Populate payment object with available data
iyzicoPaymentProvider.payment = pay.copyWith(
paymentGroup: paymentGroup,
paymentChannel: paymentChannel,
locale: localeData,
basketId: emptyData,
conversationId: emptyData,
currency: currencyData,
installment: installmentData,
paymentCard: card,
paidPrice: paidPriceData,
price: priceData,
buyer: buyer,
basketItems: [basket],
billingAddress: billingAddress,
shippingAddress: shippingAddress,
);
print(iyzicoPaymentProvider.payment.toJson());
}
}
This completes our mini credit card app. If you run it you will see a credit card page. If you populate the form with the test cards provided by Iyzico you should get a response similar to the one below. Happy coding!