
Apple Pay, Stripe & Xamarin
- By Justin Wright
- Sr Software Engineer
I spent a fair amount of time researching the best practices for implementing Apple Pay, Xamarin, and Stripe, but came away mostly empty-handed. Here's what I did to make it all work together nicely.
Begin by setting up Apple Pay in your Stripe dashboard.
While in stripe, go to Settings -> Payment Methods -> Select Configure next to Apple Pay.
Under iOS certificates, click 'Add new application' button.
This will pop up a dialog like this:
This will download a certificate signing request file. Click Continue.
Then:
- Select the link provided - 'this page' to go to Apple Developer Center.
- Sign in and click the + button next to the 'Identifiers' title.
- Select Merchant IDs and click 'Continue' to create your Merchant ID.
- Click 'Register'.
- Select your new Merchant ID from the list.
- Click on 'Create Certificate'.
- For 'Will payments associated with this Merchant ID be processed exclusively in China mainland?' -> Select 'No'.
- Select 'Choose File' when prompted to 'Upload a Certificate Signing Request'.
- Find your previously downloaded 'stripe.certSigning.Request' file and click 'Continue'.
- Select 'Download' to get your 'apple_pay.cer'.
- Now switch back to your Stripe tab - (should still be in another tab) and hit 'Continue'.
- Click 'Upload certificate' and click 'Submit'.
Stripe and Apple Pay are now ready to go.
To set up your app, open the Entitlements.plist
file in your .iOS Xamarin project.
Enable the Apple Pay Entitlement and add your Merchant ID to the list.
To use Apple Pay in your project, you'll need to reference the following NuGet packages.
- Stripe.iOS
- Xamarin.iOS
Create a Service in your iOS Xamarin project that implements PKPaymentAuthorizationControllerDelegate
.
(public class PaymentService : PKPaymentAuthorizationControllerDelegate)
Implementing the abstract class will create the following two methods:
public override void DidAuthorizePayment(PKPaymentAuthorizationController controller, PKPayment payment, [BlockProxy(typeof(NIDActionArity1V213))] Action completion) => throw new NotImplementedException();
public override void DidFinish(PKPaymentAuthorizationController controller) => throw new NotImplementedException();
There will also be an error that it can't find the namespace for NIDActionArity1V213
.
Just delete this portion [BlockProxy(typeof(NIDActionArity1V213))]
of the DidAuthorizePayment
method.
Next, you will need to initialize your service with the Merchant ID and your Stripe Publishable Key.
(This can be done in your AppDelegate.cs
or the constructor of your service.)
var client = new ApiClient(apiConfiguration.StripePublishableKey);
client.Configuration.AppleMerchantIdentifier = apiConfiguration.MerchantId;
ApiClient.SharedClient.PublishableKey = apiConfiguration.StripePublishableKey;
Create an interface in your shared Xamarin project for use within your pages (ie. IPaymentService.c
s)
Add the following methods:
event EventHandler AuthorizationComplete;
bool CanMakePayments { get; }
void AuthorizePayment(decimal total);
Add this interface to your service in the iOS service and implement its methods.
public class PaymentService : PKPaymentAuthorizationControllerDelegate, IPaymentService
Register your service and interface in your AppDelegate.cs
.
services.AddSingleton();
Now add the following code to your new methods.
public bool CanMakePayments => StripeSdk.DeviceSupportsApplePay;
public void AuthorizePayment(decimal total)
{
var request = new PKPaymentRequest
{
PaymentSummaryItems = new[] { new PKPaymentSummaryItem { Label = "GoSnapShop", Amount = new NSDecimalNumber(total), Type = PKPaymentSummaryItemType.Final } },
CountryCode = "US",
CurrencyCode = "USD",
MerchantIdentifier = ApiConfiguration.MerchantId,
MerchantCapabilities = PKMerchantCapability.ThreeDS,
SupportedNetworks = new[] { PKPaymentNetwork.Amex, PKPaymentNetwork.MasterCard, PKPaymentNetwork.Visa }
};
var authorization = new PKPaymentAuthorizationController(request)
{
Delegate = (IPKPaymentAuthorizationControllerDelegate)Self
};
authorization.Present(null);
}
public override void DidAuthorizePayment(PKPaymentAuthorizationController controller, PKPayment payment, Action completion)
{
ApiClient.SharedClient.CreateToken(payment, TokenComplete);
completion(PKPaymentAuthorizationStatus.Success);
}
protected void TokenComplete(Token token, NSError arg1) => AuthorizationComplete.Invoke(this, token.TokenId);
public override void DidFinish(PKPaymentAuthorizationController controller) => controller.Dismiss(null);
You're now ready to implement your service in your ViewModel.
Add IPaymentService paymentService
to the constructor of your ViewModel to inject it as a dependency and then initialize it as a property.
protected IPaymentService PaymentService { get; }
public AddCreditCardViewModel(IRouteNavigator routeNavigator, IShoppingCartService shoppingCartService,
IStorageService storageService, IOrderService orderService, IPaymentService paymentService)
{
RouteNavigator = routeNavigator;
ShoppingCartService = shoppingCartService;
StorageService = storageService;
PaymentService = paymentService;
You can also subscribe to your event in the constructor.
PaymentService.AuthorizationComplete += PaymentService_AuthorizationComplete;
private void PaymentService_AuthorizationComplete(object sender, string token)
{
PaymentToken = token;
Task.Run(async () => await PlaceOrder());
}
Next, you'll need to wire up your button. In your page's xaml - add the button (or image).
Your button IsVisible
will be bound to a property in your view model.
private bool IsApplePayReadyField;
public bool IsApplePayReady
{
get => IsApplePayReadyField;
set { IsApplePayReadyField = value; NotifyOfChange(); }
}
This will be initialized in your OnActivate
method.
IsApplePayReady = PaymentService.CanMakePayments;
Now, you're ready to capture the payment.
On the server side, you'll create an API method that allows you to pass the amount to capture along with your token.
In my case, I'm using a model that will be part of the order save method.
var order = new Order
{
OrderId = 1,
Total = 100.00,
PaymentInformation = new PaymentInformation { NameOnCard = "Mobile Wallet Payment", WalletPaymentToken = PaymentToken };
}
The controller method is implemented as follows:
[HttpPost("add")]
public async Task AddOrder([FromBody] Models.Order order)
{
try
{
var savedOrder = await OrderService.AddOrder(Mapper.Map(order));
var mapped = Mapper.Map(savedOrder);
mapped.PaymentResult = Mapper.Map(await PaymentService.AddPayment(savedOrder.OrderId, savedOrder.Total, Mapper.Map(order.PaymentInformation)));
return Json(mapped);
}
catch (Exception e)
{
return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
}
}
The PaymentService.AddPayment
is then created to capture the payment.
public async Task AddPayment(int orderId, decimal amount, PaymentInformation paymentInformation)
{
string tokenId = paymentInformation.WalletPaymentToken;
if (string.IsNullOrWhiteSpace(tokenId))
{
// handle regular credit card payments
}
var chargeOptions = new ChargeCreateOptions
{
Amount = (long)( amount * 100 ),
Currency = "usd",
Description = "GoSnapShop.com Mobile Order",
Metadata = new Dictionary { { "OrderId", orderId.ToString() } },
Source = tokenId
};
var service = new ChargeService();
var result = await service.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(payment);
}
The following result will be returned from the ChargeService.CreateAsync()
.
{
"id": "ch_1IEdnPIzLphRtLIE89seDJAL",
"object": "charge",
"amount": 198,
"amount_captured": 198,
"amount_refunded": 0,
"application": null,
"application_fee": null,
"application_fee_amount": null,
"authorization_code": null,
"balance_transaction": "txn_1IEdnPIzLphRtLIERwOqJS6s",
"billing_details": {
"address": {
"city": null,
"country": null,
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": null,
"name": null,
"phone": null
},
"calculated_statement_descriptor": "JUSTIN TEST ACCOUNT",
"captured": true,
"created": 1611852999,
"currency": "usd",
"customer": null,
"description": "GoSnapShop.com Mobile Order",
"destination": null,
"dispute": null,
"disputed": false,
"failure_code": null,
"failure_message": null,
"fraud_details": {
"stripe_report": null,
"user_report": null
},
"invoice": null,
"level3": null,
"livemode": false,
"metadata": {
"OrderId": "29"
},
"on_behalf_of": null,
"order": null,
"outcome": {
"network_status": "approved_by_network",
"reason": null,
"risk_level": "normal",
"risk_score": 42,
"rule": null,
"seller_message": "Payment complete.",
"type": "authorized"
},
"paid": true,
"payment_intent": null,
"payment_method": "card_1IEdnJIzLphRtLIEKPw7oitN",
"payment_method_details": {
"ach_credit_transfer": null,
"ach_debit": null,
"acss_debit": null,
"alipay": null,
"au_becs_debit": null,
"bacs_debit": null,
"bancontact": null,
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": null
},
"country": "US",
"description": null,
"exp_month": 12,
"exp_year": 2022,
"fingerprint": "v3sKq9NqSbnF3ZLd",
"funding": "credit",
"iin": null,
"installments": null,
"issuer": null,
"last4": "4242",
"moto": null,
"network": "visa",
"three_d_secure": null,
"wallet": {
"amex_express_checkout": null,
"apple_pay": {},
"dynamic_last4": "4242",
"google_pay": null,
"masterpass": null,
"samsung_pay": null,
"type": "apple_pay",
"visa_checkout": null
}
},
"card_present": null,
"eps": null,
"fpx": null,
"giropay": null,
"grabpay": null,
"ideal": null,
"interac_present": null,
"klarna": null,
"multibanco": null,
"oxxo": null,
"p24": null,
"sepa_debit": null,
"stripe_account": null,
"type": "card",
"wechat": null
},
"receipt_email": null,
"receipt_number": null,
"receipt_url": "https://pay.stripe.com/receipts/acct_1CLZd5IzLphRtLIE/ch_1IEdnPIzLphRtLIE89seDJAL/rcpt_IqKSxnqxmmzwb2H53UhVjybaf7eH14Q",
"refunded": false,
"refunds": {
"object": "list",
"data": [],
"has_more": false,
"url": "/v1/charges/ch_1IEdnPIzLphRtLIE89seDJAL/refunds"
},
"review": null,
"shipping": null,
"source": {
"id": "card_1IEdnJIzLphRtLIEKPw7oitN",
"object": "card",
"account": null,
"address_city": null,
"address_country": null,
"address_line1": null,
"address_line1_check": null,
"address_line2": null,
"address_state": null,
"address_zip": null,
"address_zip_check": null,
"available_payout_methods": null,
"brand": "Visa",
"country": "US",
"currency": null,
"customer": null,
"cvc_check": null,
"default_for_currency": null,
"description": null,
"dynamic_last4": "4242",
"exp_month": 12,
"exp_year": 2022,
"fingerprint": "v3sKq9NqSbnF3ZLd",
"funding": "credit",
"iin": null,
"issuer": null,
"last4": "4242",
"metadata": {},
"name": null,
"tokenization_method": "apple_pay"
},
"source_transfer": null,
"statement_descriptor": null,
"statement_descriptor_suffix": null,
"status": "succeeded",
"transfer": null,
"transfer_data": null,
"transfer_group": null
}