@shopbb/helium 0.5.10 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/cache/withCache.d.ts +49 -0
  2. package/dist/cache/withCache.d.ts.map +1 -0
  3. package/dist/cache/withCache.js +117 -0
  4. package/dist/cache/withCache.js.map +1 -0
  5. package/dist/components/AddToCartButton.d.ts +28 -22
  6. package/dist/components/AddToCartButton.d.ts.map +1 -1
  7. package/dist/components/AddToCartButton.js +36 -47
  8. package/dist/components/AddToCartButton.js.map +1 -1
  9. package/dist/components/BuyNowButton.d.ts +45 -0
  10. package/dist/components/BuyNowButton.d.ts.map +1 -0
  11. package/dist/components/BuyNowButton.js +49 -0
  12. package/dist/components/BuyNowButton.js.map +1 -0
  13. package/dist/components/CartCheckoutButton.d.ts +39 -0
  14. package/dist/components/CartCheckoutButton.d.ts.map +1 -0
  15. package/dist/components/CartCheckoutButton.js +32 -0
  16. package/dist/components/CartCheckoutButton.js.map +1 -0
  17. package/dist/components/CartCost.d.ts +43 -0
  18. package/dist/components/CartCost.d.ts.map +1 -0
  19. package/dist/components/CartCost.js +34 -0
  20. package/dist/components/CartCost.js.map +1 -0
  21. package/dist/components/CartForm.d.ts +201 -0
  22. package/dist/components/CartForm.d.ts.map +1 -0
  23. package/dist/components/CartForm.js +213 -0
  24. package/dist/components/CartForm.js.map +1 -0
  25. package/dist/components/CartLineProvider.d.ts +78 -0
  26. package/dist/components/CartLineProvider.d.ts.map +1 -0
  27. package/dist/components/CartLineProvider.js +46 -0
  28. package/dist/components/CartLineProvider.js.map +1 -0
  29. package/dist/components/CartLineQuantity.d.ts +24 -0
  30. package/dist/components/CartLineQuantity.d.ts.map +1 -0
  31. package/dist/components/CartLineQuantity.js +9 -0
  32. package/dist/components/CartLineQuantity.js.map +1 -0
  33. package/dist/components/DiscountSelector.d.ts.map +1 -1
  34. package/dist/components/DiscountSelector.js +8 -19
  35. package/dist/components/DiscountSelector.js.map +1 -1
  36. package/dist/components/Image.d.ts +18 -0
  37. package/dist/components/Image.d.ts.map +1 -1
  38. package/dist/components/Image.js +26 -0
  39. package/dist/components/Image.js.map +1 -1
  40. package/dist/components/Pagination.d.ts +82 -0
  41. package/dist/components/Pagination.d.ts.map +1 -0
  42. package/dist/components/Pagination.js +84 -0
  43. package/dist/components/Pagination.js.map +1 -0
  44. package/dist/components/RichText.d.ts +78 -0
  45. package/dist/components/RichText.d.ts.map +1 -0
  46. package/dist/components/RichText.js +93 -0
  47. package/dist/components/RichText.js.map +1 -0
  48. package/dist/components/Seo.d.ts +25 -0
  49. package/dist/components/Seo.d.ts.map +1 -0
  50. package/dist/components/Seo.js +54 -0
  51. package/dist/components/Seo.js.map +1 -0
  52. package/dist/components/hooks/useMoney.d.ts +40 -0
  53. package/dist/components/hooks/useMoney.d.ts.map +1 -0
  54. package/dist/components/hooks/useMoney.js +60 -0
  55. package/dist/components/hooks/useMoney.js.map +1 -0
  56. package/dist/components/hooks/useOptimisticCart.d.ts +50 -0
  57. package/dist/components/hooks/useOptimisticCart.d.ts.map +1 -0
  58. package/dist/components/hooks/useOptimisticCart.js +146 -0
  59. package/dist/components/hooks/useOptimisticCart.js.map +1 -0
  60. package/dist/components/index.d.ts +28 -0
  61. package/dist/components/index.d.ts.map +1 -1
  62. package/dist/components/index.js +21 -0
  63. package/dist/components/index.js.map +1 -1
  64. package/dist/createCartHandler.d.ts.map +1 -1
  65. package/dist/createCartHandler.js +57 -0
  66. package/dist/createCartHandler.js.map +1 -1
  67. package/dist/csp/csp.d.ts +57 -0
  68. package/dist/csp/csp.d.ts.map +1 -0
  69. package/dist/csp/csp.js +73 -0
  70. package/dist/csp/csp.js.map +1 -0
  71. package/dist/customer/createCustomerAccountClient.d.ts +43 -0
  72. package/dist/customer/createCustomerAccountClient.d.ts.map +1 -0
  73. package/dist/customer/createCustomerAccountClient.js +68 -0
  74. package/dist/customer/createCustomerAccountClient.js.map +1 -0
  75. package/dist/handleCartFormAction.d.ts +39 -0
  76. package/dist/handleCartFormAction.d.ts.map +1 -0
  77. package/dist/handleCartFormAction.js +103 -0
  78. package/dist/handleCartFormAction.js.map +1 -0
  79. package/dist/index.d.ts +18 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +11 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/routing/storefrontRedirect.d.ts +37 -0
  84. package/dist/routing/storefrontRedirect.d.ts.map +1 -0
  85. package/dist/routing/storefrontRedirect.js +64 -0
  86. package/dist/routing/storefrontRedirect.js.map +1 -0
  87. package/dist/seo/getSeoMeta.d.ts +68 -0
  88. package/dist/seo/getSeoMeta.d.ts.map +1 -0
  89. package/dist/seo/getSeoMeta.js +89 -0
  90. package/dist/seo/getSeoMeta.js.map +1 -0
  91. package/dist/sitemap/sitemap.d.ts +55 -0
  92. package/dist/sitemap/sitemap.d.ts.map +1 -0
  93. package/dist/sitemap/sitemap.js +93 -0
  94. package/dist/sitemap/sitemap.js.map +1 -0
  95. package/dist/types.d.ts +12 -0
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/utils/flattenConnection.d.ts +25 -0
  98. package/dist/utils/flattenConnection.d.ts.map +1 -0
  99. package/dist/utils/flattenConnection.js +25 -0
  100. package/dist/utils/flattenConnection.js.map +1 -0
  101. package/dist/utils/parseGid.d.ts +17 -0
  102. package/dist/utils/parseGid.d.ts.map +1 -0
  103. package/dist/utils/parseGid.js +19 -0
  104. package/dist/utils/parseGid.js.map +1 -0
  105. package/package.json +1 -1
  106. package/src/cache/withCache.ts +144 -0
  107. package/src/components/AddToCartButton.tsx +94 -56
  108. package/src/components/BuyNowButton.tsx +135 -0
  109. package/src/components/CartCheckoutButton.tsx +97 -0
  110. package/src/components/CartCost.tsx +65 -0
  111. package/src/components/CartForm.tsx +311 -0
  112. package/src/components/CartLineProvider.tsx +77 -0
  113. package/src/components/CartLineQuantity.tsx +37 -0
  114. package/src/components/DiscountSelector.tsx +34 -45
  115. package/src/components/Image.tsx +27 -0
  116. package/src/components/Pagination.tsx +139 -0
  117. package/src/components/RichText.tsx +122 -0
  118. package/src/components/Seo.tsx +61 -0
  119. package/src/components/hooks/useMoney.ts +87 -0
  120. package/src/components/hooks/useOptimisticCart.ts +183 -0
  121. package/src/components/index.ts +44 -0
  122. package/src/createCartHandler.ts +71 -0
  123. package/src/csp/csp.tsx +119 -0
  124. package/src/customer/createCustomerAccountClient.ts +89 -0
  125. package/src/handleCartFormAction.ts +129 -0
  126. package/src/index.ts +24 -0
  127. package/src/routing/storefrontRedirect.ts +86 -0
  128. package/src/seo/getSeoMeta.ts +125 -0
  129. package/src/sitemap/sitemap.ts +121 -0
  130. package/src/types.ts +12 -1
  131. package/src/utils/flattenConnection.ts +33 -0
  132. package/src/utils/parseGid.ts +25 -0
