Press "Enter" to skip to content

Braintree integration in Xamarin Forms (Part.4 )

In our previous article, we covered how to support Paypal payments in Xamarin Forms. There are use cases where you just need a quick, easy and UI ready way to start accepting payments, Braintree does provide a complete, ready-made payment UI called Drop- in UI. Today I will extending the sample to support Braintree Drop-In UI.

Let’s code

In the Forms project, we added the following methods to the IPayService interface: OnDropUISuccessful, OnDropUIError and ShowDropUI to be able to support and handle the drop-in UI feature.

using System;
using System.Threading.Tasks;
using BraintreeXFSample.Models;
namespace BraintreeXFSample.Services
{
public interface IPayService
{
event EventHandler<DropUIResult> OnDropUISuccessful;
event EventHandler<string> OnDropUIError;
Task<DropUIResult> ShowDropUI(double totalPrice, string merchantId, int requestCode = 1234);
}
}

Xamarin iOS project

Add the package Naxam.BraintreeDropUI.iOS.

Implement the ShowDropUI in the iOSPayService this method will trigger the drop-in UI that displays a popup with all the payment options supported. In the specific case of Apple Pay, even when using the drop-in UI feature, we have to handle the tokenization based on the native platform payment offering so we will call the TokenizePlatform method which was implemented in this article.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Acr.UserDialogs;
using BraintreeApplePay;
using BraintreeCore;
using BraintreeDropIn;
using BraintreeUIKit;
using BraintreeXFSample.Models;
using BraintreeXFSample.Services;
using Foundation;
using PassKit;
using UIKit;
using Xamarin.Forms;
[assembly: Dependency(typeof(BraintreeXFSample.iOS.Services.iOSPayService))]
namespace BraintreeXFSample.iOS.Services
{
public class iOSPayService : PKPaymentAuthorizationViewControllerDelegate, IPayService
{
bool isDropUI = false;
string _clientToken;
TaskCompletionSource<string> payTcs;
TaskCompletionSource<DropUIResult> dropUiPayTcs;
PKPaymentAuthorizationViewController pKPaymentAuthorizationViewController;
public event EventHandler<DropUIResult> OnDropUISuccessful = delegate { };
public event EventHandler<string> OnDropUIError = delegate { };
public event EventHandler<string> OnTokenizationSuccessful = delegate { };
public event EventHandler<string> OnTokenizationError = delegate { };
bool isReady;
BTAPIClient braintreeClient;
public bool CanPay
{
get
{
return isReady;
}
}
public async Task<DropUIResult> ShowDropUI(double totalPrice, string merchantId, int resultCode = 1234)
{
dropUiPayTcs = new TaskCompletionSource<DropUIResult>();
if (CanPay)
{
BTDropInRequest request = new BTDropInRequest();
request.Amount = $"{totalPrice}";
BTDropInController bTDropInController = new BTDropInController(_clientToken, request, async (controller, result, error) =>
{
if (error == null)
{
if (result.Cancelled)
{
dropUiPayTcs.SetCanceled();
}
else if (result.PaymentOptionType == BTUIKPaymentOptionType.ApplePay)
{
try
{
isDropUI = true;
var nonce = await TokenizePlatform(totalPrice, merchantId);
var dropResult = new DropUIResult()
{
Nonce = nonce ?? string.Empty,
Type = $"{BTUIKPaymentOptionType.ApplePay}"
};
OnDropUISuccessful?.Invoke(this, dropResult);
dropUiPayTcs.TrySetResult(dropResult);
}
catch (TaskCanceledException)
{
dropUiPayTcs.SetCanceled();
}
catch (Exception exception)
{
OnDropUIError?.Invoke(this, exception.Message);
dropUiPayTcs.TrySetException(exception);
}
finally
{
pKPaymentAuthorizationViewController?.DismissViewController(true, null);
isDropUI = false;
}
}
else
{
var dropResult = new DropUIResult()
{
Nonce = result.PaymentMethod?.Nonce ?? string.Empty,
Type = $"{result.PaymentOptionType}"
};
OnDropUISuccessful?.Invoke(this, dropResult);
dropUiPayTcs.TrySetResult(dropResult);
}
}
else
{
OnDropUIError?.Invoke(this, error.Description);
dropUiPayTcs.TrySetException(new Exception(error.Description));
}
controller.DismissViewController(true, null);
});
var window = UIApplication.SharedApplication.KeyWindow;
var _viewController = window.RootViewController;
while (_viewController.PresentedViewController != null)
_viewController = _viewController.PresentedViewController;
_viewController?.PresentViewController(bTDropInController, true, null);
}
else
{
OnDropUIError?.Invoke(this, "Platform is not ready to accept payments");
dropUiPayTcs.TrySetException(new Exception("Platform is not ready to accept payments"));
}
return await dropUiPayTcs.Task;
}
public async Task<string> TokenizePlatform(double totalPrice, string merchantId)
{
payTcs = new TaskCompletionSource<string>();
if (isReady)
{
var applePayClient = new BTApplePayClient(braintreeClient);
applePayClient.PaymentRequest((request, error) =>
{
if (error == null)
{
RequestPaymentAuthorization(request, new Dictionary<string, double>{
{ "My App",totalPrice}
}, merchantId);
}
else
{
if (!isDropUI)
{
OnTokenizationError?.Invoke(this, "Error: Couldn't create payment request.");
}
payTcs.TrySetException(new Exception("Error: Couldn't create payment request."));
}
});
}
else
{
if (!isDropUI)
{
OnTokenizationError?.Invoke(this, "Platform is not ready to accept payments");
}
payTcs.TrySetException(new Exception("Platform is not ready to accept payments"));
}
return await payTcs.Task;
}
void RequestPaymentAuthorization(PKPaymentRequest paymentRequest, IDictionary<string, double> summaryItems, string merchantId)
{
UserDialogs.Instance.ShowLoading("Loading");
paymentRequest.MerchantIdentifier = merchantId;
paymentRequest.MerchantCapabilities = PKMerchantCapability.ThreeDS;
paymentRequest.CountryCode = "US";
paymentRequest.CurrencyCode = "USD";
if (summaryItems != null)
{
paymentRequest.PaymentSummaryItems = summaryItems.Select(i => new PKPaymentSummaryItem()
{
Label = i.Key,
Amount = new NSDecimalNumber(i.Value)
}).ToArray();
}
var window = UIApplication.SharedApplication.KeyWindow;
var _viewController = window.RootViewController;
while (_viewController.PresentedViewController != null)
_viewController = _viewController.PresentedViewController;
pKPaymentAuthorizationViewController = new PKPaymentAuthorizationViewController(paymentRequest);
UserDialogs.Instance.HideLoading();
if (pKPaymentAuthorizationViewController != null)
{
pKPaymentAuthorizationViewController.Delegate = this;
_viewController?.PresentViewController(pKPaymentAuthorizationViewController, true, null);
}
else
{
if (!isDropUI)
{
OnTokenizationError?.Invoke(this, "Error: Payment request is invalid.");
}
payTcs?.SetException(new Exception("Error: Payment request is invalid."));
}
}
public override void DidAuthorizePayment(PKPaymentAuthorizationViewController controller, PKPayment payment, Action<PKPaymentAuthorizationStatus> completion)
{
var applePayClient = new BTApplePayClient(braintreeClient);
applePayClient.TokenizeApplePayPayment(payment, (tokenizedApplePayPayment, error) =>
{
if (error == null)
{
if (string.IsNullOrEmpty(tokenizedApplePayPayment.Nonce))
{
payTcs?.SetCanceled();
}
else
{
if (!isDropUI)
{
OnTokenizationSuccessful?.Invoke(this, tokenizedApplePayPayment.Nonce);
}
payTcs?.TrySetResult(tokenizedApplePayPayment.Nonce);
}
completion(PKPaymentAuthorizationStatus.Success);
}
else
{
if (!isDropUI)
{
OnTokenizationError?.Invoke(this, "Error - Payment tokenization failed");
}
payTcs?.TrySetException(new Exception("Error - Payment tokenization failed"));
completion(PKPaymentAuthorizationStatus.Failure);
}
});
}
public override void PaymentAuthorizationViewControllerDidFinish(PKPaymentAuthorizationViewController controller)
{
controller.DismissViewController(true, null);
}
public override void WillAuthorizePayment(PKPaymentAuthorizationViewController controller)
{
}
}
}

