Issue #219
Show basic add card in iOS
import UIKit
import Stripe
final class MainController: UIViewController {
func showPayment() {
let addCardViewController = STPAddCardViewController()
addCardViewController.delegate = self
let navigationController = UINavigationController(rootViewController: addCardViewController)
present(navigationController, animated: true, completion: nil)
}
}
extension MainController: STPAddCardViewControllerDelegate {
func addCardViewControllerDidCancel(_ addCardViewController: STPAddCardViewController) {
dismiss(animated: true, completion: nil)
}
func addCardViewController(_ addCardViewController: STPAddCardViewController, didCreateToken token: STPToken, completion: @escaping STPErrorBlock) {
_ = token.tokenId
completion(nil)
dismiss(animated: true, completion: nil)
}
}
Generate ephemeral key
https://stripe.com/docs/mobile/ios/standard#ephemeral-key
In order for our prebuilt UI elements to function, you’ll need to provide them with an ephemeral key, a short-lived API key with restricted API access. You can think of an ephemeral key as a session, authorizing the SDK to retrieve and update a specific Customer object for the duration of the session.
Backend in Go
https://github.com/stripe/stripe-go
Need a secret key by going to Stripe dashboard -> Developers -> API keys -> Secret key
stripe.Key = "sk_key"
Need customer id. We can manually create one in Stripe dashboard -> Customers
package main
import (
"net"
"encoding/json"
"fmt"
"net/http"
"github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/ephemeralkey"
)
func main() {
stripe.Key = "sk_test_mM2MkqO61n7vvbVRfeYmBgWm00Si2PtWab"
http.HandleFunc("/ephemeral_keys", generateEphemeralKey)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
type EphemeralKeysRequest struct {
ApiVersion string `json:"api_version"`
}
func generateEphemeralKey(w http.ResponseWriter, r *http.Request) {
customerId := "cus_Eys6aeP5xR89ab"
decoder := json.NewDecoder(r.Body)
var t EphemeralKeysRequest
err := decoder.Decode(&t)
if err != nil {
panic(err)
}
stripeVersion := t.ApiVersion
if stripeVersion == "" {
log.Printf("Stripe-Version not found\n")
w.WriteHeader(400)
return
}
params := &stripe.EphemeralKeyParams{
Customer: stripe.String(customerId),
StripeVersion: stripe.String(stripeVersion),
}
key, err := ephemeralkey.New(params)
if err != nil {
log.Printf("Stripe bindings call failed, %v\n", err)
w.WriteHeader(500)
return
}
w.Write(key.RawJSON)
}
iOS client
Networking client uses How to make simple networking client in Swift
Need an object that conforms to STPCustomerEphemeralKeyProvider
final class EphemeralKeyClient: NSObject, STPCustomerEphemeralKeyProvider {
let client = NetworkClient(baseUrl: URL(string: "http://localhost:8080")!)
func createCustomerKey(withAPIVersion apiVersion: String, completion: @escaping STPJSONResponseCompletionBlock) {
var options = Options()
options.httpMethod = .post
options.path = "ephemeral_keys"
options.parameters = [
"api_version": apiVersion
]
client.makeJson(options: options, completion: { result in
switch result {
case .success(let json):
completion(json, nil)
case .failure(let error):
completion(nil, error)
}
})
}
}
Setting up STPCustomerContext
and STPPaymentContext
final class MainController: UIViewController {
let client = EphemeralKeyClient()
let customerContext: STPCustomerContext
let paymentContext: STPPaymentContext
init() {
self.customerContext = STPCustomerContext(keyProvider: client)
self.paymentContext = STPPaymentContext(customerContext: customerContext)
super.init(nibName: nil, bundle: nil)
paymentContext.delegate = self
paymentContext.hostViewController = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func start() {
paymentContext.presentShippingViewController()
}
}
Handle charge
https://stripe.com/docs/charges
Backend in Go
If we use stripe_id
from card, which has the form of card_xxx
, we need to include customer
info
If we use token
, which has the form tok_xxx
, then no need for customer
info
From STPPaymentResult
When you’re using
STPPaymentContext
to request your user’s payment details, this is the object that will be returned to your application when they’ve successfully made a payment. It currently just contains asource
, but in the future will include any relevant metadata as well. You should passsource.stripeID
to your server, and call the charge creation endpoint. This assumes you are charging a Customer, so you should specify thecustomer
parameter to be that customer’s ID and thesource
parameter to the value returned here. For more information, see https://stripe.com/docs/api#create_charge
package main
import (
"net"
"encoding/json"
"fmt"
"net/http"
"log"
"os"
"github.com/stripe/stripe-go/charge"
)
func main() {
stripe.Key = "sk_test_mM2MkqO61n7vvbVRfeYmBgWm00Si2PtWab"
http.HandleFunc("/request_charge", handleCharge)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
var customerId = "cus_Eys6aeP5xR89ab"
type PaymentResult struct {
StripeId string `json:"stripe_id"`
}
func handleCharge(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var t PaymentResult
err := decoder.Decode(&t)
if err != nil {
panic(err)
}
params := &stripe.ChargeParams{
Amount: stripe.Int64(150),
Currency: stripe.String(string(stripe.CurrencyUSD)),
Description: stripe.String("Charge from my Go backend"),
Customer: stripe.String(customerId),
}
params.SetSource(t.StripeId)
ch, err := charge.New(params)
if err != nil {
fmt.Fprintf(w, "Could not process payment: %v", err)
fmt.Println(ch)
w.WriteHeader(400)
}
w.WriteHeader(200)
}
iOS client
final class PaymentClient {
let client = NetworkClient(baseUrl: URL(string: "http://192.168.210.219:8080")!)
func requestCharge(source: STPSourceProtocol, completion: @escaping (Result<(), Error>) -> Void) {
var options = Options()
options.httpMethod = .post
options.path = "request_charge"
options.parameters = [
"stripe_id": source.stripeID
]
client.makeJson(options: options, completion: { result in
completion(result.map({ _ in () }))
})
}
}
paymentContext.requestPayment()
extension MainController: STPPaymentContextDelegate {
func paymentContext(_ paymentContext: STPPaymentContext, didCreatePaymentResult paymentResult: STPPaymentResult, completion: @escaping STPErrorBlock) {
client.requestCharge(source: paymentResult.source, completion: { result in
switch result {
case .success:
completion(nil)
case .failure(let error):
completion(error)
}
})
}
}
Token from card
Use STPAPIClient.shared().createToken
to get token from card https://stripe.com/docs/mobile/ios/custom#collecting-card-details
let cardParams = STPCardParams()
cardParams.number = "4242424242424242"
cardParams.expMonth = 10
cardParams.expYear = 2021
cardParams.cvc = "123"
STPAPIClient.shared().createToken(withCard: cardParams) { (token: STPToken?, error: Error?) in
guard let token = token, error == nil else {
// Present error to user...
return
}
submitTokenToBackend(token, completion: { (error: Error?) in
if let error = error {
// Present error to user...
}
else {
// Continue with payment...
}
})
}
Payment options and shipping view controllers
Instead of using paymentContext
paymentContext.pushShippingViewController()
paymentContext.pushPaymentOptionsViewController()
We can use view controllers https://stripe.com/docs/mobile/ios/custom#stppaymentoptionsviewcontroller directly with STPPaymentOptionsViewController
and STPShippingAddressViewController
. Then implement STPPaymentOptionsViewControllerDelegate
and STPShippingAddressViewControllerDelegate
Register merchant Id and Apple Pay certificate
https://stripe.com/docs/apple-pay/apps
Get Certificate signing request file from Stripe https://dashboard.stripe.com/account/apple_pay
We can’t register merchant id with Enterprise account
Use Apple Pay
Go backend
Use token
type ApplePayRequest struct {
Token string `json:"token"`
}
func handleChargeUsingApplePay(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var t ApplePayRequest
err := decoder.Decode(&t)
if err != nil {
panic(err)
}
params := &stripe.ChargeParams{
Amount: stripe.Int64(150),
Currency: stripe.String(string(stripe.CurrencyUSD)),
Description: stripe.String("Charge from my Go backend for Apple Pay"),
}
params.SetSource(t.Token)
ch, err := charge.New(params)
if err != nil {
fmt.Fprintf(w, "Could not process payment: %v", err)
fmt.Println(ch)
w.WriteHeader(400)
}
w.WriteHeader(200)
}
iOS client
Update client to send STPToken
final class PaymentClient: NSObject {
let client = NetworkClient(baseUrl: URL(string: "localhost:8080")!)
func requestCharge(token: STPToken, completion: @escaping (Result<(), Error>) -> Void) {
var options = Options()
options.httpMethod = .post
options.path = "request_charge_apple_pay"
options.parameters = [
"token": token.tokenId
]
client.make(options: options, completion: { result in
completion(result.map({ _ in () }))
})
}
func useApplePay(payment: PKPayment, completion: @escaping (Result<(), Error>) -> Void) {
STPAPIClient.shared().createToken(with: payment, completion: { (token: STPToken?, error: Error?) in
guard let token = token, error == nil else {
return
}
self.requestCharge(token: token, completion: completion)
})
}
}
Use PKPaymentAuthorizationViewController
, not PKPaymentAuthorizationController
https://developer.apple.com/documentation/passkit/pkpaymentauthorizationcontroller
The PKPaymentAuthorizationController class performs the same role as the PKPaymentAuthorizationViewController class, but it does not depend on the UIKit framework. This means that the authorization controller can be used in places where a view controller cannot (for example, in watchOS apps or in SiriKit extensions).
extension MainController {
func showApplePay() {
let merchantId = "merchant.com.onmyway133.MyApp"
let paymentRequest = Stripe.paymentRequest(withMerchantIdentifier: merchantId, country: "US", currency: "USD")
paymentRequest.paymentSummaryItems = [
PKPaymentSummaryItem(label: "Rubber duck", amount: 1.5)
]
guard Stripe.canSubmitPaymentRequest(paymentRequest) else {
assertionFailure()
return
}
guard let authorizationViewController = PKPaymentAuthorizationViewController(paymentRequest: paymentRequest) else {
assertionFailure()
return
}
authorizationViewController.delegate = self
innerNavigationController.present(authorizationViewController, animated: true, completion: nil)
}
}
extension MainController: PKPaymentAuthorizationViewControllerDelegate {
func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
controller.dismiss(animated: true, completion: nil)
}
func paymentAuthorizationViewController(
_ controller: PKPaymentAuthorizationViewController,
didAuthorizePayment payment: PKPayment,
handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) {
client.useApplePay(payment: payment, completion: { result in
switch result {
case .success:
completion(.init(status: .success, errors: nil))
case .failure(let error):
completion(.init(status: .failure, errors: [error]))
}
})
}
}
Showing Apple Pay option
From appleMerchantIdentifier
The Apple Merchant Identifier to use during Apple Pay transactions. To create one of these, see our guide at https://stripe.com/docs/mobile/apple-pay . You must set this to a valid identifier in order to automatically enable Apple Pay.
if Stripe.deviceSupportsApplePay() {
STPPaymentConfiguration.shared().appleMerchantIdentifier = "merchant.com.onmyway133.MyApp"
}
paymentContext.pushPaymentOptionsViewController()
requestPayment not showing UI
From requestPayment
Requests payment from the user. This may need to present some supplemental UI to the user, in which case it will be presented on the payment context’s
hostViewController
. For instance, if they’ve selected Apple Pay as their payment method, calling this method will show the payment sheet. If the user has a card on file, this will use that without presenting any additional UI. After this is called, thepaymentContext:didCreatePaymentResult:completion:
andpaymentContext:didFinishWithStatus:error:
methods will be called on the context’sdelegate
.
Use STPPaymentOptionsViewController
to show cards and Apple Pay options
Code for requestPayment
- (void)requestPayment {
WEAK(self);
[[[self.didAppearPromise voidFlatMap:^STPPromise * _Nonnull{
STRONG(self);
return self.loadingPromise;
}] onSuccess:^(__unused STPPaymentOptionTuple *tuple) {
STRONG(self);
if (!self) {
return;
}
if (self.state != STPPaymentContextStateNone) {
return;
}
if (!self.selectedPaymentOption) {
[self presentPaymentOptionsViewControllerWithNewState:STPPaymentContextStateRequestingPayment];
}
else if ([self requestPaymentShouldPresentShippingViewController]) {
[self presentShippingViewControllerWithNewState:STPPaymentContextStateRequestingPayment];
}
else if ([self.selectedPaymentOption isKindOfClass:[STPCard class]] ||
[self.selectedPaymentOption isKindOfClass:[STPSource class]]) {
self.state = STPPaymentContextStateRequestingPayment;
STPPaymentResult *result = [[STPPaymentResult alloc] initWithSource:(id<STPSourceProtocol>)self.selectedPaymentOption];
[self.delegate paymentContext:self didCreatePaymentResult:result completion:^(NSError * _Nullable error) {
stpDispatchToMainThreadIfNecessary(^{
if (error) {
[self didFinishWithStatus:STPPaymentStatusError error:error];
} else {
[self didFinishWithStatus:STPPaymentStatusSuccess error:nil];
}
});
}];
}
else if ([self.selectedPaymentOption isKindOfClass:[STPApplePayPaymentOption class]]) {
// ....
Payment options
func paymentOptionsViewController(_ paymentOptionsViewController: STPPaymentOptionsViewController, didSelect paymentOption: STPPaymentOption) {
// No op
}
After user selects payment option, the change is saved in dashboard https://dashboard.stripe.com/test/customers
, but for card only. Select Apple Pay does not reflect change in web dashboard.
Apple pay option is added manually locally, from STPCustomer+SourceTuple.m
😲
- (STPPaymentOptionTuple *)filteredSourceTupleForUIWithConfiguration:(STPPaymentConfiguration *)configuration {
id<STPPaymentOption> _Nullable selectedMethod = nil;
NSMutableArray<id<STPPaymentOption>> *methods = [NSMutableArray array];
for (id<STPSourceProtocol> customerSource in self.sources) {
if ([customerSource isKindOfClass:[STPCard class]]) {
STPCard *card = (STPCard *)customerSource;
[methods addObject:card];
if ([card.stripeID isEqualToString:self.defaultSource.stripeID]) {
selectedMethod = card;
}
}
else if ([customerSource isKindOfClass:[STPSource class]]) {
STPSource *source = (STPSource *)customerSource;
if (source.type == STPSourceTypeCard
&& source.cardDetails != nil) {
[methods addObject:source];
if ([source.stripeID isEqualToString:self.defaultSource.stripeID]) {
selectedMethod = source;
}
}
}
}
return [STPPaymentOptionTuple tupleWithPaymentOptions:methods
selectedPaymentOption:selectedMethod
addApplePayOption:configuration.applePayEnabled];
}
STPApplePayPaymentOptionis not available in
paymentContext.paymentOptions` immediately
Change selected payment option
In STPPaymentContext
setSelectedPaymentOption
is read only and trigger paymentContextDidChange
, but it checks if the new selected payment option
is equal to existing selected payment option
- (void)setSelectedPaymentOption:(id<STPPaymentOption>)selectedPaymentOption {
if (selectedPaymentOption && ![self.paymentOptions containsObject:selectedPaymentOption]) {
self.paymentOptions = [self.paymentOptions arrayByAddingObject:selectedPaymentOption];
}
if (![_selectedPaymentOption isEqual:selectedPaymentOption]) {
_selectedPaymentOption = selectedPaymentOption;
stpDispatchToMainThreadIfNecessary(^{
[self.delegate paymentContextDidChange:self];
});
}
}
There is retryLoading
which is called at init
- (void)retryLoading {
// Clear any cached customer object before refetching
if ([self.apiAdapter isKindOfClass:[STPCustomerContext class]]) {
STPCustomerContext *customerContext = (STPCustomerContext *)self.apiAdapter;
[customerContext clearCachedCustomer];
}
WEAK(self);
self.loadingPromise = [[[STPPromise<STPPaymentOptionTuple *> new] onSuccess:^(STPPaymentOptionTuple *tuple) {
STRONG(self);
self.paymentOptions = tuple.paymentOptions;
self.selectedPaymentOption = tuple.selectedPaymentOption;
}] onFailure:^(NSError * _Nonnull error) {
STRONG(self);
if (self.hostViewController) {
[self.didAppearPromise onSuccess:^(__unused id value) {
if (self.paymentOptionsViewController) {
[self appropriatelyDismissPaymentOptionsViewController:self.paymentOptionsViewController completion:^{
[self.delegate paymentContext:self didFailToLoadWithError:error];
}];
} else {
[self.delegate paymentContext:self didFailToLoadWithError:error];
}
}];
}
}];
[self.apiAdapter retrieveCustomer:^(STPCustomer * _Nullable customer, NSError * _Nullable error) {
stpDispatchToMainThreadIfNecessary(^{
STRONG(self);
if (!self) {
return;
}
if (error) {
[self.loadingPromise fail:error];
return;
}
if (!self.shippingAddress && customer.shippingAddress) {
self.shippingAddress = customer.shippingAddress;
self.shippingAddressNeedsVerification = YES;
}
STPPaymentOptionTuple *paymentTuple = [customer filteredSourceTupleForUIWithConfiguration:self.configuration];
[self.loadingPromise succeed:paymentTuple];
});
}];
}
Which in turns call STPCustomerEphemeralKeyProvider
. As stripe does not save Apple Pay option in dashboard, this method return list of card payment options, together with the default card as selected payment option 😲
Although the new STPCard
has a different address, it is the exact same card with the same info, and the isEqual
method of STPCard
is
- (BOOL)isEqualToCard:(nullable STPCard *)other {
if (self == other) {
return YES;
}
if (!other || ![other isKindOfClass:self.class]) {
return NO;
}
return [self.stripeID isEqualToString:other.stripeID];
}
I raised an issue How to change selected payment option? hope it gets resolved soon 😢