Google Pay, Stripe & Xamarin

Google Pay, Stripe & Xamarin

Similar to my post on integrating Apple Pay with Stripe and Xamarin, I spent a fair amount of time researching the best practices for implementing Google Pay, Stripe, and Xamarin. Again, I came away mostly empty-handed. Here's what I did to make it all work together nicely.

To implement Google Pay with Stripe, I started with the following documentation.

These examples are based on Java and Kotlin, so I had to figure out how to rework the documentation to work for C# & Xamarin.

I started with my interface, IPaymentService. (See previous post on Apple Pay)

public interface IPaymentService
{
    event EventHandler CanMakePaymentsUpdated;
    event EventHandler<string> AuthorizationComplete;
    bool CanMakePayments { get; }

    void AuthorizePayment(decimal total);
}

Next, I created my PaymentService based on the interface in my Android project.

It inherits from AppCompatibility and injects dependencies for my configuration and messaging center in the constructor.

public class PaymentService : AppCompatActivity, IPaymentService
public PaymentService(IApiConfiguration apiConfiguration, IMessagingCenter messageCenter)

At this point, I was ready to start my setup of the Google Pay wallet.

For this, we install Xamarin.GooglePlayServices.Wallet from NuGet and set it up in the constructor as follows.

PaymentsClient = WalletClass.GetPaymentsClient(
    Android.App.Application.Context,
    new WalletClass.WalletOptions.Builder()
    .SetEnvironment(WalletConstants.EnvironmentTest)
    .Build());

I also set up the IsReadyToPay logic to wire up the CanMakePayments property so that I can show/hide the button based on the availability of Google Pay on the user's device.

var readyToPayRequest = IsReadyToPayRequest.FromJson(GetReadyToPayRequest());
var task = PaymentsClient.IsReadyToPay(readyToPayRequest);
 task.AddOnCompleteListener(this);

For this, I also had to implement the interface IOnCompleteListener. This allowed me to listen for the completion of the PaymentsClient.IsReadyToPay result.

public void OnComplete(Task completeTask)
{
    CanMakePayments = completeTask.IsComplete;
    CanMakePaymentsUpdated?.Invoke(this, null);
}

My Xamarin View Model was then listening for the CanMakePaymentsUpdated event to fire and make my button visible.

PaymentService.CanMakePaymentsUpdated += PaymentService_CanMakePaymentsUpdated;
private void PaymentService_CanMakePaymentsUpdated(object sender, EventArgs e) => IsMobileWalletReady = PaymentService.CanMakePayments;

For the GetReadyToPayRequest, I created the following methods.

public string GetReadyToPayRequest() => JsonConvert.SerializeObject(GetBaseRequest());
protected GooglePaymentRequest GetBaseRequest() =>
    new GooglePaymentRequest
     {
        ApiVersion = 2,
        ApiVersionMinor = 0,
        MerchantInfo = new MerchantInfo { MerchantName = "GoSnapShop" },
        AllowedPaymentMethods = new[]
        {
            new PaymentMethod
            {
                Type = "CARD",
                Parameters = new PaymentParameters
                {
                    AllowedAuthMethods = new[] { "PAN_ONLY", "CRYPTOGRAM_3DS" },
                    AllowedCardNetworks = new[] { "AMEX", "DISCOVER", "MASTERCARD", "VISA" }
                },
                TokenizationSpecification = new TokenizationSpecification
                {
                    Type = "PAYMENT_GATEWAY",
                    Parameters = new TokenizationSpecificationParameters
                    {
                        Gateway = "stripe",
                        StripeVersion = "2020-03-02",
                        StripeKey = ApiConfiguration.StripePublishableKey
                    }
                }
            }
        }
    };

The GooglePayRequest can be implemented in multiple ways. Rather than use JSONObject and JSONArray, as specified in the Google/Stripe examples, I decided creating my own classes would be preferable.