@@ -1,101 +1,139 @@
1
1
  /**
2
2
  * <AddToCartButton> — 加入购物车按钮
3
3
  *
4
- * 对齐 @shopify/hydrogen <AddToCartButton>:
5
- * - 内置 loading 态、disabled、error 处理
6
- * - 支持 onAdd 回调(实际加购逻辑由外部 cart hook 提供)
7
- * - 触发 analytics 事件钩子
8
- * - 不带样式,商家自己控制
4
+ * 对齐 Shopify Hydrogen:本质是包了一个 <CartForm action={LinesAdd}> + <button type="submit">。
9
5
  *
10
- * 用法(Phase 1,需要传 onAdd):
11
- * const { addLine } = useCart();
12
- * <AddToCartButton
13
- * variantId={selectedVariant.id}
14
- * quantity={1}
15
- * disabled={!selectedVariant.availableForSale}
16
- * onAdd={(vid, qty) => addLine(vid, qty)}
17
- * >
18
- * 加入购物车
19
- * </AddToCartButton>
6
+ * - SSR HTML 是原生 <form>,无 JS 也能加购(form 提交 → 服务端 redirect 回 referrer)
7
+ * - hydrate JS 拦截 submit → fetch /cart → CartProvider.applyCart → 无刷新
8
+ * - loading 状态由 CartForm.fetcher 暴露,通过 useFetcher() 读取
20
9
  *
21
- * Phase 2 后:组件会自动从 <CartProvider> 拿,无需 onAdd 也能用。
10
+ * 商家也可以直接组合 <CartForm>,不用我们这个 wrapper:
11
+ *
12
+ * <CartForm route="/cart" action={CartForm.ACTIONS.LinesAdd} inputs={{ lines: [...] }}>
13
+ * <button type="submit">加入购物车</button>
14
+ * </CartForm>
22
15
  */
23
16
 
24
17
  import * as React from 'react';
25
- import { useCartOptional } from './CartProvider';
18
+ import { CartForm, useFetcher, type CartLineInput } from './CartForm';
26
19
  import { useAnalytics } from './AnalyticsProvider';
27
20
 
28
- export interface AddToCartButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'onError'> {
21
+ export interface AddToCartButtonProps {
29
22
  /** 必传:variant 的 GID(gid://shopbb/ProductVariant/...) */
30
23
  variantId: string;
31
24
  /** 数量,默认 1 */
32
25
  quantity?: number;
33
- /** 实际加购回调。Phase 1 必传;Phase 2 后可从 Provider 自动获取 */
34
- onAdd?: (variantId: string, quantity: number) => Promise<void>;
35
- /** 加购成功回调(如导航到 /cart) */
26
+ /** 行级 attributes(如 gift_message) */
27
+ attributes?: Array<{ key: string; value: string }>;
28
+ /** 加购成功回调(仅 hydrated 后触发) */
36
29
  onAdded?: () => void;
37
30
  /** 加购失败回调 */
38
31
  onError?: (err: Error) => void;
39
- /** 加购中显示的文本,默认 "加入中..." */
32
+ /** 加购中显示的文本 */
40
33
  loadingText?: React.ReactNode;
41
- /** 不可用时显示的文本,默认 "缺货" */
34
+ /** 不可用时显示的文本 */
42
35
  unavailableText?: React.ReactNode;
36
+ /** disabled */
37
+ disabled?: boolean;
38
+ /** 包装 form className */
39
+ className?: string;
40
+ /** 按钮 className */
41
+ buttonClassName?: string;
42
+ /** 提交的 route,默认 /cart */
43
+ route?: string;
44
+ /** 按钮内容 */
45
+ children?: React.ReactNode;
43
46
  }
44
47
 
45
48
  export function AddToCartButton(props: AddToCartButtonProps) {
46
49
  const {
47
50
  variantId,
48
51
  quantity = 1,
49
- onAdd,
52
+ attributes,
50
53
  onAdded,
51
54
  onError,
52
55
  loadingText = '加入中...',
53
56
  unavailableText = '缺货',
57
+ disabled = false,
58
+ className,
59
+ buttonClassName,
60
+ route = '/cart',
54
61
  children = '加入购物车',
55
- disabled: disabledProp,
56
- ...rest
57
62
  } = props;
58
63
 
59
- const [adding, setAdding] = React.useState(false);
60
- const cart = useCartOptional();
61
- const analytics = useAnalytics();
64
+ const line: CartLineInput = { merchandiseId: variantId, quantity };
65
+ if (attributes && attributes.length > 0) line.attributes = attributes;
66
+
67
+ return (
68
+ <CartForm
69
+ route={route}
70
+ action={CartForm.ACTIONS.LinesAdd}
71
+ inputs={{ lines: [line] }}
72
+ className={className}
73
+ >
74
+ <AddToCartButtonInner
75
+ disabled={disabled}
76
+ buttonClassName={buttonClassName}
77
+ loadingText={loadingText}
78
+ unavailableText={unavailableText}
79
+ variantId={variantId}
80
+ quantity={quantity}
81
+ onAdded={onAdded}
82
+ onError={onError}
83
+ >
84
+ {children}
85
+ </AddToCartButtonInner>
86
+ </CartForm>
87
+ );
88
+ }
62
89
 
63
- const handleClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
64
- e.preventDefault();
65
- if (adding || disabledProp) return;
90
+ /**
91
+ * Inner component — 合法地 useFetcher / useEffect,监听 fetcher.state 变化。
92
+ */
93
+ function AddToCartButtonInner({
94
+ disabled, buttonClassName, loadingText, unavailableText, children,
95
+ variantId, quantity, onAdded, onError,
96
+ }: {
97
+ disabled: boolean;
98
+ buttonClassName?: string;
99
+ loadingText: React.ReactNode;
100
+ unavailableText: React.ReactNode;
101
+ children: React.ReactNode;
102
+ variantId: string;
103
+ quantity: number;
104
+ onAdded?: () => void;
105
+ onError?: (err: Error) => void;
106
+ }) {
107
+ const fetcher = useFetcher();
108
+ const analytics = useAnalytics();
66
109
 
67
- setAdding(true);
68
- try {
69
- if (onAdd) {
70
- // 商家显式传了 onAdd 优先用(Phase 1 兼容路径)
71
- await onAdd(variantId, quantity);
72
- } else if (cart) {
73
- // 自动从 <CartProvider> 取
74
- await cart.linesAdd([{ merchandiseId: variantId, quantity }]);
75
- } else {
76
- console.warn('[AddToCartButton] no onAdd and no <CartProvider> — button does nothing');
77
- return;
110
+ // 监听 fetcher 完成
111
+ const prevState = React.useRef(fetcher.state);
112
+ React.useEffect(() => {
113
+ const wasNonIdle = prevState.current !== 'idle';
114
+ const isIdle = fetcher.state === 'idle';
115
+ if (wasNonIdle && isIdle) {
116
+ if (fetcher.error) {
117
+ onError?.(new Error(fetcher.error));
118
+ } else if (fetcher.data?.cart) {
119
+ analytics.emit('add_to_cart', { variantId, quantity });
120
+ onAdded?.();
78
121
  }
79
- analytics.emit('add_to_cart', { variantId, quantity });
80
- onAdded?.();
81
- } catch (err: any) {
82
- console.error('[AddToCartButton] add failed:', err);
83
- onError?.(err instanceof Error ? err : new Error(String(err)));
84
- } finally {
85
- setAdding(false);
86
122
  }
87
- };
123
+ prevState.current = fetcher.state;
124
+ }, [fetcher.state, fetcher.error, fetcher.data, onAdded, onError, analytics, variantId, quantity]);
125
+
126
+ const adding = fetcher.state !== 'idle';
88
127
 
89
128
  return (
90
129
  <button
91
- type="button"
92
- {...rest}
130
+ type="submit"
131
+ className={buttonClassName}
132
+ disabled={disabled || adding}
93
133
  data-add-to-cart
94
134
  data-loading={adding ? '' : undefined}
95
- disabled={disabledProp || adding}
96
- onClick={handleClick}
97
135
  >
98
- {adding ? loadingText : disabledProp ? unavailableText : children}
136
+ {adding ? loadingText : disabled ? unavailableText : children}
99
137
  </button>
100
138
  );
101
139
  }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * <BuyNowButton> — 对齐 Hydrogen React
3
+ *
4
+ * "立即购买" — 创建/添加 line + 跳 checkout,一步到位。
5
+ *
6
+ * 实现:
7
+ * 1. 内部包 <CartForm action="LinesAdd" inputs={{lines, navigateOnSuccess: '/checkout'}}>
8
+ * 2. submit 成功后 client 自动 navigate 到 checkoutUrl(或 fallback /checkout)
9
+ * 3. 无 JS 时:浏览器走 form POST,服务端 303 跳 /checkout
10
+ *
11
+ * 用法:
12
+ * <BuyNowButton variantId="gid://..." quantity={1}>立即购买</BuyNowButton>
13
+ */
14
+
15
+ import * as React from 'react';
16
+ import { CartForm, useFetcher, type CartLineInput } from './CartForm';
17
+ import { useAnalytics } from './AnalyticsProvider';
18
+
19
+ export interface BuyNowButtonProps {
20
+ /** 必传 variant GID */
21
+ variantId: string;
22
+ /** 数量,默认 1 */
23
+ quantity?: number;
24
+ /** 行级 attributes */
25
+ attributes?: Array<{ key: string; value: string }>;
26
+ /** 加购后跳转的 URL(client 端),默认走 cart.checkoutUrl,回退 /checkout */
27
+ redirectTo?: string;
28
+ /** 失败回调 */
29
+ onError?: (err: Error) => void;
30
+ /** Loading 文案 */
31
+ loadingText?: React.ReactNode;
32
+ /** Disabled 文案 */
33
+ unavailableText?: React.ReactNode;
34
+ /** disabled */
35
+ disabled?: boolean;
36
+ /** Form className */
37
+ className?: string;
38
+ /** 按钮 className */
39
+ buttonClassName?: string;
40
+ /** 按钮 content */
41
+ children?: React.ReactNode;
42
+ /** 提交 route,默认 /cart */
43
+ route?: string;
44
+ }
45
+
46
+ export function BuyNowButton(props: BuyNowButtonProps) {
47
+ const {
48
+ variantId,
49
+ quantity = 1,
50
+ attributes,
51
+ redirectTo,
52
+ onError,
53
+ loadingText = '处理中...',
54
+ unavailableText = '暂不可购',
55
+ disabled = false,
56
+ className,
57
+ buttonClassName,
58
+ children = '立即购买',
59
+ route = '/cart',
60
+ } = props;
61
+
62
+ const line: CartLineInput = { merchandiseId: variantId, quantity };
63
+ if (attributes && attributes.length > 0) line.attributes = attributes;
64
+
65
+ return (
66
+ <CartForm
67
+ route={route}
68
+ action={CartForm.ACTIONS.LinesAdd}
69
+ inputs={{ lines: [line] }}
70
+ className={className}
71
+ >
72
+ <BuyNowButtonInner
73
+ disabled={disabled}
74
+ buttonClassName={buttonClassName}
75
+ loadingText={loadingText}
76
+ unavailableText={unavailableText}
77
+ variantId={variantId}
78
+ quantity={quantity}
79
+ redirectTo={redirectTo}
80
+ onError={onError}
81
+ >
82
+ {children}
83
+ </BuyNowButtonInner>
84
+ </CartForm>
85
+ );
86
+ }
87
+
88
+ function BuyNowButtonInner({
89
+ disabled, buttonClassName, loadingText, unavailableText, children,
90
+ variantId, quantity, redirectTo, onError,
91
+ }: {
92
+ disabled: boolean;
93
+ buttonClassName?: string;
94
+ loadingText: React.ReactNode;
95
+ unavailableText: React.ReactNode;
96
+ children: React.ReactNode;
97
+ variantId: string;
98
+ quantity: number;
99
+ redirectTo?: string;
100
+ onError?: (err: Error) => void;
101
+ }) {
102
+ const fetcher = useFetcher();
103
+ const analytics = useAnalytics();
104
+ const prevState = React.useRef(fetcher.state);
105
+
106
+ React.useEffect(() => {
107
+ const wasNonIdle = prevState.current !== 'idle';
108
+ const isIdle = fetcher.state === 'idle';
109
+ if (wasNonIdle && isIdle) {
110
+ if (fetcher.error) {
111
+ onError?.(new Error(fetcher.error));
112
+ } else if (fetcher.data?.cart) {
113
+ analytics.emit('buy_now', { variantId, quantity });
114
+ const target = redirectTo || (fetcher.data.cart as any).checkoutUrl || '/checkout';
115
+ if (typeof window !== 'undefined') {
116
+ window.location.href = target;
117
+ }
118
+ }
119
+ }
120
+ prevState.current = fetcher.state;
121
+ }, [fetcher.state, fetcher.error, fetcher.data, redirectTo, onError, analytics, variantId, quantity]);
122
+
123
+ const pending = fetcher.state !== 'idle';
124
+ return (
125
+ <button
126
+ type="submit"
127
+ className={buttonClassName}
128
+ disabled={disabled || pending}
129
+ data-buy-now
130
+ data-loading={pending ? '' : undefined}
131
+ >
132
+ {pending ? loadingText : disabled ? unavailableText : children}
133
+ </button>
134
+ );
135
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * <CartCheckoutButton> — 对齐 Hydrogen React
3
+ *
4
+ * 渲染一个跳到 cart.checkoutUrl 的按钮。最常见的"去结算"入口。
5
+ *
6
+ * 行为:
7
+ * - 取 cart.checkoutUrl,无 cart / 空 cart 时按钮 disabled
8
+ * - 默认 <a> 元素(无 JS 也能用),可改成 <button> + onClick
9
+ * - 渲染纯样式,行为非业务逻辑
10
+ *
11
+ * 用法:
12
+ * <CartCheckoutButton>去结算</CartCheckoutButton>
13
+ *
14
+ * // 自定义渲染
15
+ * <CartCheckoutButton>
16
+ * {(href, disabled) => (
17
+ * <a href={href} className="my-cta" aria-disabled={disabled}>
18
+ * 结算 {cart.totalQuantity} 件
19
+ * </a>
20
+ * )}
21
+ * </CartCheckoutButton>
22
+ */
23
+
24
+ import * as React from 'react';
25
+ import { useCart } from './CartProvider';
26
+
27
+ export interface CartCheckoutButtonProps {
28
+ /** className */
29
+ className?: string;
30
+ /** 强制 disabled(即使 cart 有 checkoutUrl) */
31
+ disabled?: boolean;
32
+ /** 按钮内容;不传默认 "去结算" */
33
+ children?: React.ReactNode | ((href: string, disabled: boolean) => React.ReactNode);
34
+ /** 自定义渲染元素,默认 a */
35
+ as?: 'a' | 'button';
36
+ /** 点击回调(事件触发) */
37
+ onClick?: React.MouseEventHandler;
38
+ /** 透传其它 props 到根元素 */
39
+ [key: string]: any;
40
+ }
41
+
42
+ export function CartCheckoutButton(props: CartCheckoutButtonProps) {
43
+ const { className, disabled: disabledProp, children, as = 'a', onClick, ...rest } = props;
44
+ const { cart } = useCart();
45
+
46
+ const href = (cart as any)?.checkoutUrl as string | undefined;
47
+ const noCart = !cart || cart.totalQuantity === 0 || !href;
48
+ const disabled = !!disabledProp || noCart;
49
+
50
+ if (typeof children === 'function') {
51
+ return <>{children(href || '#', disabled)}</>;
52
+ }
53
+
54
+ const label = children ?? '去结算';
55
+
56
+ if (as === 'button') {
57
+ return (
58
+ <button
59
+ type="button"
60
+ className={className}
61
+ disabled={disabled}
62
+ data-cart-checkout-button
63
+ onClick={(e) => {
64
+ if (disabled) return;
65
+ onClick?.(e);
66
+ if (!e.defaultPrevented && href) {
67
+ window.location.href = href;
68
+ }
69
+ }}
70
+ {...rest}
71
+ >
72
+ {label}
73
+ </button>
74
+ );
75
+ }
76
+
77
+ // <a> 形式:无 JS 也能跳
78
+ return (
79
+ <a
80
+ href={disabled ? undefined : href}
81
+ className={className}
82
+ aria-disabled={disabled || undefined}
83
+ data-cart-checkout-button
84
+ data-disabled={disabled ? '' : undefined}
85
+ onClick={(e) => {
86
+ if (disabled) {
87
+ e.preventDefault();
88
+ return;
89
+ }
90
+ onClick?.(e);
91
+ }}
92
+ {...rest}
93
+ >
94
+ {label}
95
+ </a>
96
+ );
97
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * <CartCost> — 对齐 Hydrogen React
3
+ *
4
+ * 从 <CartProvider> 拿 cart.cost 渲染金额。amountType 控制渲染哪个:
5
+ * "total" → cost.totalAmount (默认)
6
+ * "subtotal" → cost.subtotalAmount
7
+ * "tax" → cost.totalTaxAmount
8
+ * "duty" → cost.totalDutyAmount
9
+ * "discount" → cost.totalDiscountAmount(shopbb-extension)
10
+ *
11
+ * 内部用 <Money> 渲染,自动按 currencyCode 加货币符号。
12
+ *
13
+ * 用法:
14
+ * <CartCost amountType="subtotal" /> ¥300.00
15
+ * <CartCost amountType="discount" as="strong" /> ¥30.00
16
+ * <CartCost amountType="total" withoutTrailingZeros />
17
+ *
18
+ * 自定义渲染:
19
+ * <CartCost amountType="total">
20
+ * {(money) => <strong>{money.amount} {money.currencyCode}</strong>}
21
+ * </CartCost>
22
+ */
23
+
24
+ import * as React from 'react';
25
+ import { useCart } from './CartProvider';
26
+ import { Money, type MoneyProps } from './Money';
27
+
28
+ export interface MoneyValue {
29
+ amount: string;
30
+ currencyCode: string;
31
+ }
32
+
33
+ export interface CartCostProps extends Omit<MoneyProps, 'data' | 'children'> {
34
+ /**
35
+ * 渲染哪一项 cost。默认 "total"。
36
+ * - "total" : 含运费 / 税 / 折扣的最终金额
37
+ * - "subtotal" : 商品小计(折扣前 / 运费前)
38
+ * - "tax" : 税
39
+ * - "duty" : 关税
40
+ * - "discount" : 优惠金额(shopbb extension)
41
+ */
42
+ amountType?: 'total' | 'subtotal' | 'tax' | 'duty' | 'discount';
43
+ /** 自定义渲染:(money) => ReactNode */
44
+ children?: (money: MoneyValue) => React.ReactNode;
45
+ }
46
+
47
+ export function CartCost(props: CartCostProps) {
48
+ const { amountType = 'total', children, ...moneyProps } = props;
49
+ const { cart } = useCart();
50
+ if (!cart?.cost) return null;
51
+
52
+ const cost = cart.cost as any;
53
+ let money: MoneyValue | null = null;
54
+ switch (amountType) {
55
+ case 'total': money = cost.totalAmount ?? null; break;
56
+ case 'subtotal': money = cost.subtotalAmount ?? null; break;
57
+ case 'tax': money = cost.totalTaxAmount ?? cost.taxAmount ?? null; break;
58
+ case 'duty': money = cost.totalDutyAmount ?? cost.dutyAmount ?? null; break;
59
+ case 'discount': money = cost.totalDiscountAmount ?? null; break;
60
+ }
61
+ if (!money) return null;
62
+
63
+ if (children) return <>{children(money)}</>;
64
+ return <Money data={money} {...moneyProps} />;
65
+ }