Skip to main content

結帳擴充功能

Shopify 的結帳是網路上轉換率最高的結帳系統,且完全可擴展。結帳擴充功能讓您新增自訂 UI、修改付款和運送選項、驗證購物車內容,以及建立購後體驗——所有這些都無需觸及 Shopify 的核心結帳程式碼。這些擴充功能在安全的沙箱化環境中運行,確保結帳效能和安全性永遠不會受到影響。

結帳可擴展性

Shopify 已從 checkout.liquid 自訂轉向基於元件的擴充功能模式。所有新的結帳自訂都應使用結帳擴充功能。checkout.liquid 方法已對新商店棄用,最終也將對現有的 Shopify Plus 商店移除。

結帳 UI 擴充功能

結帳 UI 擴充功能在結帳流程的特定位置渲染自訂元件。它們是最常見的結帳擴充功能類型。

擴充功能目標

結帳 UI 擴充功能可在以下位置渲染:

目標位置常見使用案例
purchase.checkout.block.render結帳主體自訂欄位、追加銷售、忠誠度資訊
purchase.checkout.header.render-after結帳標頭下方橫幅、信任徽章
purchase.checkout.footer.render-before結帳頁尾上方法律聲明、保證
purchase.checkout.delivery-address.render-before運送地址前禮品訊息、送貨說明
purchase.checkout.shipping-option-list.render-after運送選項後運送保護、送貨備註
purchase.checkout.payment-method-list.render-before付款方式前忠誠點數兌換
purchase.thank-you.block.render感謝頁面購後調查、推薦
purchase.order-status.block.render訂單狀態頁面追蹤、支援、評價

建構結帳 UI 擴充功能

# 產生結帳 UI 擴充功能
shopify app generate extension --template checkout_ui --name loyalty-points-checkout
// extensions/loyalty-points-checkout/src/Checkout.tsx
import {
reactExtension,
Banner,
BlockStack,
Button,
Checkbox,
Divider,
Heading,
InlineLayout,
Text,
useApi,
useApplyDiscountCodeChange,
useCartLines,
useTotalAmount,
} from '@shopify/ui-extensions-react/checkout';
import { useEffect, useState } from 'react';

const checkoutBlock = reactExtension(
'purchase.checkout.block.render',
() => <LoyaltyPointsRedemption />
);
export default checkoutBlock;

function LoyaltyPointsRedemption() {
const { buyerIdentity } = useApi();
const cartLines = useCartLines();
const totalAmount = useTotalAmount();
const applyDiscountCode = useApplyDiscountCodeChange();

const [loyaltyData, setLoyaltyData] = useState<{
pointsBalance: number;
maxRedeemable: number;
dollarValue: number;
} | null>(null);
const [redeemPoints, setRedeemPoints] = useState(false);
const [applied, setApplied] = useState(false);

const customerEmail = buyerIdentity?.email?.current;

useEffect(() => {
async function fetchLoyalty() {
if (!customerEmail) return;

try {
const response = await fetch(
`/api/loyalty/checkout-balance?email=${encodeURIComponent(customerEmail)}&total=${totalAmount.amount}`
);
const data = await response.json();
setLoyaltyData(data);
} catch (err) {
console.error('Failed to fetch loyalty data');
}
}

fetchLoyalty();
}, [customerEmail, totalAmount.amount]);

async function handleRedeem() {
if (!loyaltyData) return;

try {
const result = await applyDiscountCode({
type: 'addDiscountCode',
code: `LOYALTY-${customerEmail}-${loyaltyData.maxRedeemable}`,
});

if (result.type === 'success') {
setApplied(true);
}
} catch (err) {
console.error('Failed to apply loyalty discount');
}
}

if (!loyaltyData || loyaltyData.pointsBalance === 0) {
return null;
}

return (
<BlockStack spacing="base">
<Heading level={2}>忠誠點數</Heading>
<Divider />

<InlineLayout columns={['fill', 'auto']}>
<Text>可用點數</Text>
<Text emphasis="bold">
{loyaltyData.pointsBalance.toLocaleString()}
</Text>
</InlineLayout>

<InlineLayout columns={['fill', 'auto']}>
<Text>可兌換金額</Text>
<Text emphasis="bold">
${loyaltyData.dollarValue.toFixed(2)}
</Text>
</InlineLayout>

{!applied ? (
<BlockStack spacing="tight">
<Checkbox
checked={redeemPoints}
onChange={setRedeemPoints}
>
使用 {loyaltyData.maxRedeemable.toLocaleString()} 點數
(-${loyaltyData.dollarValue.toFixed(2)})
</Checkbox>

{redeemPoints && (
<Button kind="secondary" onPress={handleRedeem}>
兌換點數
</Button>
)}
</BlockStack>
) : (
<Banner status="success">
已使用 {loyaltyData.maxRedeemable.toLocaleString()} 點數!
您節省了 ${loyaltyData.dollarValue.toFixed(2)}
</Banner>
)}
</BlockStack>
);
}
結帳 UI 效能

結帳擴充功能必須在 500ms 內載入。保持打包體積小、在渲染期間最小化 API 呼叫,並使用 Shopify 的內建 hooks(useCartLinesuseTotalAmountuseBuyerIdentity),而非為 Shopify 已提供的資料進行單獨的 API 請求。

購後擴充功能

購後擴充功能在顧客完成付款後但在感謝頁面之前立即渲染。這是追加銷售和交叉銷售的高轉換時刻。

// extensions/post-purchase-upsell/src/PostPurchase.tsx
import {
reactExtension,
useApi,
BlockStack,
Button,
Heading,
Image,
InlineLayout,
Text,
TextBlock,
} from '@shopify/ui-extensions-react/post-purchase';
import { useState } from 'react';

const postPurchase = reactExtension(
'purchase.post.render',
() => <PostPurchaseUpsell />
);
export default postPurchase;

function PostPurchaseUpsell() {
const { inputData, calculateChangeset, applyChangeset, done } = useApi();
const [loading, setLoading] = useState(false);
const [accepted, setAccepted] = useState(false);

const offer = inputData.initialPurchase
? getRecommendedOffer(inputData.initialPurchase)
: null;

if (!offer) {
done();
return null;
}

async function handleAccept() {
setLoading(true);

const changeset = await calculateChangeset({
changes: [
{
type: 'add_variant',
variantId: offer.variantId,
quantity: 1,
discount: {
value: offer.discountPercentage,
valueType: 'percentage',
title: 'Post-purchase exclusive',
},
},
],
});

await applyChangeset(changeset.token);
setAccepted(true);
setLoading(false);
}

function handleDecline() {
done();
}

if (accepted) {
return (
<BlockStack spacing="loose">
<Heading>已加入您的訂單!</Heading>
<Text>
{offer.title} 已加入。您的信用卡已額外收取
${offer.discountedPrice.toFixed(2)}
</Text>
<Button onPress={() => done()}>繼續</Button>
</BlockStack>
);
}

return (
<BlockStack spacing="loose">
<Heading>等等——專屬優惠就給您!</Heading>

<InlineLayout columns={['auto', 'fill']} spacing="base">
<Image source={offer.imageUrl} alt={offer.title} />
<BlockStack spacing="tight">
<Text emphasis="bold" size="large">{offer.title}</Text>
<TextBlock>{offer.description}</TextBlock>
<InlineLayout columns={['auto', 'auto']} spacing="tight">
<Text appearance="subdued" accessibilityRole="deletion">
${offer.originalPrice.toFixed(2)}
</Text>
<Text emphasis="bold">
${offer.discountedPrice.toFixed(2)}
</Text>
</InlineLayout>
<Text size="small" appearance="subdued">
{offer.discountPercentage}% 折扣——離開此頁面後優惠即失效
</Text>
</BlockStack>
</InlineLayout>

<InlineLayout columns={['fill', 'fill']} spacing="base">
<Button kind="primary" onPress={handleAccept} loading={loading}>
加入訂單
</Button>
<Button kind="plain" onPress={handleDecline}>
不用了,謝謝
</Button>
</InlineLayout>
</BlockStack>
);
}

function getRecommendedOffer(initialPurchase: any) {
return {
variantId: 'gid://shopify/ProductVariant/99999',
title: 'Premium Shoe Care Kit',
description: 'Keep your new shoes looking fresh with our complete care kit.',
imageUrl: 'https://example.com/shoe-care.jpg',
originalPrice: 29.99,
discountedPrice: 19.99,
discountPercentage: 33,
};
}
購後計費

購後追加銷售會立即向顧客的付款方式收費,無需他們重新輸入信用卡資訊。這很強大,但必須負責任地使用。始終清楚地告知額外費用,並讓拒絕選項顯眼。濫用購後擴充功能將導致應用程式從 Shopify App Store 被移除。

付款自訂

付款自訂擴充功能讓您修改哪些付款方式可用以及它們出現的順序,基於購物車內容、顧客身份或其他條件。

// extensions/payment-customization/src/run.ts
import type { RunInput, FunctionRunResult } from '../generated/api';

export function run(input: RunInput): FunctionRunResult {
const cart = input.cart;
const buyerCountry = input.buyerIdentity?.countryCode;

const operations: FunctionRunResult['operations'] = [];

// 對國際訂單隱藏貨到付款
if (buyerCountry && buyerCountry !== 'US') {
const codMethod = input.paymentMethods.find(
(method) => method.name.includes('Cash on Delivery')
);
if (codMethod) {
operations.push({
hide: {
paymentMethodId: codMethod.id,
},
});
}
}

// 超過 $200 的購物車將 PayPal 移到頂部
const cartTotal = parseFloat(cart.cost.totalAmount.amount);
if (cartTotal > 200) {
const paypal = input.paymentMethods.find(
(method) => method.name.includes('PayPal')
);
if (paypal) {
operations.push({
move: {
paymentMethodId: paypal.id,
index: 0,
},
});
}
}

return { operations };
}

