Shopify / mobile-buy-sdk-ios

Shopify’s Mobile Buy SDK makes it simple to sell physical products inside your mobile app. With a few lines of code, you can connect your app with the Shopify platform and let your users buy your products using Apple Pay or their credit card.
MIT License
453 stars 198 forks source link

Unable to programmatically set an alternate shipping address for Apple Pay without losing Shipping Rate #1037

Open zackshapiro opened 5 years ago

zackshapiro commented 5 years ago

I have a flow where a user fills out an address form that we store in UserDefaults. Normal address fields, email, phone, first and last names. My goal is to pre-fill the Shipping Address on the Apple Pay prompt not with the primary shipping address attached to the user's default card, as it is currently coded into the Shopify SDK, but with my own user-generated address.

I built myself a sample app where I can do this just fine like this:

 let request = PKPaymentRequest() // this is the object fed into the PKPaymentAuthVC we eventually use
        request.supportedNetworks = [.visa, .masterCard]
        request.merchantIdentifier = "merchant.zackshapiro.apple-pay-test"
        request.merchantCapabilities = .capability3DS
        request.currencyCode = "USD"
        request.countryCode = "US"
        request.requiredShippingContactFields = [.postalAddress, .name]
        var items = [
            PKPaymentSummaryItem(label: "Food", amount: 2)
        ]

        let contact = PKContact()
        var name = PersonNameComponents()
        name.givenName = "Test"
        name.familyName = "User"
        contact.name = name

        let address = CNMutablePostalAddress()
        address.street = "123 Main Street"
        address.city = "NY"
        address.state = "NY"
        address.postalCode = "10001"
        address.country = "USA"
        address.isoCountryCode = "840"
        contact.postalAddress = address

        request.shippingContact = contact
        let dollarShipping = PKShippingMethod(label: "Dollar Shipping", amount: 1.00)
        dollarShipping.identifier = "dollarShip"
        dollarShipping.detail = "foo"

        let expensiveShipping = PKShippingMethod(label: "Expensive Shipping", amount: 10.00)
        expensiveShipping.identifier = "expensiveShip"
        expensiveShipping.detail = "bar"

        request.shippingMethods = [dollarShipping, expensiveShipping]

        items.append(PKPaymentSummaryItem(label: request.shippingMethods![0].label, amount: request.shippingMethods![0].amount))

        let total = items.reduce(0) { sum, item in
            sum + (item.amount as Decimal)
        }
        items.append(PKPaymentSummaryItem(label: "Total", amount: NSDecimalNumber(decimal: total)))
        request.paymentSummaryItems = items

        let applePayController = PKPaymentAuthorizationViewController(paymentRequest: request)!
        applePayController.delegate = self
        self.present(applePayController, animated: true)

Doing it this way, I have both the stubbed shipping address in the Apple Pay prompt as well as a shipping method.

- What is the expected behaviour?

Two expectations that both fail:

1) Before I call paySession.authorize(), when I'm constructing my PayCheckout object, when I give it a shippingAddress, I'd expect that address to show up as the Apple Pay shipping address in the modal but it doesn't. needsShipping is marked as true when I create the PayCheckout object and I've printed that address throughout the various methods that are called, the address I want to stub always shows in the logs but when the Apple Pay prompt comes up, the address tied to the primary card is always shown as the shipping.

2) The other expectation that I had was to go into func paymentRequestUsing(_ checkout: PayCheckout, currency: PayCurrency, merchantID: String) -> PKPaymentRequest and set the request.shippingContact as I did in my sample app code. Doing that shows the correct user-entered address but the shipping method goes away

Alternatively, setting the shippingAddress to my user-entered value below in func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didAuthorizePayment payment: PKPayment, completion: @escaping (PKPaymentAuthorizationStatus) -> Void) I thought might work, but also doesn't.

let authorization = PayAuthorization(
            paymentData:     payment.token.paymentData,
            billingAddress:  PayAddress(with: payment.billingContact!),
            shippingAddress: PayAddress(with: payment.shippingContact!),
            shippingRate:    shippingRate
)

I've hunted through the stack printing and stubbing and trying to get this to work. Seems like the address I provide to the PayCheckout should be honored all the way through and it's unclear why it's not. Any help would be great. Thanks

3.6.0

zackshapiro commented 5 years ago

Thinking about this more, maybe something is not calling the delegate methods that ask Shopify for shipping method options so that's why it's not showing up?

dbart01 commented 5 years ago

If you take a look at PaySession.swift, on line 184, we create the PKPaymentRequest. You're right, the shipping address is never transferred over from PaySession into the request, as you've shown in the snippet above. If injecting the shipping contact in that method helps your case, we can certainly add it to the SDK.

zackshapiro commented 5 years ago

Thanks @dbart01 . I've added it in my local, unlocked file:

    func paymentRequestUsing(_ checkout: PayCheckout, currency: PayCurrency, merchantID: String) -> PKPaymentRequest 
        let request                           = PKPaymentRequest()
        request.merchantIdentifier            = merchantID
        request.countryCode                   = currency.countryCode
        request.currencyCode                  = currency.currencyCode
        request.merchantCapabilities          = .capability3DS
        request.requiredBillingAddressFields  = .all
        request.requiredShippingAddressFields = .all

        if #available(iOS 11.0, *) { // added this in, made no difference
            request.requiredShippingContactFields = [.emailAddress, .name, .phoneNumber, .postalAddress]
        } else {
            // we don't support before 11
        }
        request.supportedNetworks             = self.acceptedCardBrands.paymentNetworks
        request.paymentSummaryItems           = checkout.summaryItems(for: self.shopName)

        let contact = PKContact()
        var name = PersonNameComponents()
        name.givenName = checkout.shippingAddress?.firstName
        name.familyName = checkout.shippingAddress?.lastName
        contact.name = name

        let address = CNMutablePostalAddress()
        address.street = checkout.shippingAddress!.addressLine1! + checkout.shippingAddress!.addressLine2!
        address.city = checkout.shippingAddress!.city!
        address.state = checkout.shippingAddress!.province!
        address.postalCode = checkout.shippingAddress!.zip!
        address.country = "USA"
        address.isoCountryCode = "840"
        contact.postalAddress = address

        request.shippingContact = contact

but then the methods to get shipping info are never called. Any idea why? I can't find a guard that's triggered or anything like that

zackshapiro commented 5 years ago

Screen Shot 2019-10-15 at 1 32 46 PM

I commented out the request.shippingContact line to look at the stack trace of where that's called from and it seems to originate inside of PassKit in PKPaymentAuthorizationController.paymentAuthorizationCoordinator

Not really sure how to debug this to get it working with the added shippingContact

zackshapiro commented 5 years ago

Hi @dbart01, wanted to bump this up. Thanks

dbart01 commented 5 years ago

What methods are you expecting to be called and when? You are correct, all the callback come directly from PassKit and pull information into the Apple Pay payment processing modal. As far as I know, there's no way to programatically invoke these methods manually.

zackshapiro commented 5 years ago

Without setting the shippingContact on the request, didSelectShippingContact is automatically called. For some reason though, setting that attribute causes it not to be called so I can't both set a user-generated address and charge the user checking out for shipping

dbart01 commented 5 years ago

I would imagine that logic is baked into PassKit. Why is didSelectShippingContact necessary to charge the user?

zackshapiro commented 5 years ago

That's what causes the other shipping/contact-related delegate methods to be called that you've built in to PaySession

zackshapiro commented 5 years ago

Since you know your pod best and I haven't been able to decipher why this is, do you know why adding the shippingContact here is causing the other PaySession delegate methods not to fire?

zackshapiro commented 5 years ago

Morning @dbart01, hope you had a good weekend. I'd really like to ship this feature this week if you have any insight into this or why it's not working with the Shopify PaySession delegate code, that'd be amazingly helpful. Thanks!

tristan-potter commented 4 years ago

:wave: Hey @zackshapiro, are you still seeing this? We haven't been able to replicate the issue and need more information. Thanks!

ksmks0921 commented 3 years ago

I could not solve this issue. I want to fill the shipping address automatically when opening apple pay page. Is it possible?

TheiOSDude commented 3 years ago

@ksmks0921 This is possible, but you'll need to fork the repository, or submit a PR. adding a shippingContact: shippingContact? property onto the PaySession and in the paymentRequestUsing method, where they create the PKPaymentRequest, set the correct property.

MihailStoicaGH commented 3 years ago

@TheiOSDude Hello, I did this but as @zackshapiro said their delegates (didRequestShippingRatesFor) don't get called anymore and this will cause an internal error.

Amalous commented 2 years ago

For those who are facing the same problem, setting the delegate variable in PaySession to a strong variable as opposed to weak (public var delegate: PaySessionDelegate? instead of public weak var delegate: PaySessionDelegate?), adding the extra fields in paymentRequestUsing(), and modifying the authorize() func did the trick. It's not a perfect solution, but it worked for me for Apple Pay. Here is the code:

Screen Shot 2022-04-25 at 2 17 43 PM Screen Shot 2022-04-25 at 2 17 53 PM