Xamarin Android project

Add the package Naxam.BraintreeDropIn.Droid.

Implement the ShowDropUI in the AndroidPayService and add a OnActivityResult static method to handle Drop-In UI response.

[assembly: Dependency(typeof(BraintreeXFSample.Droid.Services.AndroidPayService))]
namespace BraintreeXFSample.Droid.Services
{
public class AndroidPayService : Java.Lang.Object, IPayService
{
static int _requestCode;
string _clientToken;
static TaskCompletionSource<DropUIResult> dropUiPayTcs;
static AndroidPayService CurrentInstance;
bool isReady = false;
public bool CanPay { get { return isReady; } }
public event EventHandler<DropUIResult> OnDropUISuccessful;
public event EventHandler<string> OnDropUIError;
public async Task<DropUIResult> ShowDropUI(double totalPrice,string merchantId, int requestCode = 1234)
{
if (isReady)
{
CurrentInstance = this;
_requestCode = requestCode;
dropUiPayTcs = new TaskCompletionSource<DropUIResult>();
GooglePaymentRequest googlePaymentRequest = new GooglePaymentRequest();
googlePaymentRequest.InvokeTransactionInfo(TransactionInfo.NewBuilder()
.SetTotalPrice($"{totalPrice}")
.SetTotalPriceStatus(WalletConstants.TotalPriceStatusFinal)
.SetCurrencyCode("USD")
.Build());
DropInRequest dropInRequest = new DropInRequest().ClientToken(_clientToken)
.Amount($"{totalPrice}")
.InvokeGooglePaymentRequest(googlePaymentRequest);
CrossCurrentActivity.Current.Activity.StartActivityForResult(dropInRequest.GetIntent(CrossCurrentActivity.Current.Activity), requestCode);
}
else
{
OnDropUIError?.Invoke(this, "Platform is not ready to accept payments");
dropUiPayTcs.TrySetException(new System.Exception("Platform is not ready to accept payments"));
}
return await dropUiPayTcs.Task;
}
void SetDropResult(DropUIResult dropResult)
{
OnDropUISuccessful?.Invoke(this, dropResult);
dropUiPayTcs?.TrySetResult(dropResult);
}
void SetDropException(Exception exception)
{
OnDropUIError?.Invoke(this, exception.Message);
dropUiPayTcs?.TrySetException(exception);
}
void SetDropCanceled()
{
dropUiPayTcs?.TrySetCanceled();
}
public static void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if(requestCode == _requestCode)
{
if(resultCode == Result.Ok)
{
DropInResult result = data.GetParcelableExtra(DropInResult.ExtraDropInResult).JavaCast<DropInResult>();
var dropResult = new DropUIResult()
{
Nonce = result.PaymentMethodNonce.Nonce,
Type = $"{result.PaymentMethodType}"
};
CurrentInstance?.SetDropResult(dropResult);
}
else if(resultCode == Result.Canceled)
{
CurrentInstance?.SetDropCanceled();
}
else
{
Exception error= data.GetSerializableExtra(DropInActivity.ExtraError).JavaCast<Java.Lang.Exception>();
CurrentInstance?.SetDropException(error);
}
}
}
}
}

