DEV Community

Cover image for How create a credit card view in .NET MAUI
Serhii Korol
Serhii Korol

Posted on

How create a credit card view in .NET MAUI

In this article, I want to show how to create views of both sides of the credit card. Similar to some bank applications, we likely saw e-credit cards with hidden CVC. We will also develop similar cards and implement number card validation by type. Let's begin.

To start, you need to create a .NET MAUI project:

dotnet new maui -n CreditCardSample
Enter fullscreen mode Exit fullscreen mode

Following the step, you need to download iconic fonts to add credit card logos—the fonts you can find by link. Choose fonts for the desktop. You need to get FontAwesome6-Brands.otf and FontAwesome6-Regular.otf and copy them to Resources>Fonts folder. Next step, you should register fonts. Go to the MainProgram.cs and add this code:

fonts.AddFont("FontAwesome6-Brands.otf", "FA6Brands");
fonts.AddFont("FontAwesome6-Regular.otf", "FA6Regular");
Enter fullscreen mode Exit fullscreen mode

Also, for convenience, let's set the colors that we'll be using. In the Resources folder, you can find the Styles folder You'll also find Colors.xaml:

<Color x:Key="AmericanExpress">#3177CB</Color>
<Color x:Key="DinersClub">#1B4F8F</Color>
<Color x:Key="Discover">#E9752F</Color>
<Color x:Key="JCB">#9E2911</Color>
<Color x:Key="MasterCard">#394854</Color>
<Color x:Key="Visa">#2867BA</Color>
<Color x:Key="Default">#75849D</Color>

<Color x:Key="HeaderLabelTextColor">#BCBCBC</Color>
<Color x:Key="InfoLabelTextColor">#FFFFFF</Color>
Enter fullscreen mode Exit fullscreen mode

Now, let's create a couple of helpers and create a Helpers folder. The first helper will check card numbers by the pay system.

internal static class CardValidationHelper
{
    public static Regex AmericanExpress
        = new(@"^3[47][0-9]{13}$");

    public static Regex DinersClub
        = new(@"^3(?:0[0-5]|[68][0-9])[0-9]{11}$");

    public static Regex Discover
        = new(@"^6(?:011|5[0-9]{2})[0-9]{12}$");

    public static Regex JCB
       = new(@"^(?:2131|1800|35\d{3})\d{11}$");

    public static Regex MasterCard
        = new(@"^(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}$");

    public static Regex Visa
        = new(@"^4[0-9]{12}(?:[0-9]{3})?$");
}
Enter fullscreen mode Exit fullscreen mode

The second helper allows set colors by resource name.

internal static class StringExtensions
{
    public static Color ToColorFromResourceKey(this string resourceKey)
    {
        return Application.Current?.Resources
            .MergedDictionaries.First()[resourceKey] as Color;
    }
}
Enter fullscreen mode Exit fullscreen mode

And finally, let's create a Views folder and create CreditCardView. And now add this markup and I'll explain this code.

<?xml version="1.0" encoding="utf-8"?>

