Shopify / checkout-sheet-kit-react-native

Shopify's Checkout Sheet Kit for React Native - simplifying the process of adding checkout to your native apps.
https://shopify.dev/docs/custom-storefronts/mobile-apps
MIT License
30 stars 3 forks source link

Issue with Cart Quantity Sync in Checkout Sheet Kit for React Native #108

Open vadivazhagan-vadivel opened 1 month ago

vadivazhagan-vadivel commented 1 month ago

What area is the issue related to?

Checkout Sheet Kit

What platform does the issue affect?

All platforms

What version of @shopify/checkout-sheet-kit are you using?

3.0.1

Do you have reproducible example code?

  private executeCartQuery = async (model, query, inputs: any, appConfig, appModel, _cartId?: string) => {
    try {
      let result;

      const currentCart = this.getCart(model);
      const cartId = _cartId || currentCart?.id;
      const cartPlatformId = model?.get('cartPlatform');
      const cartPlatformDSModel = appModel?.getModelValue([cartPlatformId]);
      const customerAccessToken = cartPlatformDSModel?.getIn(['loggedInUserAccessToken', 'accessToken']);

      switch (query) {
        case 'CREATE':
          result = await this.queryExecutor(
            appConfig,
            appModel,
            model,
            CartQueryRecords.CreateCart,
            Object.assign({}, {customerAccessToken}, inputs),
          );
          break;
        case 'GET_CART':
          if (cartId) {
            result = await this.queryExecutor(
              appConfig,
              appModel,
              model,
              CartQueryRecords.GetCart,
              Object.assign({}, {cartId}, inputs),
            );
          }
          break;
        case 'ADD_LINE_ITEMS':
          let lineItemsToAdd = _.get(inputs, 'lines', []).map(entry => {
            return {
              merchandiseId: entry.merchandiseId,
              sellingPlanId: entry.sellingPlanId,
              quantity: _.isNil(entry.quantity) ? 1 : entry.quantity,
              attributes: _.isEmpty(entry.attributes) ? [] : entry.attributes,
            };
          });
          result = cartId
            ? await this.queryExecutor(appConfig, appModel, model, CartQueryRecords.CartLinesAdd, {
                cartId,
                lines: lineItemsToAdd,
              })
            : await this.executeCartQuery(
                model,
                'CREATE',
                {
                  lines: lineItemsToAdd,
                },
                appConfig,
                appModel,
              );
          break;
        case 'UPDATE_LINE_ITEMS':
          const lineItemsToUpdate = _.get(inputs, 'lines', []).map(entry => {
            return {
              id: entry.id,
              merchandiseId: entry.merchandiseId,
              sellingPlanId: entry.sellingPlanId,
              quantity: entry.quantity,
              attributes: _.isEmpty(entry.attributes) ? [] : entry.attributes,
            };
          });
          result = cartId
            ? await this.queryExecutor(appConfig, appModel, model, CartQueryRecords.CartLinesUpdate, {
                cartId,
                lines: lineItemsToUpdate,
              })
            : await this.executeCartQuery(
                model,
                'CREATE',
                {
                  lines: lineItemsToUpdate,
                },
                appConfig,
                appModel,
              );
          break;
        case 'REMOVE_LINE_ITEMS':
          const linesItemsToRemove = _.get(inputs, 'lines', []).map(entry => entry?.id);
          if (cartId) {
            result = await this.queryExecutor(appConfig, appModel, model, CartQueryRecords.CartLinesRemove, {
              cartId,
              lineIds: linesItemsToRemove,
            });
          }
          break;
        case 'UPDATE_DISCOUNTS':
          if (cartId) {
            result = await this.queryExecutor(
              appConfig,
              appModel,
              model,
              CartQueryRecords.CartDiscountUpdate,
              Object.assign({}, {cartId}, inputs),
            );
          }
          break;
        case 'UPDATE_BUYER_IDENTITY':
          if (cartId) {
            result = await this.queryExecutor(
              appConfig,
              appModel,
              model,
              CartQueryRecords.CartBuyerIdentityUpdate,
              Object.assign({}, {cartId, customerAccessToken}, inputs),
            );
          }
          break;
        case 'UPDATE_ATTRIBUTES':
          if (cartId) {
            result = await this.queryExecutor(
              appConfig,
              appModel,
              model,
              CartQueryRecords.CartAttributesUpdate,
              Object.assign({}, {cartId}, inputs),
            );
          }

          break;
        case 'UPDATE_NOTE':
          if (cartId) {
            result = await this.queryExecutor(
              appConfig,
              appModel,
              model,
              CartQueryRecords.CartNoteUpdate,
              Object.assign({}, {cartId}, inputs),
            );
          }
          break;
        default:
          throw new Error(`${query} cart query is not defined`);
      }

      shopifyCheckout.preload(result.checkoutUrl) <=== preloading call
      return result;
    } catch (error) {
      console.error(`Error executing cart ${query}`, error);
    }
  };

Describe the issue

I am integrating @shopify/checkout-sheet-kit version 3.0.1 with my React Native application (version 0.73). The goal is to use the preload function as recommended in the documentation to ensure that the checkout stays synchronized with the cart whenever changes occur.

While the preload function generally works as expected, I am encountering a specific issue when rapidly adding multiple items to the cart. After using the addLineItem and updateLineItem GraphQL mutations to add items to the cart, I attempt to fetch the updated cart. However, if the items are added in quick succession, the cart sometimes returns incorrect quantities. For example, I might add 3 items to the cart, but the fetched cart reflects only 2 item. This discrepancy only occurs when items are added rapidly; a slight delay between additions seems to prevent the issue.

This issue is causing inconsistencies in the user experience, particularly when users quickly add multiple items to their cart and proceed to checkout. The problem does not occur if I disable the preload function, but I need to keep this feature enabled to maintain the desired functionality of my application.

Steps to Reproduce

  1. Rapidly add items to the cart using addLineItem and updateLineItem GraphQL mutations.
  2. Immediately fetch the updated cart.

Expected Behavior

The fetched cart should accurately reflect the correct quantities, matching the number of items added.

Actual Behavior

The cart sometimes returns incorrect quantities. For example, adding 3 items might result in only 2 item being shown in the cart.

Storefront domain

staging-pilgrim.myshopify.com

Screenshots/Videos/Log output

https://github.com/user-attachments/assets/aa79ca22-c7e3-4b39-9f50-5494cf860bf4

kiftio commented 1 month ago

@vadivazhagan-vadivel I've forwarded this on to the cart API team to take a look. I'll update when possible (N.B. there are public holidays in some office locations today)

kiftio commented 2 weeks ago

Hi @vadivazhagan-vadivel, we've identified the issue and are looking into possible solutions

In the meantime, although our sample apps tend to call preload() on each mutation of the cart, the recommendation is to only call preload when there's a strong signal the customer will enter checkout. For example if they land on the cart page. This should reduce the frequency of the issue appearing.

See the first Important Consideration here for more details

vadivazhagan-vadivel commented 2 weeks ago

Hi @kiftio, I tried this approach, and while it works when the customer first lands on the cart page, the issue comes back if they change the item quantity. The preload function only works as expected if it's called when the page loads and the customer doesn't make any changes afterward.