Permalink
Switch branches/tags
Find file
4841aaf Dec 15, 2017
@bdorfman-stripe @bg-stripe @benzguo
290 lines (258 sloc) 13.6 KB
//
// CheckoutViewController.swift
// Standard Integration (Swift)
//
// Created by Ben Guo on 4/22/16.
// Copyright © 2016 Stripe. All rights reserved.
//
import UIKit
import Stripe
class CheckoutViewController: UIViewController, STPPaymentContextDelegate {
// 1) To get started with this demo, first head to https://dashboard.stripe.com/account/apikeys
// and copy your "Test Publishable Key" (it looks like pk_test_abcdef) into the line below.
let stripePublishableKey = ""
// 2) Next, optionally, to have this demo save your user's payment details, head to
// https://github.com/stripe/example-ios-backend , click "Deploy to Heroku", and follow
// the instructions (don't worry, it's free). Replace nil on the line below with your
// Heroku URL (it looks like https://blazing-sunrise-1234.herokuapp.com ).
let backendBaseURL: String? = nil
// 3) Optionally, to enable Apple Pay, follow the instructions at https://stripe.com/docs/mobile/apple-pay
// to create an Apple Merchant ID. Replace nil on the line below with it (it looks like merchant.com.yourappname).
let appleMerchantID: String? = nil
// These values will be shown to the user when they purchase with Apple Pay.
let companyName = "Emoji Apparel"
let paymentCurrency = "usd"
let paymentContext: STPPaymentContext
let theme: STPTheme
let paymentRow: CheckoutRowView
let shippingRow: CheckoutRowView
let totalRow: CheckoutRowView
let buyButton: BuyButton
let rowHeight: CGFloat = 44
let productImage = UILabel()
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
let numberFormatter: NumberFormatter
let shippingString: String
var product = ""
var paymentInProgress: Bool = false {
didSet {
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn, animations: {
if self.paymentInProgress {
self.activityIndicator.startAnimating()
self.activityIndicator.alpha = 1
self.buyButton.alpha = 0
}
else {
self.activityIndicator.stopAnimating()
self.activityIndicator.alpha = 0
self.buyButton.alpha = 1
}
}, completion: nil)
}
}
init(product: String, price: Int, settings: Settings) {
let stripePublishableKey = self.stripePublishableKey
let backendBaseURL = self.backendBaseURL
assert(stripePublishableKey.hasPrefix("pk_"), "You must set your Stripe publishable key at the top of CheckoutViewController.swift to run this app.")
assert(backendBaseURL != nil, "You must set your backend base url at the top of CheckoutViewController.swift to run this app.")
self.product = product
self.productImage.text = product
self.theme = settings.theme
MyAPIClient.sharedClient.baseURLString = self.backendBaseURL
// This code is included here for the sake of readability, but in your application you should set up your configuration and theme earlier, preferably in your App Delegate.
let config = STPPaymentConfiguration.shared()
config.publishableKey = self.stripePublishableKey
config.appleMerchantIdentifier = self.appleMerchantID
config.companyName = self.companyName
config.requiredBillingAddressFields = settings.requiredBillingAddressFields
config.requiredShippingAddressFields = settings.requiredShippingAddressFields
config.shippingType = settings.shippingType
config.additionalPaymentMethods = settings.additionalPaymentMethods
let customerContext = STPCustomerContext(keyProvider: MyAPIClient.sharedClient)
let paymentContext = STPPaymentContext(customerContext: customerContext,
configuration: config,
theme: settings.theme)
let userInformation = STPUserInformation()
paymentContext.prefilledInformation = userInformation
paymentContext.paymentAmount = price
paymentContext.paymentCurrency = self.paymentCurrency
let paymentSelectionFooter = PaymentContextFooterView(text: "You can add custom footer views to the payment selection screen.")
paymentSelectionFooter.theme = settings.theme
paymentContext.paymentMethodsViewControllerFooterView = paymentSelectionFooter
let addCardFooter = PaymentContextFooterView(text: "You can add custom footer views to the add card screen.")
addCardFooter.theme = settings.theme
paymentContext.addCardViewControllerFooterView = addCardFooter
self.paymentContext = paymentContext
self.paymentRow = CheckoutRowView(title: "Payment", detail: "Select Payment",
theme: settings.theme)
var shippingString = "Contact"
if config.requiredShippingAddressFields?.contains(.postalAddress) ?? false {
shippingString = config.shippingType == .shipping ? "Shipping" : "Delivery"
}
self.shippingString = shippingString
self.shippingRow = CheckoutRowView(title: self.shippingString,
detail: "Enter \(self.shippingString) Info",
theme: settings.theme)
self.totalRow = CheckoutRowView(title: "Total", detail: "", tappable: false,
theme: settings.theme)
self.buyButton = BuyButton(enabled: true, theme: settings.theme)
var localeComponents: [String: String] = [
NSLocale.Key.currencyCode.rawValue: self.paymentCurrency,
]
localeComponents[NSLocale.Key.languageCode.rawValue] = NSLocale.preferredLanguages.first
let localeID = NSLocale.localeIdentifier(fromComponents: localeComponents)
let numberFormatter = NumberFormatter()
numberFormatter.locale = Locale(identifier: localeID)
numberFormatter.numberStyle = .currency
numberFormatter.usesGroupingSeparator = true
self.numberFormatter = numberFormatter
super.init(nibName: nil, bundle: nil)
self.paymentContext.delegate = self
paymentContext.hostViewController = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = self.theme.primaryBackgroundColor
var red: CGFloat = 0
self.theme.primaryBackgroundColor.getRed(&red, green: nil, blue: nil, alpha: nil)
self.activityIndicator.activityIndicatorViewStyle = red < 0.5 ? .white : .gray
self.navigationItem.title = "Emoji Apparel"
self.productImage.font = UIFont.systemFont(ofSize: 70)
self.view.addSubview(self.totalRow)
self.view.addSubview(self.paymentRow)
self.view.addSubview(self.shippingRow)
self.view.addSubview(self.productImage)
self.view.addSubview(self.buyButton)
self.view.addSubview(self.activityIndicator)
self.activityIndicator.alpha = 0
self.buyButton.addTarget(self, action: #selector(didTapBuy), for: .touchUpInside)
self.totalRow.detail = self.numberFormatter.string(from: NSNumber(value: Float(self.paymentContext.paymentAmount)/100))!
self.paymentRow.onTap = { [weak self] in
self?.paymentContext.pushPaymentMethodsViewController()
}
self.shippingRow.onTap = { [weak self] in
self?.paymentContext.pushShippingViewController()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
var insets = UIEdgeInsets.zero
if #available(iOS 11.0, *) {
insets = view.safeAreaInsets
}
let width = self.view.bounds.width - (insets.left + insets.right)
self.productImage.sizeToFit()
self.productImage.center = CGPoint(x: width/2.0,
y: self.productImage.bounds.height/2.0 + rowHeight)
self.paymentRow.frame = CGRect(x: insets.left, y: self.productImage.frame.maxY + rowHeight,
width: width, height: rowHeight)
self.shippingRow.frame = CGRect(x: insets.left, y: self.paymentRow.frame.maxY,
width: width, height: rowHeight)
self.totalRow.frame = CGRect(x: insets.left, y: self.shippingRow.frame.maxY,
width: width, height: rowHeight)
self.buyButton.frame = CGRect(x: insets.left, y: 0, width: 88, height: 44)
self.buyButton.center = CGPoint(x: width/2.0, y: self.totalRow.frame.maxY + rowHeight*1.5)
self.activityIndicator.center = self.buyButton.center
}
@objc func didTapBuy() {
self.paymentInProgress = true
self.paymentContext.requestPayment()
}
// MARK: STPPaymentContextDelegate
func paymentContext(_ paymentContext: STPPaymentContext, didCreatePaymentResult paymentResult: STPPaymentResult, completion: @escaping STPErrorBlock) {
MyAPIClient.sharedClient.completeCharge(paymentResult,
amount: self.paymentContext.paymentAmount,
shippingAddress: self.paymentContext.shippingAddress,
shippingMethod: self.paymentContext.selectedShippingMethod,
completion: completion)
}
func paymentContext(_ paymentContext: STPPaymentContext, didFinishWith status: STPPaymentStatus, error: Error?) {
self.paymentInProgress = false
let title: String
let message: String
switch status {
case .error:
title = "Error"
message = error?.localizedDescription ?? ""
case .success:
title = "Success"
message = "You bought a \(self.product)!"
case .userCancellation:
return
}
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let action = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(action)
self.present(alertController, animated: true, completion: nil)
}
func paymentContextDidChange(_ paymentContext: STPPaymentContext) {
self.paymentRow.loading = paymentContext.loading
if let paymentMethod = paymentContext.selectedPaymentMethod {
self.paymentRow.detail = paymentMethod.label
}
else {
self.paymentRow.detail = "Select Payment"
}
if let shippingMethod = paymentContext.selectedShippingMethod {
self.shippingRow.detail = shippingMethod.label
}
else {
self.shippingRow.detail = "Enter \(self.shippingString) Info"
}
self.totalRow.detail = self.numberFormatter.string(from: NSNumber(value: Float(self.paymentContext.paymentAmount)/100))!
}
func paymentContext(_ paymentContext: STPPaymentContext, didFailToLoadWithError error: Error) {
let alertController = UIAlertController(
title: "Error",
message: error.localizedDescription,
preferredStyle: .alert
)
let cancel = UIAlertAction(title: "Cancel", style: .cancel, handler: { action in
// Need to assign to _ because optional binding loses @discardableResult value
// https://bugs.swift.org/browse/SR-1681
_ = self.navigationController?.popViewController(animated: true)
})
let retry = UIAlertAction(title: "Retry", style: .default, handler: { action in
self.paymentContext.retryLoading()
})
alertController.addAction(cancel)
alertController.addAction(retry)
self.present(alertController, animated: true, completion: nil)
}
// Note: this delegate method is optional. If you do not need to collect a
// shipping method from your user, you should not implement this method.
func paymentContext(_ paymentContext: STPPaymentContext, didUpdateShippingAddress address: STPAddress, completion: @escaping STPShippingMethodsCompletionBlock) {
let upsGround = PKShippingMethod()
upsGround.amount = 0
upsGround.label = "UPS Ground"
upsGround.detail = "Arrives in 3-5 days"
upsGround.identifier = "ups_ground"
let upsWorldwide = PKShippingMethod()
upsWorldwide.amount = 10.99
upsWorldwide.label = "UPS Worldwide Express"
upsWorldwide.detail = "Arrives in 1-3 days"
upsWorldwide.identifier = "ups_worldwide"
let fedEx = PKShippingMethod()
fedEx.amount = 5.99
fedEx.label = "FedEx"
fedEx.detail = "Arrives tomorrow"
fedEx.identifier = "fedex"
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
if address.country == nil || address.country == "US" {
completion(.valid, nil, [upsGround, fedEx], fedEx)
}
else if address.country == "AQ" {
let error = NSError(domain: "ShippingError", code: 123, userInfo: [NSLocalizedDescriptionKey: "Invalid Shipping Address",
NSLocalizedFailureReasonErrorKey: "We can't ship to this country."])
completion(.invalid, error, nil, nil)
}
else {
fedEx.amount = 20.99
completion(.valid, nil, [upsWorldwide, fedEx], fedEx)
}
}
}
}