Post-purchase extension does not work when there is no interaction with payment methods

I have a problem with the post-purchase api, does anyone have the same problem?

I’ve created a post-purchase extension that lets you add products to the shopping cart after payment, the app is deployed and works on a live store EXCEPT when the customer has no interaction with the payment methods.
For example, if the credit card is pre-registered, or if the payment method has no fields to fill in (e.g. bank transfer), the post-purchase page does not appear, and you go straight to the thank you page.

I put a console.log to check that the “ShouldRender” returns true:

extend(
  "Checkout::PostPurchase::ShouldRender",
  async ({ inputData, storage }) => {
    const postPurchaseOffer = await fetch(`${APP_URL}/api/offer`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${inputData.token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        referenceId: inputData.initialPurchase.referenceId,
      }),
    }).then((response) => response.json());

    await storage.update(postPurchaseOffer);

    console.log(postPurchaseOffer);
    console.log("should render", postPurchaseOffer.offers.length > 0);

    return { render: postPurchaseOffer.offers.length > 0 };
  },
);

I get should render, true in the console, but the page still doesn’t appear.

The complete code:

import { useState } from "react";
import {
  extend,
  render,
  useExtensionInput,
  BlockStack,
  Tiles,
  Layout,
} from "@shopify/post-purchase-ui-extensions-react";

import type { PostPurchaseRenderApi } from "@shopify/post-purchase-ui-extensions-react";
import type { Offer, SelectedVariant } from "./types";
import ProductCard from "./components/ProductCard";
import VisualBanner from "./ui/VisualBanner";
import VisualSummary from "./ui/VisualSummary";
import VisualSelector from "./ui/VisualSelector";

const APP_URL = "https://xxxx.fly.dev";

const CREDIT_PERCENTAGE = 0.04;

extend(
  "Checkout::PostPurchase::ShouldRender",
  async ({ inputData, storage }) => {
    const postPurchaseOffer = await fetch(`${APP_URL}/api/offer`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${inputData.token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        referenceId: inputData.initialPurchase.referenceId,
      }),
    }).then((response) => response.json());

    await storage.update(postPurchaseOffer);

    console.log(postPurchaseOffer);
    console.log("should render", postPurchaseOffer.offers.length > 0);

    return { render: postPurchaseOffer.offers.length > 0 };
  },
);

render("Checkout::PostPurchase::Render", () => <App />);

export function App() {
  const { storage, inputData, applyChangeset, done } =
    useExtensionInput() as PostPurchaseRenderApi;
  const [loading, setLoading] = useState(false);
  const [selectedProductType, setSelectedProductType] = useState("all");
  const [selectedVariants, setSelectedVariants] = useState<SelectedVariant[]>(
    [],
  );

  const initialPurchaseAmount =
    inputData.initialPurchase.totalPriceSet.presentmentMoney.amount;
  const credit = parseFloat(initialPurchaseAmount) * CREDIT_PERCENTAGE;

  const currencyCode =
    inputData.initialPurchase.totalPriceSet.presentmentMoney.currencyCode;

  const totalPriceForSelectedVariants = selectedVariants.reduce(
    (acc, variant) => acc + (variant.originalPrice * variant.quantity || 0),
    0,
  );

  /* @ts-ignore */
  const offers: Offer[] = storage.initialData?.offers;
  console.log("offers", offers);

  if (offers.length === 0) {
    declineOffer();
  }

  // Extract values from the calculated purchase.
  const originalPrice = selectedVariants.reduce(
    (total, variant) => total + variant.originalPrice * variant.quantity,
    0,
  );

  const uniqueProductTypes: string[] = [
    "all",
    ...new Set(
      offers
        .map((offer) => offer.node.productType)
        .filter((type): type is string => Boolean(type)),
    ),
  ];

  async function acceptOffer() {
    setLoading(true);

    // Make a request to your app server to sign the changeset with your app's API secret key.
    const token = await fetch(`${APP_URL}/api/sign-changeset`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${inputData.token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        referenceId: inputData.initialPurchase.referenceId,
        changes: selectedVariants.map((selectedVariant) => ({
          variantId: selectedVariant.variantId,
          quantity: selectedVariant.quantity,
          type: "add_variant",
          discount: {
            value: 100,
            valueType: "percentage",
            title: "Goodies discount",
          },
        })),
      }),
    })
      .then((response) => response.json())
      .then((response) => response.token)
      .catch((e) => console.log(e));

    // Make a request to Shopify servers to apply the changeset.
    await applyChangeset(token);

    // Redirect to the thank-you page.
    done();
  }

  function declineOffer() {
    setLoading(true);
    // Redirect to the thank-you page
    done();
  }

  return (
    <BlockStack spacing="loose">
      <VisualBanner credit={credit} currencyCode={currencyCode} />
      <VisualSelector
        selectedProductType={selectedProductType}
        setSelectedProductType={setSelectedProductType}
        uniqueProductTypes={uniqueProductTypes}
      />
      <Layout maxInlineSize={1400} sizes={["fill", 38, 340]}>
        <Tiles maxPerLine={Math.max(Math.min(offers.length, 6), 4)}>
          {offers
            .filter(
              (offer) =>
                offer.node.variants.edges[0].node.availableForSale &&
                (selectedProductType === "all" ||
                  offer.node.productType === selectedProductType),
            )
            .map((offer) => (
              <ProductCard
                productVariant={offer.node.variants.edges[0].node}
                image={offer.node.images?.nodes[0]?.url ?? ""}
                title={offer.node.title}
                totalPriceForSelectedVariants={totalPriceForSelectedVariants}
                credit={credit}
                selectedVariants={selectedVariants}
                setSelectedVariants={setSelectedVariants}
                key={offer.node.variants.edges[0].node.id}
              />
            ))}
        </Tiles>
        <BlockStack />
        <VisualSummary
          selecedVariants={selectedVariants}
          originalPrice={originalPrice}
          credit={credit}
          setSelectedVariants={setSelectedVariants}
          currencyCode={currencyCode}
          acceptOffer={acceptOffer}
          declineOffer={declineOffer}
          loading={loading}
        />
      </Layout>
    </BlockStack>
  );
}

I reiterate that the app works well when the user fills in fields in the payment methods (e.g. credit card number).

Hi Symediane,

Is this behaviour happening in your test environment too, or only on live stores?

On a development store, the manual payment methods don’t work and I can’t pre-register a credit card (probably because it’s a test payment method?).
So I can’t reproduce the bug on a development store.

Okay - is there a way you could test another app that has a post-purchase extension to see if it happening with that app too. (Which would rule out that it’s caused by your app)

I’ve also tried this application: Upsell & Cross Sell — Selleasy - Shopify Upsell App | Cross Sell, Cart Upsell, Bundles, etc. | Shopify App Store

When I use a pre-registered card or a bank transfer payment method, the post-purchase page does not appear.

So it’s possible this is a documented limitation of post-purchase extensions (see the doc here):