運送自訂

運送自訂擴充功能修改可用的運費——根據條件重新命名、重新排序或隱藏運費。

// extensions/shipping-customization/src/run.ts
import type { RunInput, FunctionRunResult } from '../generated/api';

export function run(input: RunInput): FunctionRunResult {
const cart = input.cart;
const operations: FunctionRunResult['operations'] = [];

const cartTotal = parseFloat(cart.cost.totalAmount.amount);
const hasFragileItem = cart.lines.some((line) =>
line.merchandise.__typename === 'ProductVariant' &&
line.merchandise.product.hasAnyTag &&
line.merchandise.product.tags?.includes('fragile')
);

// 重新命名標準運送以顯示預估日期
const standardRate = input.shippingRates.find(
(rate) => rate.title === 'Standard Shipping'
);
if (standardRate) {
operations.push({
rename: {
shippingRateId: standardRate.id,
title: '標準運送(5-7 個工作日)',
},
});
}

// 對易碎品隱藏經濟運送
if (hasFragileItem) {
const economyRate = input.shippingRates.find(
(rate) => rate.title.includes('Economy')
);
if (economyRate) {
operations.push({
hide: {
shippingRateId: economyRate.id,
},
});
}
}

// 超過 $100 的訂單免運費
if (cartTotal >= 100) {
const standardShipping = input.shippingRates.find(
(rate) => rate.title.includes('Standard')
);
if (standardShipping) {
operations.push({
rename: {
shippingRateId: standardShipping.id,
title: '免費標準運送(5-7 個工作日)',
},
});
}
}

return { operations };
}

購物車和結帳驗證

驗證擴充功能在結帳時強制執行業務規則,防止無效訂單被下單。

// extensions/cart-validation/src/run.ts
import type { RunInput, FunctionRunResult } from '../generated/api';

export function run(input: RunInput): FunctionRunResult {
const errors: FunctionRunResult['errors'] = [];
const cart = input.cart;

// 規則 1:任何單一產品最多 5 件
for (const line of cart.lines) {
if (line.quantity > 5) {
const title = line.merchandise.__typename === 'ProductVariant'
? line.merchandise.product.title
: 'this item';
errors.push({
localizedMessage: `每筆訂單最多可購買 5 件 ${title}。請減少數量。`,
target: 'cart',
});
}
}

// 規則 2:某些產品不能寄送到郵政信箱
const shippingAddress = input.buyerIdentity?.shippingAddress;
const hasRestrictedItem = cart.lines.some(
(line) =>
line.merchandise.__typename === 'ProductVariant' &&
line.merchandise.product.tags?.includes('no-po-box')
);

if (
hasRestrictedItem &&
shippingAddress?.address1 &&
/p\.?o\.?\s*box/i.test(shippingAddress.address1)
) {
errors.push({
localizedMessage:
'您的購物車中有一個或多個商品無法寄送到郵政信箱。請使用街道地址。',
target: 'cart',
});
}

// 規則 3:批發標籤顧客的最低訂單金額
const isWholesale = input.buyerIdentity?.customer?.hasAnyTag &&
input.buyerIdentity.customer.tags?.includes('wholesale');
const cartTotal = parseFloat(cart.cost.totalAmount.amount);

if (isWholesale && cartTotal < 250) {
errors.push({
localizedMessage: `批發帳戶的最低訂單金額為 $250.00。您目前的總計為 $${cartTotal.toFixed(2)}`,
target: 'cart',
});
}

return { errors };
}
驗證 Function 限制

購物車驗證 functions 在每次結帳更新時執行——當新增商品、地址變更或套用折扣碼時。它們必須在 5 秒 內完成,且無法進行外部網路請求。所有資料必須來自 function 輸入。如果您需要外部資料,請使用中繼欄位提前儲存。

部署和測試

# 在本機測試結帳擴充功能
shopify app dev

# CLI 提供結帳的預覽 URL:
# https://your-dev-store.myshopify.com/?preview_checkout_extensions=true

# 部署到生產環境
shopify app deploy

測試檢查清單

  • 使用不同的購物車組合測試(單一商品、多件商品、高價值、低價值)
  • 使用不同的顧客狀態測試(已登入、訪客、回訪顧客)
  • 在行動裝置上測試(結帳大量在行動裝置上使用)
  • 測試錯誤狀態(網路失敗、無效資料)
  • 使用已套用的折扣碼和禮品卡測試
  • 驗證如果您的擴充功能載入失敗,結帳仍然可以完成
下一步

繼續閱讀 佈景主題應用程式擴充功能 以了解如何使用 Online Store 2.0 應用程式區塊將您的應用程式功能新增到商家店面。