Skip to content

Instantly share code, notes, and snippets.

@blanklob
Created August 24, 2023 14:11
Show Gist options
  • Save blanklob/bb85496da960742d7ff74ea72ada7bf0 to your computer and use it in GitHub Desktop.
Save blanklob/bb85496da960742d7ff74ea72ada7bf0 to your computer and use it in GitHub Desktop.
Example checkout UI extension with external API call
import React, { useEffect, useState } from "react";
import {
reactExtension,
Divider,
Image,
Banner,
Heading,
Button,
InlineLayout,
BlockStack,
Text,
SkeletonText,
SkeletonImage,
useCartLines,
useApplyCartLinesChange,
useApi,
} from "@shopify/ui-extensions-react/checkout";
// [START product-offer-pre-purchase.ext-index]
// Set up the entry point for the extension
export default reactExtension("purchase.checkout.block.render", () => <App />);
// [END product-offer-pre-purchase.ext-index]
function App() {
const { query, i18n } = useApi();
// [START product-offer-pre-purchase.add-to-cart]
const applyCartLinesChange = useApplyCartLinesChange();
// [END product-offer-pre-purchase.add-to-cart]
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const [adding, setAdding] = useState(false);
const [showError, setShowError] = useState(false);
// [START product-offer-pre-purchase.retrieve-cart-data]
const lines = useCartLines();
// [END product-offer-pre-purchase.retrieve-cart-data]
// State to store the data from the external API
const [externalData, setExternalData] = useState(null);
useEffect(() => {
fetchExternalData();
}, []);
useEffect(() => {
fetchProducts();
}, []);
useEffect(() => {
if (showError) {
const timer = setTimeout(() => setShowError(false), 3000);
return () => clearTimeout(timer);
}
}, [showError]);
// [START product-offer-pre-purchase.add-to-cart]
async function handleAddToCart(variantId) {
setAdding(true);
const result = await applyCartLinesChange({
type: 'addCartLine',
merchandiseId: variantId,
quantity: 1,
});
setAdding(false);
if (result.type === 'error') {
setShowError(true);
console.error(result.message);
}
}
// [END product-offer-pre-purchase.add-to-cart]
// Function to make an external API call
async function fetchExternalData() {
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
setExternalData(data); // Store the data in state
} catch (error) {
console.error("Error fetching external data:", error);
}
}
// [START product-offer-pre-purchase.retrieve-products]
async function fetchProducts() {
setLoading(true);
try {
const { data } = await query(
`query ($first: Int!) {
products(first: $first) {
nodes {
id
title
images(first:1){
nodes {
url
}
}
variants(first: 1) {
nodes {
id
price {
amount
}
}
}
}
}
}`,
{
variables: { first: 5 },
}
);
setProducts(data.products.nodes);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}
// [END product-offer-pre-purchase.retrieve-products]
if (loading) {
return <LoadingSkeleton />;
}
if (!loading && products.length === 0) {
return null;
}
const productsOnOffer = getProductsOnOffer(lines, products);
if (!productsOnOffer.length) {
return null;
}
return (
<ProductOffer
product={productsOnOffer[0]}
i18n={i18n}
adding={adding}
handleAddToCart={handleAddToCart}
showError={showError}
/>
);
}
// [START product-offer-pre-purchase.loading-state]
function LoadingSkeleton() {
return (
<BlockStack spacing='loose'>
<Divider />
<Heading level={2}>You might also like</Heading>
<BlockStack spacing='loose'>
<InlineLayout
spacing='base'
columns={[64, 'fill', 'auto']}
blockAlignment='center'
>
<SkeletonImage aspectRatio={1} />
<BlockStack spacing='none'>
<SkeletonText inlineSize='large' />
<SkeletonText inlineSize='small' />
</BlockStack>
<Button kind='secondary' disabled={true}>
Add
</Button>
</InlineLayout>
</BlockStack>
</BlockStack>
);
}
// [END product-offer-pre-purchase.loading-state]
// [START product-offer-pre-purchase.filter-products]
function getProductsOnOffer(lines, products) {
const cartLineProductVariantIds = lines.map((item) => item.merchandise.id);
return products.filter((product) => {
const isProductVariantInCart = product.variants.nodes.some(({ id }) =>
cartLineProductVariantIds.includes(id)
);
return !isProductVariantInCart;
});
}
// [END product-offer-pre-purchase.filter-products]
// [START product-offer-pre-purchase.offer-ui]
function ProductOffer({ product, i18n, adding, handleAddToCart, showError }) {
const { images, title, variants } = product;
const renderPrice = i18n.formatCurrency(variants.nodes[0].price.amount);
const imageUrl =
images.nodes[0]?.url ??
'https://cdn.shopify.com/s/files/1/0533/2089/files/placeholder-images-image_medium.png?format=webp&v=1530129081';
return (
<BlockStack spacing='loose'>
<Divider />
<Heading level={2}>You might also like</Heading>
<BlockStack spacing='loose'>
<InlineLayout
spacing='base'
columns={[64, 'fill', 'auto']}
blockAlignment='center'
>
<Image
border='base'
borderWidth='base'
borderRadius='loose'
source={imageUrl}
description={title}
aspectRatio={1}
/>
<BlockStack spacing='none'>
<Text size='medium' emphasis='strong'>
{title}
</Text>
<Text appearance='subdued'>{renderPrice}</Text>
</BlockStack>
<Button
kind='secondary'
loading={adding}
accessibilityLabel={`Add ${title} to cart`}
onPress={() => handleAddToCart(variants.nodes[0].id)}
>
Add
</Button>
</InlineLayout>
</BlockStack>
{showError && <ErrorBanner />}
</BlockStack>
);
}
// [END product-offer-pre-purchase.offer-ui]
// [START product-offer-pre-purchase.error-ui]
function ErrorBanner() {
return (
<Banner status='critical'>
There was an issue adding this product. Please try again.
</Banner>
);
}
// [END product-offer-pre-purchase.error-ui]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment