BUG: Access denied calling checkoutCompleteWithCreditCard from iOS App

Rob_Kerr
Shopify Partner
5 0 4

I'm developing a proof-of-concept iOS app to help my company decide whether to use Shopify as a back-end for product sales.  I've followed the iOS SDK guides, and have created a working application that does nearly everything it needs:

1. Access store info

2. Access collections and product catalog

3. Build a shopping cart, update shipping address, use the cardVaultUrl and CardCiient to obtain a token (I'm in test mode, so using a test credit card number)

On the next step I use checkoutCompleteWithCreditCard to process the order, and receive back "access denied".

I see on the uinversity forum that this issue comes up a lot (see links below), with no responses from Shopify whether this opertion shoudl work or not from a private app via iOS SDK.  

Is this feature not supported?  The documentation makes no mention of it being restricted, so it appears to be a bug.

https://ecommerce.shopify.com/c/shopify-apis-and-technology/t/checkoutcompletewithcreditcard-acess-d...

https://ecommerce.shopify.com/c/shopify-apis-and-technology/t/how-to-complete-native-checkout-using-...

https://ecommerce.shopify.com/c/shopify-apis-and-technology/t/storefront-api-checkoutcompletewithtok...

https://ecommerce.shopify.com/c/shopify-apis-and-technology/t/storefront-api-process-checkout-438429

Dima_Bart
Shopify Staff (Retired)
Shopify Staff (Retired)
74 0 8

Could you please provide a little more information on the problem to help diagnose this case? It would be helpful to see what your payload to "checkoutCompleteWithCreditCard" looks like, as well as the payload to the "vaultURL" used for vaulting the card.

Keep in mind the the vaulting service doesn't perform any validation. This happens during the checkout process instead, which might lead to falsely believing that the vaulted token is valid and can be used for payment. Seeing your payloads will help us better understand where the problem might be.

0 Likes
Rob_Kerr
Shopify Partner
5 0 4

HI Dima, thanks for your reply and assistance!

The actual payload going over the wire is difficult for me to fetch since it's being sent by the shopify API, but here's the swift code makign the api call. 

The proof-of-concept app is a series of view controllers (browse catalog, add to cart, apply shipping method, submit payment).  This is the last one--payment.  

Yes, I realize the token returned by the vault may be invalid, but if it was I'd expect an error more along the lines of "payment declined" instead of "access denied" from the checkoutCompleteWithCreditCard method (?)

I'm happy to zip up the entire project and send if it would be helpful...this is a simple proof-of-concept against  development store and test merchant account..so no confidentiality issues at all.

============ PaymentTableViewController.swift  ==============

import UIKit

import MobileBuySDK

 

class PaymentTableViewController: UITableViewController {

 

    @IBOutlet weak var firstName: UITextField!

    @IBOutlet weak var lastName: UITextField!

    @IBOutlet weak var creditCardNumber: UITextField!

    @IBOutlet weak var expiresMonth: UITextField!

    @IBOutlet weak var expiresYear: UITextField!

    @IBOutlet weak var verificationCode: UITextField!

    

 

    override func viewDidLoad() {

        super.viewDidLoad()

 

        firstName.text = "John"

        lastName.text = "Smith"

        creditCardNumber.text = "4242424242424242"

        expiresMonth.text = "12"

        expiresYear.text = "2019"

        verificationCode.text = "555"

    }

 

    // MARK: - Table view data source

 

    override func numberOfSections(in tableView: UITableView) -> Int {

        return 1

    }

 

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

        return 6

    }

    

    func fetchPaymentToken() {

        guard let cardClient = ShoppingData.shopifyCard else {

            displayError("nil card client in \(#function)")

            return

        }

        guard let cardVaultUrl = ShoppingData.cardVaultUrl else {

            displayError("nil cardVaultUrl in \(#function)")

            return

        }

        

        let creditCard = Card.CreditCard(

            firstName:        firstName.text ?? "",

            middleName:       "A",

            lastName:         lastName.text ?? "",

            number:           creditCardNumber.text ?? "",

            expiryMonth:      expiresMonth.text ?? "",

            expiryYear:       expiresYear.text ?? "",

            verificationCode: verificationCode.text ?? ""

        )

        

        let task = cardClient.vault(creditCard, to: cardVaultUrl) { token, error in

            if let token = token {

                print("Token received is: \(token)")

                self.processOrder(paymentToken: token)

            } else {

                self.displayError(error?.localizedDescription ?? "Unknown error obtaining token")

            }

        }

        task.resume()

    }

    

    @IBAction func submitOrderTapped(_ sender: UIBarButtonItem) {

        fetchPaymentToken()

    }

    

    func processOrder(paymentToken: String) {

        guard let checkoutId = ShoppingData.checkout?.id else {

            displayError("nil checkoutId in \(#function)")

            return

        }

        guard let client = ShoppingData.shopifyClient else {

            displayError("Invalid client in \(#function)")

            return

        }

        guard let billToAddress = ShoppingData.billToAddress else {

            displayError("nil billToAddress in \(#function)")

            return

        }

        guard let paymentDue = ShoppingData.checkout?.paymentDue else {

            displayError("nil paymentDue in \(#function)")

            return

        }

        let idempotencyKey = UUID().uuidString

        

        let payment = Storefront.CreditCardPaymentInput.create(

            amount:         paymentDue,

            idempotencyKey: idempotencyKey,

            billingAddress: billToAddress,

            vaultId:        paymentToken

        )

 

        let mutation = Storefront.buildMutation { $0

            .checkoutCompleteWithCreditCard(checkoutId: checkoutId, payment: payment) { $0

                .payment { $0

                    .id()

                    .ready()

                }

                .checkout { $0

                    .id()

                    .ready()

                }

                .userErrors { $0

                    .field()

                    .message()

                }

            }

        }

        

        let task = client.mutateGraphWith(mutation) { result, error in

            if let err = error {

                self.displayError(ShoppingData.shopifyErrorMessage(err: err))

                return

            }

            

            if let userError = result?.checkoutCreate?.userErrors, userError.count > 0 {

                for err in userError {

                    self.displayError(err.message)

                }

                return

            }

 

            let checkoutReady = result?.checkoutCompleteWithCreditCard?.checkout.ready ?? false

            let paymentReady  = result?.checkoutCompleteWithCreditCard?.payment?.ready ?? false

            

            print("Checkout completed: \(checkoutReady)")

            print("Payment completed: \(paymentReady)")

        }

        task.resume()

        

    }

 

 

}