using Newtonsoft.Json;
namespace Project.Droid.Payment
{
    public class GooglePaymentRequest
    {
        [JsonProperty("apiVersion")]
        public int ApiVersion { get; set; }
        [JsonProperty("apiVersionMinor")]
        public int ApiVersionMinor { get; set; }
        [JsonProperty("merchantInfo")]
        public MerchantInfo MerchantInfo { get; set; }
        [JsonProperty("allowedPaymentMethods")]
        public PaymentMethod[] AllowedPaymentMethods { get; set; }
        [JsonProperty("transactionInfo")]
        public TransactionInfo TransactionInfo { get; set; }
    }

    public class MerchantInfo
    {
        [JsonProperty("merchantName")]
        public string MerchantName { get; set; }
    }

    public class TransactionInfo
    {
        [JsonProperty("totalPriceStatus")]
        public string TotalPriceStatus { get; set; }
        [JsonProperty("totalPrice")]
        public string TotalPrice { get; set; }
        [JsonProperty("currencyCode")]
        public string CurrencyCode { get; set; }
    }

    public class PaymentMethod
    {
        [JsonProperty("type")]
        public string Type { get; set; }
        [JsonProperty("parameters")]
        public PaymentParameters Parameters { get; set; }
        [JsonProperty("tokenizationSpecification")]
        public TokenizationSpecification TokenizationSpecification { get; set; }
    }

    public class PaymentParameters
    {
        [JsonProperty("allowedAuthMethods")]
        public string[] AllowedAuthMethods { get; set; }
        [JsonProperty("allowedCardNetworks")]
        public string[] AllowedCardNetworks { get; set; }
    }

    public class TokenizationSpecification
    {
        [JsonProperty("type")]
        public string Type { get; set; }
        [JsonProperty("parameters")]
        public TokenizationSpecificationParameters Parameters { get; set; }
    }

    public class TokenizationSpecificationParameters
    {
        [JsonProperty("gateway")]
        public string Gateway { get; set; }
        [JsonProperty("stripe:version")]
        public string StripeVersion { get; set; }
        [JsonProperty("stripe:publishableKey")]
        public string StripeKey { get; set; }
    }
}

Now, we're ready to implement the AuthorizePayment method.

Although the PaymentsClient.LoadPaymentData does have an async method, I wasn't able to get it to work. It produced all kinds of errors that I wasn't able to easily resolve. I had to go with the suggested method of using the AutoResolveHelper. The 999 specified for the request code is an arbitrary number that is used to identify my request to the result handler we'll use later.

public void AuthorizePayment(decimal total) => AutoResolveHelper.ResolveTask(
    PaymentsClient.LoadPaymentData(CreatePaymentDataRequest(total)),
    Platform.CurrentActivity,
    999);

You'll notice that the CreatePaymentRequest also uses the GetBaseRequestMethod, and then adds transaction-specific data for the authorization.

protected PaymentDataRequest CreatePaymentDataRequest(decimal total)
{
    var request = GetBaseRequest();

    request.TransactionInfo = new TransactionInfo
    {
        TotalPrice = total.ToString("F"),
        TotalPriceStatus = "FINAL",
        CurrencyCode = "USD"
    };

    return PaymentDataRequest.FromJson(JsonConvert.SerializeObject(request));
}

When the authorization request is made, it looks for the Platform.Current activity (using Xamarin.Essentials) and finds the MainActivity for our Android project.

There, we implement the OnActivityResult method.

protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
    base.OnActivityResult(requestCode, resultCode, data);

    if (requestCode == 999 && resultCode == Result.Ok)
    MessagingCenter.Send(this, "AuthorizationComplete", data);
}

Here, using our request code, we look for the result and send it over to the PaymentService using MessageCenter. The PaymentService is then listening for the message.