<Frame xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       x:Class="CreditCardApp.Views.CreditCardView"
       HasShadow="False"
       BorderColor="White"
       CornerRadius="0">
    <Grid>
        <Frame x:Name="CardFront"
               Grid.Row="0"
               Grid.Column="0"
               HasShadow="True"
               BorderColor="Grey"
               HorizontalOptions="Center"
               VerticalOptions="Start"
               WidthRequest="300"
               HeightRequest="200"
               CornerRadius="8"
               Margin="40,10,40,30"
               Padding="20">
            <Frame.Resources>
                <Style TargetType="Label"
                       x:Key="HeaderLabelStyle">
                    <Setter Property="LineBreakMode"
                            Value="TailTruncation" />
                    <Setter Property="FontSize"
                            Value="12" />
                    <Setter Property="TextColor"
                            Value="{StaticResource HeaderLabelTextColor}" />
                </Style>

                <Style TargetType="Label"
                       x:Key="InfoLabelStyle">
                    <Setter Property="FontSize"
                            Value="20" />
                    <Setter Property="TextColor"
                            Value="{StaticResource InfoLabelTextColor}" />
                </Style>

                <Style TargetType="Label"
                       x:Key="CreditCardImageLabelStyle">
                    <Setter Property="FontSize"
                            Value="48" />
                    <Setter Property="TextColor"
                            Value="#FFFFFF" />
                    <Setter Property="HorizontalOptions"
                            Value="EndAndExpand" />
                </Style>
            </Frame.Resources>
            <Grid ColumnDefinitions="*,*"
                  ColumnSpacing="30"
                  RowDefinitions="Auto,Auto,40,Auto,40"
                  RowSpacing="0">
                <Label x:Name="CreditCardImageLabel"
                       Style="{StaticResource CreditCardImageLabelStyle}"
                       Grid.Row="0"
                       Grid.Column="1" />

                <Label Text="Card Number"
                       Style="{StaticResource HeaderLabelStyle}"
                       Grid.Row="1"
                       Grid.Column="0"
                       Grid.ColumnSpan="2" />

                <Label x:Name="CreditCardNumber"
                       Style="{StaticResource InfoLabelStyle}"
                       Grid.Row="2"
                       Grid.Column="0"
                       Grid.ColumnSpan="2" />

                <Label Text="Expiration"
                       Style="{StaticResource HeaderLabelStyle}"
                       Grid.Row="3"
                       Grid.Column="0" />

                <Label x:Name="ExpirationDateLabel"
                       Style="{StaticResource InfoLabelStyle}"
                       Grid.Row="4"
                       Grid.Column="0" />
            </Grid>
        </Frame>

        <Frame x:Name="CardBack"
               Grid.Row="0"
               HasShadow="True"
               BorderColor="Grey"
               HorizontalOptions="Center"
               VerticalOptions="Start"
               WidthRequest="300"
               HeightRequest="200"
               CornerRadius="8"
               Margin="40,10,40,30">
            <Frame.Resources>
                <Style TargetType="Label"
                       x:Key="InfoLabelStyle">
                    <Setter Property="FontSize"
                            Value="20" />
                    <Setter Property="TextColor"
                            Value="{StaticResource InfoLabelTextColor}" />
                </Style>
            </Frame.Resources>
            <StackLayout>
                <Grid VerticalOptions="EndAndExpand">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="Auto" />
                    </Grid.ColumnDefinitions>
                    <BoxView BackgroundColor="Grey"
                             Grid.Column="1"
                             WidthRequest="100"
                             HeightRequest="40"
                             HorizontalOptions="End"
                             VerticalOptions="End" />
                    <Label x:Name="CardValidationCodeLabel"
                           Style="{StaticResource InfoLabelStyle}"
                           Grid.Column="1"
                           HorizontalOptions="Center"
                           VerticalOptions="Center" />
                </Grid>
            </StackLayout>
        </Frame>
        <Frame x:Name="Streak" Grid.Row="0" BackgroundColor="#5d3623" WidthRequest="300"
               HeightRequest="60" CornerRadius="0" HasShadow="False" VerticalOptions="Start" Margin="40">
        </Frame>
    </Grid>
</Frame>
Enter fullscreen mode Exit fullscreen mode

Because we want to show both sides of the cards, we need to use different frames. Each frame has its own state and doesn't depend from another frame. Also, it allows the implementation of different views in one. I split the view into three parts: the front side, the back side, and the decoration frame.

public partial class CreditCardView
{
    public static readonly BindableProperty CardNumberProperty
        = BindableProperty.Create(nameof(CardNumber), 
            typeof(string), typeof(CreditCardView), 
            propertyChanged: OnCardNumberChanged);

    public string CardNumber
    {
        get => (string)GetValue(CardNumberProperty);
        set => SetValue(CardNumberProperty, value);
    }

    public static readonly BindableProperty ExpirationDateProperty
        = BindableProperty.Create(nameof(ExpirationDate), 
            typeof(DateTime), typeof(CreditCardView), DateTime.Now, 
            propertyChanged: OnExpirationDateChanged);

    public DateTime ExpirationDate
    {
        get => (DateTime)GetValue(ExpirationDateProperty);
        set => SetValue(ExpirationDateProperty, value);
    }