============ ShoppingData.swift =================

class ShoppingData {

    static var shopifyClient: Graph.Client?                     // endpoint to shopify store

    static var shopifyCard: Card.Client?                        // endpoint to card processing

    static var itemCollections : [CatalogCollection] = []       // all items in the catalog

    static var cartItems : [CatalogItem] = []                   // all items in the shopping cart

    static var checkout: Storefront.Checkout?                   // the checkout object (from which the checkoutId is pulled from)

    static var billToAddress: Storefront.MailingAddressInput?   // save the address to re-use in the payment submission (in real life the bill-to may be different though)

    static var cardVaultUrl: URL?                               // the URL used to obtain credit card tokens

    

    static func shopifyErrorMessage(err: Graph.QueryError) -> String {

        switch err {

        case .request(let error):

            return error?.localizedDescription ?? "Error has nil local description"

        case .http(let statusCode):

            return HTTPURLResponse.localizedString(forStatusCode: statusCode)

        case .noData:

            return err.localizedDescription

        case .jsonDeserializationFailed:

            return "json deserialization failed"

        case .invalidJson:

            return "invalid json"

        case .invalidQuery(let reasons):

            return reasons[0].message

        case .schemaViolation(let schemaError):

            return "schema error: \(schemaError.localizedDescription)"

        }

    }

}

0 Likes
Dima_Bart
Shopify Staff (Retired)
Shopify Staff (Retired)
74 0 8

It's possible the gateway doesn't like your test card number, verification number, etc. Have you tried a different test card?

0 Likes
Rob_Kerr
Shopify Partner
5 0 4

Yes, I'm using the numbers pasted from your site (see below). I've tried the visa and mastercard for sure, and have the same error either way.

https://help.shopify.com/manual/payments/shopify-payments/testing-shopify-payments

0 Likes
Rob_Kerr
Shopify Partner
5 0 4

just to reiterate...if the payment info was invalid, the error should be "payment declined" not "access denied", right?

0 Likes
juno
New Member
1 0 0

See post below

0 Likes
Mira_Torres
Tourist
5 0 2

Reposting with shopify shop connected account: I'm seeing the same "access denied" error when calling checkoutCompleteWithCreditCard. Using Mobile-Buy-SDK 3.1.2 on iOS with Shopify private app storefront access token. Our test payload looks very similar to powerleydev's. Our shopify payment is also in test mode, and the credit card info is the same test credit card number(4242424242424242). Please let me know what additional info I need to provide to figure out what's wrong.

Mira_Torres
Tourist
5 0 2

Request payload for checkoutCompleteWithCreditCard:

mutation{checkoutCompleteWithCreditCard(checkoutId:"Z2lkOi8vc2hvcGlmeS9DaGVja291dC9jYTdiYTk5YTFjNTY0MmU1M2I0NTFjNjBlNWU5YzlhMj9rZXk9MjAwNDk4ZWRiYTM3NDQ2MmFjMzBiNjQyMGVmNjAxNzM=",payment:{amount:"12.15",idempotencyKey:"56D2826C-B45A-4F19-8182-EC6B06F3DC9C",billingAddress:{address1:"1 Gary",address2:"",city:"San Francisco",company:null,country:"United States",firstName:"Joe",lastName:"Black",phone:"5101231234",province:"California",zip:"94110"},vaultId:"west-8c88cce51c8695bcaea71480f140ebbc",test:true}){checkout{id,ready,requiresShipping,taxesIncluded,email,shippingAddress{firstName,lastName,phone,address1,address2,city,country,countryCode,province,provinceCode,zip},shippingLine{handle,title,price},note,lineItems(first:250){edges{cursor,node{variant{id,price},title,quantity}}},webUrl,currencyCode,subtotalPrice,totalTax,totalPrice,paymentDue,order{id,orderNumber,customerUrl,email}},userErrors{field,message}}}

Response:

{
    "data": {
        "checkoutCompleteWithCreditCard": null
    },
    "errors": [{
        "message": "CheckoutCompleteWithCreditCard access denied",
        "locations": [{
            "line": 1,
            "column": 10
        }],
        "path": ["checkoutCompleteWithCreditCard"]
    }]
}

Mira_Torres
Tourist
5 0 2

Aready treid with real credit card info and turning off payment test mode, got same error.

Is there a permission that needs to added to the private app that's not avaiable to be set in the admin UI? If so, how do I get that set or whom should I contact? Already tried customer support, but they can't resolve API related issues and directed me to this forum.

Please advice on next step.

0 Likes