Shopify Headless Cart Implementation Requires Users to Login Twice

FireHydrant
Tourist
4 0 6

Hello,

I am working on a headless shopify store implementation that also uses subscriptions/contracts.

I set up a private app to create carts, add line items, login etc.

Users are required to login in order to check out. 

 

Here are the graphQL calls I perform for any checkout:

 

mutation cartCreate

this creates a new cart object

 

mutation cartLinesAdd

this will add items to cart and assign a selling plan if possible

 

before checkout, the user is required to log in (if they are not logged in already)

 

mutation customerAccessTokenCreate

the mutation above logs them in and provides me with an accessToken

 

finally, I can use the accessToken above to perform the following:

 

mutation cartBuyerIdentityUpdate

using the accessToken, I am able to update the cart's buyer identity to the current user. I am also able to verify that this update has indeed taken place by querying the cart by id right after the final mutation.

 

the user is then redirected to the cart object's checkoutUrl. The checkout url leads to the unbranded/non-headless login page.

Is that behavior expected?
Has anyone been able to solve this double-login issue?

Thanks in advance!

Replies 6 (6)
weotch
Shopify Partner
23 0 6

I am encountering the same issue.  I don't really get the purpose of `cartBuyerIdentityUpdate` if not to create the checkout in the context of an authenticated customer...

kaiakolk
Shopify Partner
6 1 0

Same here.

Could anyone from Shopify confirm, if it is possible to add accessToken to the checkout url query params so that the users do not have to take the extra login step in the cart checkout page?

weotch
Shopify Partner
23 0 6

Here are some tighter steps to reproduce this issue.  This is using API version 2022-01.

 

1. Login and fetch customer access token

 

// QUERY
mutation customerAccessTokenCreate($input: CustomerAccessTokenCreateInput!) {
  customerAccessTokenCreate(input: $input) {
    customerAccessToken {
      accessToken
    }
    customerUserErrors {
      message
    }
  }
}

// VARIABLES
{
  "input": {
    "email": "me@domain.com",
    "password": "password"
  }
}

// RESPONSE
{
  "data": {
    "customerAccessTokenCreate": {
      "customerAccessToken": {
        "accessToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
      },
      "customerUserErrors": []
    }
  }
}

 

 

2. Create a cart with buyer identity via accessToken

 

// QUERY
mutation cartCreate($input: CartInput) {
  cartCreate(input: $input) {
    cart {
      checkoutUrl
      buyerIdentity {
        customer {
          id
          email
        }
      }
      estimatedCost {
        totalAmount {
          amount
        }
      }
    }
    userErrors {
      message
    }
  }
}

// VARIABLES
{
  "input": {
    "buyerIdentity": {
      "customerAccessToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    },
    "lines": [
      {
        "merchandiseId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC80MTA4OTUzNDc4Nzc2OA==",
        "quantity": 1
      }
    ]
  }
}

// RESPONSE
{
  "data": {
    "cartCreate": {
      "cart": {
        "checkoutUrl": "https:\/\/domain.com\/cart\/c\/zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
        "buyerIdentity": {
          "customer": {
            "id": "Z2lkOi8vc2hvcGlmeS9DdXN0b21lci81NzM4NDExNDI1OTc2",
            "email": "me@domain.com"
          }
        },
        "estimatedCost": {
          "totalAmount": {
            "amount": "5.0"
          }
        }
      },
      "userErrors": []
    }
  }
}

 

 

  • I only supplied my accessToken with the buyerIdentity in my variables yet, in the response, my email is returned with the buyerIdentity.  This tells me that the accessToken was accepted as valid.
  • Shopify has correctly calculated the estimatedCost.  This tells me the variant id that I passed in the variables in lines was accepted.

 

3. Navigate to the checkoutUrl

In other words, go to https://domain.com/cart/c/zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz in the browser.  At this point I would expect to be taken to the checkout flow with my customer state applied.  Instead, my customer state has been lost and I am asked to login (see https://yo.bkwld.com/DOum4oNW).

weotch
Shopify Partner
23 0 6

Alright, I have a functional workaround for this.  I arrived at this solution after finding this Shopify response where they said:

 

So when a customer is associated with a checkout, we require them to re-login to validate that they have access.

That post was from 2019 and was related to the Checkout Storefront API and not the Cart Storefront API, though.  And it's contradicted by this page of the docs where it's said:

 

You can associate a customer with a checkout so that the customer doesn't have to enter customer information when checking out. You can associate the customer to Shopify's web checkoutor to a checkout that will be completed through the API.

And then they go on to demo associating a customer with the checkoutCustomerAssociateV2 mutation.  So either the docs are wrong or there is a bug.

 

Anyhow, this page also talks about using Multipass to pull this off and that's what I ended up getting working.  Rather than redirecting a user on checkout directly to the checkoutUrl from the cart, I am sending the checkoutUrl as well as the customer's email to a server side script.  In my case, I'm using a Netlify function.  In other words, I'm doing this:

 

POST https://domain.com/.netify/functions/checkout?checkoutUrl=https://me.myshopify.com/cart/c/123&email=me@domain.com

The Netlify function (aka an AWS lambda) looks like this:

 

 

// Deps
const Multipassify = require('multipassify')

// Make a mulitpass url that returns the user to the checkout URL with their
// customer data intact
exports.handler = async function(req) {

	// Get the checkout url from query params
	const { checkoutUrl, email } = req.queryStringParameters

	// Disable remote_ip when running Netlify Dev
	const remote_ip = req.headers['client-ip'] == '::1' ?
		undefined : req.headers['client-ip']

	// Assemble multipass payload
	const customerData = { email, remote_ip, return_to: checkoutUrl, }

	// Create a multipass URL
	const multipassify = new Multipassify(process.env.SHOPIFY_MULTIPASS_SECRET),
		storeDomain = process.env.SHOPIFY_URL.replace(/^https:\/\//, ''),
		redirectUrl = multipassify.generateUrl(customerData, storeDomain)

	// Redirect to the multipass-bound checkout URL
	return {
		statusCode: 302,
		headers: {
			'Location': redirectUrl,
			'Cache-Control': 'no-cache, no-store, must-revalidate',
		}
	}
}

 

 

Thus, I'm building a Multipass URL that redirects the user on to the checkoutUrl while binding their customer session to the URL.  This is working for me.

FireHydrant
Tourist
4 0 6

Hiya, @weotch thanks for posting your workaround... I was hoping that we'd come up with a solution that does not require shopify plus?

In any case, the documentation is misleading or there is abug as you mentioned in your post so it would be great if we could get a responsible answer or something fixed.

vanska
Shopify Partner
2 0 0