Credit card implementation with Flutter and Iyzico

Photo by CardMapr on Unsplash

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.

ezgif.com-gif-maker.gif

First be sure that our server is running

npm run dev

Now lets move to flutter, here is how our project layout will be

Screen Shot 2022-05-01 at 14.51.57.png

We will use the following packages

Screen Shot 2022-05-01 at 14.52.30.png

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!

Screen Shot 2022-05-03 at 16.05.18.png