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).