In the MainActivity override OnActivityResult method and call AndroidPayService.OnActivityResult to process the result.

namespace BraintreeXFSample.Droid
{
[Activity(Label = "BraintreeXFSample", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
UserDialogs.Init(this);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
LoadApplication(new App());
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
AndroidPayService.OnActivityResult(requestCode, resultCode, data);
}
}
}

Xamarin Forms project

In the PaymentPageViewModel we call the ShowDropUI method to show the popup with all payment options.

namespace BraintreeXFSample.ViewModels
{
public class PaymentPageViewModel: INotifyPropertyChanged
{
public ICommand PayCommand { get; set; }
public ICommand OnPaymentOptionSelected { get; set; }
public PaymentOptionEnum PaymentOptionEnum { get; set; }
IPayService _payService;
string paymentClientToken = "";
const string MerchantId = "";
const double AmountToPay = 200;
public PaymentPageViewModel()
{
_payService= Xamarin.Forms.DependencyService.Get<IPayService>();
PayCommand = new Command(async () => await CreatePayment());
OnPaymentOptionSelected = new Command<PaymentOptionEnum>((data) => {
PaymentOptionEnum = data;
if (PaymentOptionEnum != PaymentOptionEnum.CreditCard)
PayCommand.Execute(null);
});
GetPaymentConfig();
_payService.OnTokenizationSuccessful += OnTokenizationSuccessful;
_payService.OnTokenizationError += OnTokenizationError;
_payService.OnDropUISuccessful += OnDropUISuccessful;
_payService.OnTokenizationError += OnDropUIError;
}
async Task GetPaymentConfig()
{
await _payService.InitializeAsync(paymentClientToken);
}
async Task CreatePayment()
{
UserDialogs.Instance.ShowLoading("Loading");
if (_payService.CanPay)
{
try
{
switch (PaymentOptionEnum)
{
case PaymentOptionEnum.DropUI:
UserDialogs.Instance.HideLoading();
await _payService.ShowDropUI(AmountToPay, MerchantId);
break;
default:
break;
}
}
catch (TaskCanceledException ex)
{
UserDialogs.Instance.HideLoading();
await App.Current.MainPage.DisplayAlert("Error", "Processing was cancelled", "Ok");
System.Diagnostics.Debug.WriteLine(ex);
}
catch (Exception ex)
{
UserDialogs.Instance.HideLoading();
await App.Current.MainPage.DisplayAlert("Error", "Unable to process payment", "Ok");
System.Diagnostics.Debug.WriteLine(ex);
}
}
else
{
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
UserDialogs.Instance.HideLoading();
await App.Current.MainPage.DisplayAlert("Error", "Payment not available", "Ok");
});
}
}
async void OnDropUIError(object sender, string e)
{
System.Diagnostics.Debug.WriteLine(e);
await App.Current.MainPage.DisplayAlert("Error", "Unable to process payment", "Ok");
}
async void OnDropUISuccessful(object sender, DropUIResult e)
{
System.Diagnostics.Debug.WriteLine($"Payment Authorized - {e.Nonce} by {e.Type}");
await App.Current.MainPage.DisplayAlert("Success", $"Payment Authorized: the token is {e.Nonce} by {e.Type}", "Ok");
}
async void OnTokenizationSuccessful(object sender, string e)
{
System.Diagnostics.Debug.WriteLine($"Payment Authorized - {e}");
UserDialogs.Instance.HideLoading();
await App.Current.MainPage.DisplayAlert("Success", $"Payment Authorized: the token is {e}", "Ok");
}
async void OnTokenizationError(object sender, string e)
{
System.Diagnostics.Debug.WriteLine(e);
UserDialogs.Instance.HideLoading();
await App.Current.MainPage.DisplayAlert("Error", "Unable to process payment", "Ok");
}
public event PropertyChangedEventHandler PropertyChanged;
}
}

In the XAML view add the DropUI option.

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:BraintreeXFSample"
xmlns:fastEntry="clr-namespace:XamarinFastEntrySample.FastEntry;assembly=XamarinFastEntry.Behaviors"
x:Class="BraintreeXFSample.Views.PaymentPage">
<ContentPage.Content>
<ScrollView>
<Grid VerticalOptions="CenterAndExpand" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3.3*"/>
<ColumnDefinition Width="3.3*"/>
<ColumnDefinition Width="3.3*"/>
<ColumnDefinition Width="3.3*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="60"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ImageButton Source="ic_creditcard"
Command="{Binding OnPaymentOptionSelected}"
CommandParameter="{x:Static local:PaymentOptionEnum.CreditCard}"
BorderColor="Gray"
BorderWidth="1"
CornerRadius="0"
Grid.Column="0"
Grid.Row="0"/>
<ImageButton Source="{OnPlatform iOS=ic_applepay, Android=ic_googlepay}"
Command="{Binding OnPaymentOptionSelected}"
CommandParameter="{x:Static local:PaymentOptionEnum.Platform}"
BorderColor="Gray"
BorderWidth="1"
CornerRadius="0"
Grid.Column="1"
Grid.Row="0"/>
<ImageButton Source="ic_paypal"
Command="{Binding OnPaymentOptionSelected}"
CommandParameter="{x:Static local:PaymentOptionEnum.PayPal}"
BorderColor="Gray"
BorderWidth="1"
CornerRadius="0"
Grid.Column="2"
Grid.Row="0"/>
<Button Text="Drop UI"
Command="{Binding OnPaymentOptionSelected}"
CommandParameter="{x:Static local:PaymentOptionEnum.DropUI}"
BorderColor="Gray"
BorderWidth="1"
CornerRadius="0"
Grid.Column="3"
Grid.Row="0"/>
</Grid>
</ScrollView>
</ContentPage.Content>
</ContentPage>

Let’s test

For the sake of this demo, I’m just displaying the tokenization result once you tap on pay but when getting the event OnDropUISuccessful is where you should call your backend endpoint to process the payment.

Now that we are ready, the result should be the following.

Check full source code here.

References

Happy Braintree integration!

7 Comments

  1. Paolo F Paolo F

    Great article, very useful. Thanks!

  2. Kai Kai

    Hello Rendy,

    thank you for your great articles.
    I implemented braintree like you mentioned in a test app and everything works great in iOS. In Android, after calling GooglePayment.IsReadyToPay the Respose is always false on my Test Device (Samsung Galaxy 10 with Android 10). I used your code and integrated “com.google.android.gms.wallet.api.enabled” to the Android manifest. Any idea what I have missed?

    Thaks in advance
    Kai

    • Paolo F. Paolo F.

      I think it’s normal if you are not using platform specific payments in braintree, just cut them out with an optional parameter in the initialize method that deactivates them if you do not use them. At least, this is what I did.

    • Rendy Rendy

      Are you testing on a real device?

      • Kai Kai

        Yes, Samsung Galaxy S10 with Android 10.

    • Jenish Jenish

      I am also facing same error. testing on real device.

  3. Kai Kai

    Thank you for your response.

    Yes, I am using a real device for testing.

Comments are closed.