I am integrating apple pay. I am following applepay documentation. Here is my code.
import UIKit
import PassKit
class ApplePayViewController: UIViewController {
#IBOutlet var lblLoading : UILabel!
#IBOutlet var loader : UIActivityIndicatorView!
var totalAmount = "100.0"
let paymentRequest = PKPaymentRequest()
override func viewDidLoad() {
super.viewDidLoad()
// Set up the payment request
paymentRequest.merchantIdentifier = "merchant.com.apple.example"
paymentRequest.supportedNetworks = [.visa, .masterCard, .amex, .discover]
paymentRequest.merchantCapabilities = .capability3DS
paymentRequest.countryCode = "US"
paymentRequest.currencyCode = "USD"
// Add a payment item
let item = PKPaymentSummaryItem(label: "Total", amount: NSDecimalNumber(string: totalAmount))
paymentRequest.paymentSummaryItems = [item]
// Check if the device is capable of making payments
if PKPaymentAuthorizationViewController.canMakePayments() {
let authorizationViewController = PKPaymentAuthorizationViewController(paymentRequest: paymentRequest)
authorizationViewController?.delegate = self
present(authorizationViewController!, animated: true, completion: nil)
} else {
SharedManager.showAlertWithMessage(title: NSLocalizedString("Sorry", comment: ""), alertMessage: "This device is not capable of making payments.", viewController: self)
}
}}
extension ApplePayViewController: PKPaymentAuthorizationViewControllerDelegate {
func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
controller.dismiss(animated: true, completion: nil)
}
func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, handler completion: #escaping (PKPaymentAuthorizationResult) -> Void) {
// Verify the payment with your server
// ...
completion(PKPaymentAuthorizationResult(status: .success, errors: nil))
}}
In didAuthorizePayment delegate i dont know how to verify the payment with server. I cannot find any function or post data related to this in document. Can you help me with this.
Thank you
You need to have a payment processor (like Stripe) to actually do the transaction.
See this tutorial: https://www.kodeco.com/2113-apple-pay-tutorial-getting-started
It explains how to do this with Stripe.
In this function, if Stripe is successful, you will also have to tell your own backend all of the information it needs to actually send the product or whatever the user ordered.
Related
Apple Pay token contains wrong transactionAmount info. E.g if the transaction amount is $1.10, on iOS device, Apple Pay screen stills show correct amount but paymentToken returns 110 when decrypted
I used this library to decrypt Apple Pay token using my own public-private key pair
https://github.com/sidimansourjs/applepay-token
My sample code
class ViewController: UIViewController {
...
#IBAction func payBtn(_ sender: UIButton) {
do {
let paymentItem = PKPaymentSummaryItem.init(label: "Test item", amount: NSDecimalNumber(value: 1.10))
let paymentNetworks = [PKPaymentNetwork.amex, .discover, .masterCard, .visa]
if PKPaymentAuthorizationViewController.canMakePayments(usingNetworks: paymentNetworks) {
let request = PKPaymentRequest()
request.currencyCode = "SGD" // 1
request.countryCode = "SG" // 2
request.merchantIdentifier = "merchant.com.xxxxx" // 3
request.merchantCapabilities = PKMerchantCapability.capability3DS // 4
request.supportedNetworks = paymentNetworks // 5
request.paymentSummaryItems = [paymentItem] // 6
guard let paymentVC = PKPaymentAuthorizationViewController(paymentRequest: request) else {
displayDefaultAlert(title: "Error", message: "Unable to present Apple Pay authorization.")
return
}
paymentVC.delegate = self
self.present(paymentVC, animated: true, completion: nil)
} else {
displayDefaultAlert(title: "Error", message: "Unable to make Apple Pay transaction.")
}
} catch {
print(error.localizedDescription)
}
}
}
extension ViewController: PKPaymentAuthorizationViewControllerDelegate {
func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
dismiss(animated: true, completion: nil)
}
func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, handler completion: #escaping (PKPaymentAuthorizationResult) -> Void) {
let token = String(data: payment.token.paymentData, encoding: .utf8)
let utf8str = token!.data(using: .utf8)
if let base64Encoded = utf8str?.base64EncodedString()
{
print("Encoded: \(base64Encoded)")
//Send token to backend server to decrypt
}
}
}
I discovered this when try to make payment request to Adyen (a payment gateway). Request must contain both amount field and paymentToken field but was never successful because amount in amount and paymentToken is mis-matched. Then I tried amount 110 instead of 1.10 then request was success, but ironically in Adyen dashboard somehow it still understood transaction amount was $1.10
I expected transactionAmount in Apple Pay token to be 1.10
Please help to explain why there's this difference in Apple Pay amount shown on device and in token
Found answer as suggested by #Don
Singapore Dollar SGD's smallest unit is cent, which is 1/100 of a dollar. Therefore I need to convert dollar amount into cent integer by multiply with 100. Then, use this value when send over to Adyen
I implemented apple pay with stripe in ios using swift. So when the time of testing a program it is runs fine with simulators but not in real device. To test the apple pay with real device it doesn't show the payment sheet to process the transaction. My doubt is to test with apple pay with real device can we add test cards to wallet or have the sandbox account ? is required adding test cards to wallet when using stripe gate way? Could you please give me a clarity?
This is my code :
import UIKit
import PassKit
import Stripe
class ViewController: UIViewController {
let request = PKPaymentRequest()
var paymentStatus = PKPaymentAuthorizationStatus.failure
var paymentController: PKPaymentAuthorizationController?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func applepay(_ sender: Any) {
let merchantIdentifier = "merchant.Appplepay"
let paymentNetworks = [PKPaymentNetwork.masterCard, PKPaymentNetwork.visa,PKPaymentNetwork.amex,PKPaymentNetwork.discover,PKPaymentNetwork.interac]
if PKPaymentAuthorizationViewController.canMakePayments(usingNetworks: paymentNetworks)
{
request.merchantIdentifier = "merchant.Appplepay"
request.countryCode = "US"
request.currencyCode = "USD"
request.supportedNetworks = paymentNetworks
request.merchantCapabilities = .capability3DS
request.requiredShippingAddressFields = [.phone, .email,.postalAddress,.all]
request.paymentSummaryItems = [PKPaymentSummaryItem(label: "Fancy Hat", amount: 50.00),
// The final line should represent your company;
// it'll be prepended with the word "Pay" (i.e. "Pay iHats, Inc $50")
PKPaymentSummaryItem(label: "iHats, Inc", amount: 50.00),]
} else {
// Traditional checkout flow
}
func applePaySupported() -> Bool {
return PKPaymentAuthorizationViewController.canMakePayments() && PKPaymentAuthorizationViewController.canMakePayments(usingNetworks: [.amex, .visa, .masterCard])
}
// Setup payment authorization view controller
if Stripe.canSubmitPaymentRequest(request) {
// Setup payment authorization view controller
let paymentAuthorizationViewController = PKPaymentAuthorizationViewController(paymentRequest: request)
paymentAuthorizationViewController?.delegate = self
// Present payment authorization view controller
self.present((paymentAuthorizationViewController)!, animated: true,completion: nil)
}
else {
// There is a problem with your Apple Pay configuration
print("Failed to present PaymentSheet");
}
}
}
extension ViewController : PKPaymentAuthorizationViewControllerDelegate {
func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, completion: #escaping (PKPaymentAuthorizationStatus) -> Void) {
if payment.shippingContact?.emailAddress == nil || payment.shippingContact?.phoneNumber == nil {
paymentStatus = .invalidShippingContact
} else {
STPAPIClient.shared().createToken(with: payment) { (token: STPToken?, error)-> Void in
print("Stripe token is \(String(describing: token!))")
//self.paymentStatus = .success
completion(.success)
}
}
}
func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
controller.dismiss(animated: true, completion: nil)
}
}
To answer your questions about testing on a real device: You can only add real cards to your wallet to use with Apple Pay, however if you use a test stripe api key, that is detected by stripe and your real card will not be charged
Not sure why the payment sheet isn't displaying on the real device without seeing some code, but you would want to make sure that the device is set up for apple pay, mainly that it has cards added to the wallet (that you support)
Not all countries support Apple Pay so there isn't always an option to add a card to the wallet, this would mean that the Apple Pay sheet wouldn't display due to if Stripe.canSubmitPaymentRequest(request) returning false.
I'm implementing In-App purchases in an XCode project, and everything works fine except for one error. When the user isn't connected to the internet and he clicks a purchase button, the app crashes. I believe this happens because the in-app purchases haven't been fetched from iTunes and clicking the button can't run the purchase process. This crash also happens when the user clicks the button on the first second that the shop screen loads, because – I think – some time is needed to fetch (or request) the products. Here's what I'm talking about:
override func didMove(to view: SKView) {
...
fetchAvailableProducts()
}
func fetchAvailableProducts() {
// Put here your IAP Products ID's
let productIdentifiers = NSSet(objects:
productID100,
productID250,
productID500,
productIDRemoveAds,
productIDUnlockAll)
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set<String>)
productsRequest.delegate = self
productsRequest.start()
}
I'm basing my code on this tutorial.
Is there a way to change my code to make it "crash-proof", so that it first checks if the products can be bought to let you use the button?
I use SwiftyStorkeKit ( https://github.com/bizz84/SwiftyStoreKit ) , which uses a completion handler to fill in the products (will save you from reinventing the wheel as well - and is a great resource to learn from)
As for checking network connection, I use Apple's Reachability ( https://developer.apple.com/library/content/samplecode/Reachability/Introduction/Intro.html ). Here is the relevant parts of an implementation. It also check in situations where the app loses focus. You can also use the network check before any store operation.
class vcon: UIViewController {
#IBOutlet weak var noConnectionView: UIView!
func isNetworkAvailable() -> Bool {
//quick test if network is available
var netTest:Reachability? = Reachability(hostName: "apple.com")!
if netTest?.currentReachabilityStatus == .notReachable {
netTest = nil
return false
}
netTest = nil
return true
}
func displayNoNetworkView() {
//this example pulls from a storyboard to a view I have in front of everything else at all times, and shows the view to block everything else if the network isnt available
let ncvc = UIStoryboard(name: "HelpPrefsInfo", bundle: nil).instantiateViewController(withIdentifier: "noNetworkVC") as! noNetworkVC
ncvc.view.frame = noConnectionView.bounds
ncvc.view.backgroundColor = color03
ncvc.no_connection_imageView.tintColor = color01
ncvc.noInternetConnection_label.textColor = color01
noConnectionView.addSubview(ncvc.view)
}
func hideDataIfNoConnection() {
//the actual code that displays the no net connection view
if !isNetworkAvailable() {
if noConnectionView.isHidden == true {
noConnectionView.alpha = 0
noConnectionView.isHidden = false
self.iapObjects = []
UIView.animate(withDuration: 0.50, animations: {
self.noConnectionView.alpha = 1
}, completion:{(finished : Bool) in
});
}
} else {
if noConnectionView.isHidden == false {
self.collection_view.reloadData()
UIView.animate(withDuration: 0.50, animations: {
self.noConnectionView.alpha = 0
}, completion:{(finished : Bool) in
self.noConnectionView.isHidden = true
self.loadIAPData()
});
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: .UIApplicationDidBecomeActive, object: nil)
displayNoNetworkView()
loadIAPData()
}
func loadIAPData() {
load the data if the network is available
if isNetworkAvailable() {
helper.requestProductsWithCompletionHandler(completionHandler: { (success, products) -> Void in
if success {
self.iapObjects = products!
self.collection_view.reloadData()
} else {
let alert = UIAlertController(title: "Error", message: "Cannot retrieve products list right now.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
})
}
}
func willEnterForeground() {
hideDataIfNoConnection()
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
hideDataIfNoConnection()
}
In my app i have a option to login to the app using google sign in. Login is working fine. Once i click the Logout Button, I am not able to logout from google. When ever I click on login button it's not showing login page as image given below:
Instead its redirecting to authentication dialog page as image given below:
Code :
override func viewDidLoad()
{
super.viewDidLoad()
GIDSignIn.sharedInstance().uiDelegate = self
let button = GIDSignInButton(frame: CGRectMake(0, 0, 100, 100))
button.center = view.center
view.addSubview(button)
}
#IBAction func signOutButton(sender: AnyObject) {
GIDSignIn.sharedInstance().signOut()
}
1.import below
import GoogleAPIClient
import GTMOAuth2
2.declare below variable
let kKeychainItemName = "your app name"
let kClientID = "your app clinet id"
let scopes = [kGTLAuthScopeDrive]
let service = GTLServiceDrive()
3.replace this methods to your existing one
override func viewDidLoad() {
super.viewDidLoad()
if let auth = GTMOAuth2ViewControllerTouch.authForGoogleFromKeychainForName(
kKeychainItemName,
clientID: kClientID,
clientSecret: nil) {
service.authorizer = auth
}
self.tblView.tableFooterView=UIView()
// Do any additional setup after loading the view.
}
override func viewDidAppear(animated: Bool) {
if let authorizer = service.authorizer,
canAuth = authorizer.canAuthorize where canAuth {
fetchFiles()
} else {
presentViewController(
createAuthController(),
animated: true,
completion: nil
)
}
}
func fetchFiles() {
let query = GTLQueryDrive.queryForFilesList()
query.pageSize = 10
query.fields = "nextPageToken, files(id, name)"
service.executeQuery(
query,
delegate: self,
didFinishSelector: #selector(GoogleDriveVC.displayResultWithTicket(_:finishedWithObject:error:))
)
}
// Parse results and display
func displayResultWithTicket(ticket : GTLServiceTicket,
finishedWithObject response : GTLDriveFileList,
error : NSError?) {
if let error = error {
showAlert("Error", message: error.localizedDescription)
return
}
if let files = response.files where !files.isEmpty {
for file in files as! [GTLDriveFile] {
self.arrayOfNames.append(file.name)
self.arrayOfIdentifier.append(file.identifier)
}
}
self.tblView.reloadData()
}
// Creates the auth controller for authorizing access to Drive API
private func createAuthController() -> GTMOAuth2ViewControllerTouch {
let scopeString = scopes.joinWithSeparator(" ")
return GTMOAuth2ViewControllerTouch(
scope: scopeString,
clientID: kClientID,
clientSecret: nil,
keychainItemName: kKeychainItemName,
delegate: self,
finishedSelector: #selector(GoogleDriveVC.viewController(_:finishedWithAuth:error:))
)
}
// Handle completion of the authorization process, and update the Drive API
// with the new credentials.
func viewController(vc : UIViewController,
finishedWithAuth authResult : GTMOAuth2Authentication, error : NSError?) {
if let error = error {
service.authorizer = nil
showAlert("Authentication Error", message: error.localizedDescription)
return
}
service.authorizer = authResult
dismissViewControllerAnimated(true, completion: nil)
}
// Helper for showing an alert
func showAlert(title : String, message: String) {
let alert = UIAlertController(
title: title,
message: message,
preferredStyle: UIAlertControllerStyle.Alert
)
let ok = UIAlertAction(
title: "OK",
style: UIAlertActionStyle.Default,
handler: nil
)
alert.addAction(ok)
presentViewController(alert, animated: true, completion: nil)
}
at last logout using
func logout(){
//logout code
GTMOAuth2ViewControllerTouch.removeAuthFromKeychainForName(kKeychainItemName)
navigationController?.popViewControllerAnimated(true)
}
this is the full implementation
If OP is still looking (doubt it since it's been 3 years) and if anyone is still working on this I think I figured it out.
I'm using Objective C but the methodology is still the same.
I have an app that uses Google Sign In to authenticate. This work as OP has described. For our sign out I have the following:
(IBAction)didTapSignOut:(id)sender
{
GIDSignIn *gidObject = [GIDSignIn sharedInstance];
[gidObject signOut];
[gidObject disconnect];
NSString *logOutUrl = #"https://www.google.com/accounts/Logout";
[[UIApplication sharedApplication] openURL:[NSURL URLWithString: logOutUrl] options:#{} completionHandler:nil];
}
Make sure signOut is before disconnect (i originally had those two reversed and it did not sign the user out).
For our workflow I have it so that the logout url appears. This works because if the user wants to sign in again then they need to authenticate. If the user is signed in already it bypasses the authentication and goes directly into application.
I'm trying to setup Apple Pay on my app, but I don't have an iPhone 6 with me at the moment. So I'm trying to get everything up and running with the simulator before I can go buy one, or try to get someone to lend one to me.
Anyway, I got to the point of actually showing the ApplePay view controller:
But when I tap on "Pay with Passcode" nothing happens, so I can't go any further and finish testing everything with the server.
Here are the relevant parts of my code:
class PaymentVC: UIViewController,PKPaymentAuthorizationViewControllerDelegate {
#IBAction func onPaymentSubmit(sender: AnyObject) {
if PKPaymentAuthorizationViewController.canMakePaymentsUsingNetworks(supportedPaymentNetworks) {
let applePayMerchantID = "merchant.com.id"
let request = PKPaymentRequest()
request.merchantIdentifier = applePayMerchantID
request.supportedNetworks = supportedPaymentNetworks
request.merchantCapabilities = PKMerchantCapability.Capability3DS
request.countryCode = "US"
request.currencyCode = "USD"
request.paymentSummaryItems = [
PKPaymentSummaryItem(label: "Custom Order", amount: NSDecimalNumber(float: total))
]
let applePayController = PKPaymentAuthorizationViewController(paymentRequest: request)
applePayController.delegate = self
self.presentViewController(applePayController, animated: true, completion: nil)
}
}
//MARK: Apple Pay
func paymentAuthorizationViewController(controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, completion: (PKPaymentAuthorizationStatus) -> Void) {
}
func paymentAuthorizationViewControllerDidFinish(controller: PKPaymentAuthorizationViewController) {
controller.dismissViewControllerAnimated(true, completion: nil)
}
}
Any idea what might be wrong?
You need to return a payment status in paymentAuthorizationViewController. You'll see that delegate method has a completion handler that you must call to indicate whether you were able to process the payment successfully or not.