    public static readonly BindableProperty CardValidationCodeProperty
    = BindableProperty.Create(nameof(CardValidationCode), 
        typeof(string), typeof(CreditCardView), "-", 
        propertyChanged: OnCardValidationCodeChanged);

    public string CardValidationCode
    {
        get => (string)GetValue(CardValidationCodeProperty);
        set => SetValue(CardValidationCodeProperty, value);
    }

    private static void OnCardNumberChanged(BindableObject bindable, 
        object oldValue, object newValue)
    {
        if (bindable is not CreditCardView creditCardView)
            return;

        creditCardView.SetCreditCardNumber();
    }

    private static void OnExpirationDateChanged(BindableObject bindable, 
        object oldValue, object newValue)
    {
        if (bindable is not CreditCardView creditCardView)
            return;

        creditCardView.SetExpirationDate();
    }

    private static void OnCardValidationCodeChanged(BindableObject bindable, 
        object oldValue, object newValue)
    {
        if (bindable is not CreditCardView creditCardView)
            return;

        creditCardView.SetCardValidationCode();
    }

    public CreditCardView()
    {
        InitializeComponent();

        CardFront.BackgroundColor = "Default".ToColorFromResourceKey();
        CardBack.BackgroundColor = "Default".ToColorFromResourceKey();

        CreditCardImageLabel.Text = "\uf09d";
        CreditCardImageLabel.FontFamily = "FA6Regular";

        ExpirationDateLabel.Text = "-";

        CardValidationCodeLabel.Text = $"CVC: -";
        CardBack.IsVisible = false;
        CardFront.IsVisible = true;
        Streak.IsVisible = false;

        var tapGestureRecognizer = new TapGestureRecognizer();
        tapGestureRecognizer.Tapped += OnCardTapped;
        this.GestureRecognizers.Add(tapGestureRecognizer);

    }

    private void SetCreditCardNumber()
    {
        if (string.IsNullOrEmpty(CardNumber))
        {
            CardFront.BackgroundColor = (Color)Application.Current?.Resources["Default"];
            CardBack.BackgroundColor = (Color)Application.Current?.Resources["Default"];
            CreditCardImageLabel.Text = "\uf09d";
            CreditCardImageLabel.FontFamily = "FA6Regular";
        }

        if (long.TryParse(CardNumber, out long cardNumberAsLong))
        {
            CreditCardNumber.Text = 
                string.Format("{0:0000  0000  0000  0000}", cardNumberAsLong);
        }
        else
        {
            CreditCardNumber.Text = "-";
        }

        if (CardNumber != null)
        {
            var normalizedCardNumber = CardNumber.Replace("-", string.Empty);

            if (CardValidationHelper.AmericanExpress.IsMatch(normalizedCardNumber))
            {
                CardFront.BackgroundColor = "AmericanExpress".ToColorFromResourceKey();
                CardBack.BackgroundColor = "AmericanExpress".ToColorFromResourceKey();
                CreditCardImageLabel.Text = "\uf1f3";
                CreditCardImageLabel.FontFamily = "FA6Brands";
            }
            else if (CardValidationHelper.DinersClub.IsMatch(normalizedCardNumber))
            {
                CardFront.BackgroundColor = "DinersClub".ToColorFromResourceKey();
                CardBack.BackgroundColor = "DinersClub".ToColorFromResourceKey();
                CreditCardImageLabel.Text = "\uf24c";
                CreditCardImageLabel.FontFamily = "FA6Brands";
            }
            else if (CardValidationHelper.Discover.IsMatch(normalizedCardNumber))
            {
                CardFront.BackgroundColor = "Discover".ToColorFromResourceKey();
                CardBack.BackgroundColor = "Discover".ToColorFromResourceKey();
                CreditCardImageLabel.Text = "\uf1f2";
                CreditCardImageLabel.FontFamily = "FA6Brands";
            }
            else if (CardValidationHelper.JCB.IsMatch(normalizedCardNumber))
            {
                CardFront.BackgroundColor = "JCB".ToColorFromResourceKey();
                CardBack.BackgroundColor = "JCB".ToColorFromResourceKey();
                CreditCardImageLabel.Text = "\uf24b";
                CreditCardImageLabel.FontFamily = "FA6Brands";
            }
            else if (CardValidationHelper.MasterCard.IsMatch(normalizedCardNumber))
            {
                CardFront.BackgroundColor = "MasterCard".ToColorFromResourceKey();
                CardBack.BackgroundColor = "MasterCard".ToColorFromResourceKey();
                CreditCardImageLabel.Text = "\uf1f1";
                CreditCardImageLabel.FontFamily = "FA6Brands";
            }
            else if (CardValidationHelper.Visa.IsMatch(normalizedCardNumber))
            {
                CardFront.BackgroundColor = "Visa".ToColorFromResourceKey();
                CardBack.BackgroundColor = "Visa".ToColorFromResourceKey();
                CreditCardImageLabel.Text = "\uf1f0";
                CreditCardImageLabel.FontFamily = "FA6Brands";
            }
            else
            {
                CardFront.BackgroundColor = "Default".ToColorFromResourceKey();
                CardBack.BackgroundColor = "Default".ToColorFromResourceKey();
                CreditCardImageLabel.Text = "\uf09d";
                CreditCardImageLabel.FontFamily = "FA6Regular";
            }
        }
    }

    private void SetExpirationDate()
    {
        ExpirationDateLabel.Text = ExpirationDate.ToString("MM/yy", 
            CultureInfo.InvariantCulture);
    }

    private void SetCardValidationCode()
    {
        CardValidationCodeLabel.Text = $"CVC: {CardValidationCode}";
    }


    public static readonly BindableProperty IsFlippedProperty =
        BindableProperty.Create(nameof(IsFlipped), typeof(bool), typeof(CreditCardView), false);

    public bool IsFlipped
    {
        get => (bool)GetValue(IsFlippedProperty);
        set
        {
            SetValue(IsFlippedProperty, value);
            UpdateCardSideVisibility();
        }
    }

    private void UpdateCardSideVisibility()
    {
        if (IsFlipped)
        {
            CardFront.IsVisible = false;
            CardBack.IsVisible = true;
            Streak.IsVisible = true;
        }
        else
        {
            CardFront.IsVisible = true;
            CardBack.IsVisible = false;
            Streak.IsVisible = false;
        }
    }

    private void OnCardTapped(object sender, EventArgs e)
    {
        IsFlipped = !IsFlipped;
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's explain what's going on in this file. The CardNumberProperty bind a card number. The ExpirationDateProperty binds an expiration date. The CardValidationCodeProperty binds the security code on the backward side. The IsFlippedProperty bind card state. The SetCreditCardNumber sets styles for each card. The SetExpirationDate formats expiration date. The SetCardValidationCode joins the security code with the title as more convenient. The OnCardNumberChanged, OnExpirationDateChanged, OnCardValidationCodeChanged, OnCardTapped observable changes. The UpdateCardSideVisibility switch frames.

The last step, let's add credit card views with passing data.

<VerticalStackLayout>
            <views:CreditCardView CardNumber="371449635398431"                                  
                                  ExpirationDate="2024-12-01"
                                  CardValidationCode="123"/>
            <views:CreditCardView CardNumber="38520000023237"
                                  ExpirationDate="2025-12-01"
                                  CardValidationCode="456"/>
            <views:CreditCardView CardNumber="6011000990139424"
                                  ExpirationDate="2026-12-01"
                                  CardValidationCode="789"/>
            <views:CreditCardView CardNumber="3566002020360505"
                                  ExpirationDate="2027-12-01"
                                  CardValidationCode="321"/>
            <views:CreditCardView CardNumber="5555555555554444"
                                  ExpirationDate="2028-12-01"
                                  CardValidationCode="654"/>
            <views:CreditCardView CardNumber="4012888888881881"
                                  ExpirationDate="2028-12-01"
                                  CardValidationCode="987"/>
            <views:CreditCardView />
        </VerticalStackLayout>
Enter fullscreen mode Exit fullscreen mode

Let's check it out.

front

Tap by card.

back

You even can tap by another card.

back 2

If this topic is interesting, I show how to make animation in .NET MAUI in the next article. And now that's all. See you in the next article and happy coding!

The source code you can find by link.

Buy Me A Beer

Top comments (0)