Created
August 24, 2023 14:11
-
-
Save blanklob/bb85496da960742d7ff74ea72ada7bf0 to your computer and use it in GitHub Desktop.
Example checkout UI extension with external API call
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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