MessagingCenter.Subscribe<MainActivity, Intent>(this, "AuthorizationComplete", (sender, intent) =>
{
    var paymentData = PaymentData.GetFromIntent(intent); 

We then have to parse the result and get our authorization token.

  string paymentInfo = paymentData.ToJson();

    if (paymentInfo == null)
    {
        return;
    }


    var paymentMethodData = (JObject)JsonConvert.DeserializeObject(paymentInfo);
    string tokenData = paymentMethodData.SelectToken("paymentMethodData.tokenizationData.token").ToString();
    var token = JsonConvert.DeserializeObject<GooglePaymentResponseToken>(tokenData);

Again, I created a GooglePaymentResponseToken class to deserialize the result into:

public class GooglePaymentResponseToken
{
    public string Id { get; set; }
    public string Object { get; set; }
    [JsonProperty("client_ip")]
    public string ClientIp { get; set; }
    public int Created { get; set; }
    public bool LiveMode { get; set; }
    public string Type { get; set; }
    public bool Used { get; set; }
}

Then, we notify the Xamarin view model listening for my AuthorizationComplete event, which in turn, calls my api to complete the payment.

AuthorizationComplete.Invoke(this, token.Id);

The API completion method looks like this:

public async Task<PaymentResult> AddPayment(int orderId, decimal amount, string email, PaymentInformation paymentInformation)
{
    var existingPayment = await PaymentRepository.Entity
        .ByOrderId(orderId)
        .FirstOrDefaultAsync();

     if (existingPayment != null)
        throw new InvalidOperationException($"A payment of {existingPayment.Amount} has already been charged for this order.");

        string tokenId = paymentInformation.WalletPaymentToken;

        // handle regular credit card payments
        if (string.IsNullOrWhiteSpace(tokenId))
        {
            var options = new TokenCreateOptions
            {
                Card = new TokenCardOptions
                {
                    Name = paymentInformation.NameOnCard,
                    AddressZip = paymentInformation.ZipCode,
                    Number = paymentInformation.CardNumber,
                    ExpMonth = paymentInformation.ExpireMonth,
                    ExpYear = paymentInformation.ExpireYear,
                    Cvc = paymentInformation.CVV.ToString(),
                }
            };

            var token = TokenService.Create(options);
            tokenId = token.Id;
        }

        var chargeOptions = new ChargeCreateOptions
        {
            Amount = (long)( amount * 100 ),
            Currency = "usd",
            Description = "Mobile Order",
            ReceiptEmail = email,
            Metadata = new Dictionary<string, string> { { "OrderId", orderId.ToString() } },
            Source = tokenId
        };

        var result = await ChargeService.CreateAsync(chargeOptions);

        var payment = new DB.Payment
        {
            OrderId = orderId,
            Amount = amount,
            Name = paymentInformation.NameOnCard,
            ZipCode = paymentInformation.ZipCode,
            Response = result.Outcome.ToJson(),
            ResponseCode = result.Status,
            ReceiptUrl = result.ReceiptUrl,
            AuthCode = result.Id
       };

        PaymentRepository.AddOrUpdate(payment);

        await PaymentRepository.SaveAsync();

        return Mapper.Map<PaymentResult>(payment);
}

Now, we implement the Google Pay button.

Google provides the assets for the Android resources. If you download the assets here, you'll get what you need.

Once you unzip the downloaded files, you just copy them into your Resources folder exactly as they are provided.

To implement them in Xamarin view, you'll need a custom renderer in the Android project. It renders a custom buttton - PaymentButton that is setup as follows in my Xamarin shared project.

public class PaymentButton : Button
{
}

Now for the renderer code:

using Android.Content;
using Project.Droid.Renderer;
using Project.Mobile.Controls;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(PaymentButton), typeof(PaymentButtonRenderer))]
namespace {Project.Droid.Renderer
{
    public class PaymentButtonRenderer : ViewRenderer
    {
        public PaymentButtonRenderer(Context context) : base(context) { }

        protected override void OnElementChanged(ElementChangedEventArgs<View> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement != null)
            {
                if (Control == null)
                {
                    SetNativeControl(Inflate(Context, Resource.Layout.buy_with_googlepay_button, null));
                }

                Control.Click += Control_Click;
            }
        }

        private void Control_Click(object sender, System.EventArgs e) => ( (IButtonController)Element ).SendClicked();
    }
}

That's it. If Google Wallet is setup on the device, the button will appear. When clicking on the button, you'll see the Google Pay prompt.

SHARE


comments powered by Disqus

Follow Us

Latest Posts

subscribe to our newsletter