📝 Shopify Checkout UI extensions
ShopifyのCheckout UI extensionsを使った開発が必要になっているのでそのスクラップです。
📝
-
Checkout UI extensions
- 公式ドキュメント
-
【2024年8月13日まで】ShopifyパートナーなしでCheckout Extensibilityに移行する手順
- 流れなど参考になる。ただ、もしかしたら最新の状況だとメンテに切り換えず切り換えする方法も考えられるかも。公開の見通したったら調べよう
まずはアプリつくってprivateな実ストアにインストールして表示するところまでやってみた。まだ初見者なのでいろいろと勘違いがあるかもしれない。
はじめに
パートナーアカウントと開発ストアが必要なんだけど まずは Checkout UI extensions の Scaffolding an extension
のコマンドから実行してくれる感じで大丈夫。上記コマンドのセットアップ途中で terminal上に partner ログインしてね、とか開発ストアのURLは?とか訪ねられるのでその通りやっていけばよい。
※ ここでいう開発ストアは、パートナーダッシュボードでつくれる開発ストアのこと。
-
Checkout UI extensions
のみ使いたいとしてもアプリをセットアップ流れ。アプリの一機能としてそれが有る。 - ひとつのアプリ内で複数の extensions を管理できるので、大きく機能が変わらないならアプリは一つでOKそう
-
npm run shopify app dev
したあとどうしたら..?ってなったが、terminal にでてる preview URL にアクセスして、 Preview link おしたらその extension が追加された状態で確認できる
追記
- Checkout UI extensionsで実装してみた~利用規約等のチェックボックスを設置 | REWIRED の流れがとってもわかりやすかった
開発と本番
これちょっとどうしようか迷っているのだけど、本番と開発の管理どういうのがいいんだろう。
extension の内容の修正の確認。 npm run deploy
でいきなり本番切り換えず、一度ステージング環境なりでクライアントにも内容を確認してほしい場合など。
shopify.app.[env].toml
をつかって切り換え
案: 本番・開発アプリを用意して、※ これから試す案です
App configuration が基本のディレクトリ構成で、shopify.app.toml
ファイルにアプリ名やclient_id があるのだけど、 App structure によるとこの ファイルは複数もてて起動時に指定できるようだ。
- ストアには開発用と本番用のアプリを用意しておく
- ステージングストアには開発用アプリ、本番ストアには本番用アプリをインストール
-
shopify.app.toml
のデフォルトは開発アプリ設定で、本番アプリ用のshopify.app.prod.toml
を用意しておく
で、 node の env 見たいに切り換えできたるするんだろうか。できるならそれがよさそうだけど、
Shopify App Storeへ登録済みアプリのパートナーダッシュボード設定を保護したい - Shopify Community
本番アプリに対応する開発アプリを1つずつ作り、開発ストアもそれぞれ作ることで、そもそも --reset をする必要をほぼなくすことが可能か
とあったのでそんなに簡単にはいかないのかもしれない。
それにしても depoly コマンドで簡単に配布できるのはありがたい反面、事故がこわいないぁ。
開発準備
今回 checkout.liquid をカスタマイズしているものを Checkout UI extensions に置き換えることが達成したい目標で、たとえば今こんな感じのカスタマイズをしている
- 商品の登録内容(メタフィールド)や所属するコレクションに応じた処理
- ログイン済の最近利用した住所を直近の5件じゃなくて登録済住所を全件候補として表示
- お届け先住所の内容制限(文字数と利用可能な文字の制限)
- 商品重量・価格による購入制限
- お届け先住所や購入商品に応じたお届け候補日の入力の提供
Line Item Property や Cart Attributes を利用しているので、まずは開発ストアでチェックアウトにそのあたりを引き渡せるようにする。
main-product.liquid
) でかご追加時に Property を追加
商品詳細 ( <dl>
<dt><label for="">lineItemProperty1</label></dt>
<dd><input type="text" name="properties[lineItemProperty1]" value="`lineItemProperty1`" form="{{ product_form_id }}"></dd>
</dl>
<dl>
<dt><label for="">_lineItemProperty2</label></dt>
<dd><input type="text" name="properties[_lineItemProperty2]" value="`_lineItemProperty2`" form="{{ product_form_id }}"></dd>
</dl>
カート( main-cart-items.liquid) に Cart Attributes を追加
<dl>
<dt>cartAttribute1</dt>
<dd><input type="text" name="attributes[cartAttribute1]" value="`cartAttribute1`" form="cart"></dd>
</dl>
<dl>
<dt>_cartAttribute2</dt>
<dd><input type="text" name="attributes[_cartAttribute2]" value="`_cartAttribute2`" form="cart"></dd>
</dl>
簡易的だけどこれで properties と attributes わたせたのでそれを取得表示と、更新をやってみる。
- カート内の商品を取得してみる
- その商品を商品情報を StoreFront API から取得してみる
- Cart Attributes を取得・更新してみる
import { useState, useEffect } from "react";
import {
reactExtension,
useApi,
useApplyAttributeChange,
useAttributeValues,
useCartLines,
} from "@shopify/ui-extensions-react/checkout";
import {
Heading,
BlockSpacer,
BlockStack,
Checkbox,
TextBlock,
Text,
View,
Divider,
} from "@shopify/ui-extensions-react/checkout";
export default reactExtension("purchase.checkout.block.render", () => <App />);
function App() {
const { query } = useApi();
const [products, setProducts] = useState([]);
const cartLine = useCartLines();
const cartLineString = JSON.stringify(cartLine);
const cartLinesIds = cartLine.map((item) => item.merchandise.product.id);
async function fetchProducts(productIds) {
const results = [];
const uniqueIds = Array.from(new Set(productIds));
for (const id of uniqueIds) {
results.push(fetchProduct(id))
}
const productsData = await Promise.all(results);
setProducts(productsData);
}
async function fetchProduct(id) {
try {
const { data } = await query(
`query {
product(id: "${id}") {
id
title
handle
tags
}
}`,
);
return data;
} catch (error) {
console.error(error);
}
return null;
}
useEffect(() => {
if (cartLinesIds && products.length === 0) {
fetchProducts(cartLinesIds);
}
}, [cartLinesIds, products]);
const [cartAttribute1, _cartAttribute2] =
useAttributeValues([
'cartAttribute1',
'_cartAttribute2',
]);
const applyAttributeChange = useApplyAttributeChange();
const handleChange = async () => {
setChecked(!checked);
await applyAttributeChange({
type: "updateAttribute",
key: "cartAttribute1",
value: "update cartAttribute1",
});
};
// Render the extension components
return (
<BlockStack>
<Heading>Debug</Heading>
<View padding="base" border="base">
<Text size="base">CartLine</Text>
<TextBlock size="small">
{cartLineString}
</TextBlock>
<BlockSpacer />
<Text size="base">Products</Text>
<TextBlock size="small">
{JSON.stringify(products)}
</TextBlock>
</View>
<View padding="base" border="base">
<Text size="base">cartAttribute</Text>
<TextBlock size="small">cartAttribute1 : {cartAttribute1}</TextBlock>
<TextBlock size="small">_cartAttribute2: {_cartAttribute2}</TextBlock>
</View>
<Divider />
<Checkbox checked={checked} onChange={handleChange}>
Update
</Checkbox>
</BlockStack>
);
}
UIとか取得・更新の方法など提供されていてありがたい..
📝 Target
どこにその Extensionを出すか?の指定。
Checkout-ui-extensions targets にだいたいまとまってる。
ちなみに shopify.extensioin.toml
に
[[extensions.targeting]]
module = "./src/Debug.tsx"
target = "purchase.checkout.block.render"
[[extensions.targeting]]
module = "./src/Header.tsx"
target = "purchase.checkout.header.render-after"
みたいにかいておけば、ひとつの Extension で複数のブロックの管理ができる。
🤔 StoreFront API でチェックアウト時の配送先の住所指定をしたい
Migrate your app だと、checkoutShippingAddressUpdateV2 はもう非推奨なので cartBuyerIdentityUpdate つかってね、ということで deliveryAddressPreferences に住所わたして、Cart APIのレスポンスの住所かわっているけど、実際チェックアウトに進んでも住所指定されてなくないか。
以下のように アドレス帳の customerAddressId 渡してAPI上のレスポンスの deliveryAddressPreferences は更新されてるっぽいけどなぁ。 これは Shipping Address とはまた別なんだろうか。
mutation {
cartBuyerIdentityUpdate(
cartId: "gid://shopify/Cart/${cartToken}"
buyerIdentity: {
email: "${customerEmail}"
countryCode: JP
customerAccessToken: "${customerAccessToken}"
deliveryAddressPreferences: [
{
customerAddressId: "${customerAddress.id}"
}
]
}
) {
cart {
id
checkoutUrl
buyerIdentity {
email
phone
countryCode
customer {
id
}
deliveryAddressPreferences {
... on MailingAddress {
id
formatted
firstName
lastName
}
}
}
}
}
}
追記
いやそんなことなかった。これで配送先住所の指定できてた。
あとはチェックアウト画面でアドレス帳が引ければいいのだけど、今の Checkout UI extensions だとチェックアウト画面で CustomerAccessToken とれないよね..? AdminAPIと連携してひっぱることもできそうだけど、ちょっとセキュリティ的にアレな感じもするので、カスタムストアフロント側でチェックアウトに進む前にログイン済ならアドレス帳の候補だして選択してもらう -> cartBuyerIdentityUpdate でチェックアウトで指定住所選択済にする、という感じかなぁ。
📝 Static extension targets と Block extension targets
https://zenn.dev/link/comments/2d1c64dfa61305 の続きなんだけど、
- 1つのExtension の中で 複数の
purchase.checkout.block.render
は定義できない。内容が異なる複数のBlock をおきたいなら extension の定義わけないといけない - block の場合に、それが配置された target (
INFORMATION1
など ) がとれるといいのだけど、どうもそれは提供されてないっぽい..?
Blockごとにつくるの面倒なので、Static のみの Extension と Blockのみの Extension の定義にわけて Block 側の settings で target 指定して、その値で処理わけるしかないのかなぁ。
[extensions.settings]
[[extensions.settings.fields]]
key = "target"
type = "single_line_text_field"
name = "Block のターゲット (behavior)"
description = ""
[[extensions.settings.fields.validations]]
name = "choices"
value = "[\"INFORMATION1\", \"INFORMATION2\", \"INFORMATION3\"]"
import { reactExtension, useSettings } from '@shopify/ui-extensions-react/checkout';
import { BlockStack } from '@shopify/ui-extensions-react/checkout';
import INFORMATION1 from './information/INFORMATION1';
import INFORMATION2 from './information/INFORMATION2';
const checkoutBlock = reactExtension('purchase.checkout.block.render', () => <App />);
export { checkoutBlock };
function App() {
const { target } = useSettings();
if (target === 'INFORMATION1') {
return <INFORMATION1 />;
}
if (target === 'INFORMATION2') {
return <INFORMATION2 />;
}
return <BlockStack>{target}</BlockStack>;
}
📝 画面にでない Line Item Properties
You can add an underscore to the beginning of a property name to hide it from customers at checkout. For example, properties[_hiddenPropertyName].
https://shopify.dev/docs/api/liquid/objects/line_item#line_item-properties
これあるおかげで、チェックアウト画面等には表示したくないけどデータとして設定したい商品のプロパティが設定できる。具体的には商品ページで
<input type="text" name="properties[_.ItemId]" value="{{ "now" | date: "%Y%m%d%H%M%S" }}" form="{{ product_form_id }}">
のように _
始まりで設定したプロパティは、チェックアウト画面等の商品情報には表示されないけど、APIでCart内商品引いたりするとちゃんと設定されています。
🤔 Checkout UI Extension内で Storefront API の mutation 使える?
まとめて複数のCart Attribute更新したかったんだけど、まだ?できないぽい。 useApplyAttributeChange
って1つの値しか更新できないぽいよね..。これを繰り返し呼ぶしかないのかな。
🤔 不要な Cart Attributes を削除したい
できないんだ、そうか.. Liquid 側( cart.js ) ではできるそう。
ちなみに、Storefront API でもできないね。空にしようとしても must not be blank
いわれて更新できないし。そのうちできるようになるかな。
📝 今の所の理解
Storefront API の cartAttributesUpdate で value に空を送った場合
- 空文字を渡すと
must not be blank
エラーで更新自体失敗する - cartAttributesUpdate は複数の attribute の更新をまとめてリクエストできるが、value が空文字なものが 1 件でもあればエラーとなってそのリクエスト自体失敗する
- 空文字以外にも スペース(
Extension の useApplyAttributeChange で value に空文字を送った場合
- こっちはvalue に空おくったら、
must not be blank
でなくて更新できる。ただし、その attributes は削除されない。値が空になって残り続ける。
📝 タイムアウト問題
- ネットワークが極端に遅い場合などでエクステンションの読み込みがタイムアウトしてしまう場合がある ( Chromeで 3G回線で試してみたけど、結構落ちる )
- タイムアウトした場合、自動で再読み込み等はされず、そのままエクステンション無し状態のチェックアウトのままで進めてしまう
- バンドルサイズを軽くしたり、通信あるものはなるだけ抑えたりと工夫が必要
なんらか注文上必須な項目をエクステンション内で設定させようとしている場合は、Checkout Validation API も併用してちゃんと値が設定されたかチェックしたほうがいいですね..
ref.