@shopbb/helium 0.6.3 → 0.7.0

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 (81) hide show
  1. package/dist/components/AddToCartButton.d.ts +17 -22
  2. package/dist/components/AddToCartButton.d.ts.map +1 -1
  3. package/dist/components/AddToCartButton.js +8 -45
  4. package/dist/components/AddToCartButton.js.map +1 -1
  5. package/dist/components/AddressForm.d.ts +42 -18
  6. package/dist/components/AddressForm.d.ts.map +1 -1
  7. package/dist/components/AddressForm.js +23 -20
  8. package/dist/components/AddressForm.js.map +1 -1
  9. package/dist/components/AddressList.d.ts +34 -17
  10. package/dist/components/AddressList.d.ts.map +1 -1
  11. package/dist/components/AddressList.js +7 -21
  12. package/dist/components/AddressList.js.map +1 -1
  13. package/dist/components/AddressPicker.d.ts +14 -16
  14. package/dist/components/AddressPicker.d.ts.map +1 -1
  15. package/dist/components/AddressPicker.js +10 -26
  16. package/dist/components/AddressPicker.js.map +1 -1
  17. package/dist/components/AnalyticsProvider.d.ts +5 -2
  18. package/dist/components/AnalyticsProvider.d.ts.map +1 -1
  19. package/dist/components/AnalyticsProvider.js +13 -11
  20. package/dist/components/AnalyticsProvider.js.map +1 -1
  21. package/dist/components/BuyNowButton.d.ts +7 -24
  22. package/dist/components/BuyNowButton.d.ts.map +1 -1
  23. package/dist/components/BuyNowButton.js +9 -43
  24. package/dist/components/BuyNowButton.js.map +1 -1
  25. package/dist/components/CartCheckoutButton.d.ts +10 -21
  26. package/dist/components/CartCheckoutButton.d.ts.map +1 -1
  27. package/dist/components/CartCheckoutButton.js +6 -11
  28. package/dist/components/CartCheckoutButton.js.map +1 -1
  29. package/dist/components/CartCost.d.ts +15 -23
  30. package/dist/components/CartCost.d.ts.map +1 -1
  31. package/dist/components/CartCost.js +1 -3
  32. package/dist/components/CartCost.js.map +1 -1
  33. package/dist/components/CartForm.d.ts +30 -102
  34. package/dist/components/CartForm.d.ts.map +1 -1
  35. package/dist/components/CartForm.js +32 -172
  36. package/dist/components/CartForm.js.map +1 -1
  37. package/dist/components/DiscountComponents.d.ts +67 -17
  38. package/dist/components/DiscountComponents.d.ts.map +1 -1
  39. package/dist/components/DiscountComponents.js +28 -74
  40. package/dist/components/DiscountComponents.js.map +1 -1
  41. package/dist/components/DiscountSelector.d.ts +50 -15
  42. package/dist/components/DiscountSelector.d.ts.map +1 -1
  43. package/dist/components/DiscountSelector.js +16 -44
  44. package/dist/components/DiscountSelector.js.map +1 -1
  45. package/dist/components/hooks/useOptimisticCart.d.ts +36 -37
  46. package/dist/components/hooks/useOptimisticCart.d.ts.map +1 -1
  47. package/dist/components/hooks/useOptimisticCart.js +95 -127
  48. package/dist/components/hooks/useOptimisticCart.js.map +1 -1
  49. package/dist/components/index.d.ts +24 -45
  50. package/dist/components/index.d.ts.map +1 -1
  51. package/dist/components/index.js +21 -37
  52. package/dist/components/index.js.map +1 -1
  53. package/dist/createCartHandler.d.ts.map +1 -1
  54. package/dist/createCartHandler.js +8 -1
  55. package/dist/createCartHandler.js.map +1 -1
  56. package/dist/index.d.ts +0 -2
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +0 -1
  59. package/dist/index.js.map +1 -1
  60. package/package.json +4 -10
  61. package/src/components/AddToCartButton.tsx +34 -92
  62. package/src/components/AddressForm.tsx +56 -26
  63. package/src/components/AddressList.tsx +42 -33
  64. package/src/components/AddressPicker.tsx +19 -29
  65. package/src/components/AnalyticsProvider.tsx +18 -13
  66. package/src/components/BuyNowButton.tsx +28 -93
  67. package/src/components/CartCheckoutButton.tsx +16 -33
  68. package/src/components/CartCost.tsx +16 -28
  69. package/src/components/CartForm.tsx +87 -231
  70. package/src/components/DiscountComponents.tsx +94 -100
  71. package/src/components/DiscountSelector.tsx +68 -49
  72. package/src/components/hooks/useOptimisticCart.ts +122 -156
  73. package/src/components/index.ts +51 -99
  74. package/src/createCartHandler.ts +10 -1
  75. package/src/index.ts +0 -2
  76. /package/src/components/{AddressBookProvider.tsx → AddressBookProvider.tsx.deleted-0.7} +0 -0
  77. /package/src/components/{CartLineQuantityAdjustButton.tsx → CartLineQuantityAdjustButton.tsx.deleted-0.7} +0 -0
  78. /package/src/components/{CartProvider.tsx → CartProvider.tsx.deleted-0.7} +0 -0
  79. /package/src/components/{DiscountProvider.tsx → DiscountProvider.tsx.deleted-0.7} +0 -0
  80. /package/src/components/hooks/{useMounted.ts → useMounted.ts.deleted-0.7} +0 -0
  81. /package/src/{handleCartFormAction.ts → handleCartFormAction.ts.deleted-0.7} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopbb/helium",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "shopbb storefront framework — components, React SSR, GraphQL client, cart handler, cache for Cloudflare Workers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,15 +49,8 @@
49
49
  "repository": "https://github.com/shopbb/shopbb",
50
50
  "peerDependencies": {
51
51
  "react": ">=18",
52
- "react-dom": ">=18"
53
- },
54
- "peerDependenciesMeta": {
55
- "react": {
56
- "optional": true
57
- },
58
- "react-dom": {
59
- "optional": true
60
- }
52
+ "react-dom": ">=18",
53
+ "react-router": ">=7"
61
54
  },
62
55
  "devDependencies": {
63
56
  "@cloudflare/workers-types": "^4.20240117.0",
@@ -65,6 +58,7 @@
65
58
  "@types/react-dom": "^18",
66
59
  "react": "^18.3.0",
67
60
  "react-dom": "^18.3.0",
61
+ "react-router": "^7.0.0",
68
62
  "typescript": "^5.3.3"
69
63
  },
70
64
  "engines": {
@@ -1,47 +1,41 @@
1
1
  /**
2
2
  * <AddToCartButton> — 加入购物车按钮
3
3
  *
4
- * 对齐 Shopify Hydrogen:本质是包了一个 <CartForm action={LinesAdd}> + <button type="submit">。
4
+ * helium 0.7:对齐 Hydrogen 模式。这是 <CartForm action=LinesAdd> 的便捷包装。
5
5
  *
6
- * - SSR HTML 是原生 <form>,无 JS 也能加购(form 提交 → 服务端 redirect 回 referrer)
7
- * - hydrate 后 JS 拦截 submit → fetch /cart → CartProvider.applyCart → 无刷新
8
- * - loading 状态由 CartForm.fetcher 暴露,通过 useFetcher() 读取
9
- *
10
- * 商家也可以直接组合 <CartForm>,不用我们这个 wrapper:
6
+ * 商家也可以直接用 <CartForm>(更灵活):
11
7
  *
12
8
  * <CartForm route="/cart" action={CartForm.ACTIONS.LinesAdd} inputs={{ lines: [...] }}>
13
- * <button type="submit">加入购物车</button>
9
+ * {(fetcher) => (
10
+ * <button type="submit" disabled={fetcher.state !== 'idle'}>
11
+ * {fetcher.state === 'idle' ? '加入购物车' : '加入中...'}
12
+ * </button>
13
+ * )}
14
14
  * </CartForm>
15
+ *
16
+ * 想要加购后自定义行为(弹抽屉、跳页等):商家组件用 useFetcher / useNavigation
17
+ * 监听全局 fetcher 状态。Hydrogen demo store 用 `useAside().open()` 模式。
15
18
  */
16
19
 
17
20
  import * as React from 'react';
18
- import { CartForm, useFetcher, type CartLineInput } from './CartForm';
19
- import { useAnalytics } from './AnalyticsProvider';
21
+ import { CartForm, type CartLineInput } from './CartForm';
20
22
 
21
23
  export interface AddToCartButtonProps {
22
- /** 必传:variant GID(gid://shopbb/ProductVariant/...) */
24
+ /** variant GID */
23
25
  variantId: string;
24
26
  /** 数量,默认 1 */
25
27
  quantity?: number;
26
- /** 行级 attributes(如 gift_message) */
28
+ /** 行级 attributes */
27
29
  attributes?: Array<{ key: string; value: string }>;
28
- /** 加购成功回调(仅 hydrated 后触发) */
29
- onAdded?: () => void;
30
- /** 加购失败回调 */
31
- onError?: (err: Error) => void;
32
- /** 加购中显示的文本 */
33
- loadingText?: React.ReactNode;
34
- /** 不可用时显示的文本 */
30
+ /** optimistic UI 用:传整个 variant 对象(含 product 等) */
31
+ selectedVariant?: any;
32
+ /** Disabled 文本 */
35
33
  unavailableText?: React.ReactNode;
36
- /** disabled */
37
34
  disabled?: boolean;
38
- /** 包装 form className */
39
35
  className?: string;
40
- /** 按钮 className */
41
- buttonClassName?: string;
42
- /** 提交的 route,默认 /cart */
36
+ /** 提交 route,默认 /cart */
43
37
  route?: string;
44
- /** 按钮内容 */
38
+ /** 按钮 children */
45
39
  children?: React.ReactNode;
46
40
  }
47
41
 
@@ -50,90 +44,38 @@ export function AddToCartButton(props: AddToCartButtonProps) {
50
44
  variantId,
51
45
  quantity = 1,
52
46
  attributes,
53
- onAdded,
54
- onError,
55
- loadingText = '加入中...',
47
+ selectedVariant,
56
48
  unavailableText = '缺货',
57
49
  disabled = false,
58
50
  className,
59
- buttonClassName,
60
51
  route = '/cart',
61
52
  children = '加入购物车',
62
53
  } = props;
63
54
 
64
55
  const line: CartLineInput = { merchandiseId: variantId, quantity };
65
56
  if (attributes && attributes.length > 0) line.attributes = attributes;
57
+ if (selectedVariant) line.selectedVariant = selectedVariant;
66
58
 
67
59
  return (
68
60
  <CartForm
69
61
  route={route}
70
62
  action={CartForm.ACTIONS.LinesAdd}
71
63
  inputs={{ lines: [line] }}
72
- className={className}
73
64
  >
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>
65
+ {(fetcher) => {
66
+ const submitting = fetcher.state !== 'idle';
67
+ return (
68
+ <button
69
+ type="submit"
70
+ className={className}
71
+ disabled={disabled || submitting}
72
+ data-add-to-cart
73
+ data-loading={submitting ? '' : undefined}
74
+ >
75
+ {submitting ? '加入中...' : disabled ? unavailableText : children}
76
+ </button>
77
+ );
78
+ }}
86
79
  </CartForm>
87
80
  );
88
81
  }
89
-
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();
109
-
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?.();
121
- }
122
- }
123
- prevState.current = fetcher.state;
124
- }, [fetcher.state, fetcher.error, fetcher.data, onAdded, onError, analytics, variantId, quantity]);
125
-
126
- const adding = fetcher.state !== 'idle';
127
-
128
- return (
129
- <button
130
- type="submit"
131
- className={buttonClassName}
132
- disabled={disabled || adding}
133
- data-add-to-cart
134
- data-loading={adding ? '' : undefined}
135
- >
136
- {adding ? loadingText : disabled ? unavailableText : children}
137
- </button>
138
- );
139
- }
@@ -1,41 +1,68 @@
1
1
  /**
2
2
  * <AddressForm>
3
3
  *
4
- * 地址编辑 / 新增表单。**自带**:
5
- * - 表单 state(useState)
6
- * - 必填验证
7
- * - 调 useAddressBook().createAddress / updateAddress
8
- * - userErrors 显示
9
- * - loading 态
4
+ * 地址编辑 / 新增表单。
10
5
  *
11
- * 用法(新增):
12
- * <AddressForm onSave={() => setCreating(false)} onCancel={...} />
6
+ * helium 0.7:删除 useAddressBook 调用,改为接 onSubmit prop(商家自己处理 mutation)。
13
7
  *
14
- * 用法(编辑):
15
- * <AddressForm initial={editingAddress} onSave={...} onCancel={...} />
16
- *
17
- * 表单不带样式(用 data-* 钩子或商家自己 className 控制)。
8
+ * 用法:
9
+ * <AddressForm
10
+ * initial={editingAddress}
11
+ * onSubmit={async (input, asDefault) => {
12
+ * // 商家调自己的 action 或 fetch
13
+ * const result = await myCreateOrUpdate(input, asDefault);
14
+ * return { userErrors: result.userErrors, address: result.address };
15
+ * }}
16
+ * onSaved={(addr) => setCreating(false)}
17
+ * onCancel={() => setCreating(false)}
18
+ * />
18
19
  */
19
20
 
20
21
  import * as React from 'react';
21
- import { useAddressBook, type Address, type AddressInput, type AddressBookUserError } from './AddressBookProvider';
22
+ import type { Address } from './AddressList';
23
+
24
+ export interface AddressInput {
25
+ firstName: string;
26
+ lastName?: string;
27
+ company?: string;
28
+ phone: string;
29
+ country?: string;
30
+ countryCode?: string;
31
+ province: string;
32
+ provinceCode?: string;
33
+ city: string;
34
+ district?: string;
35
+ address1: string;
36
+ address2?: string;
37
+ zip?: string;
38
+ }
39
+
40
+ export interface AddressBookUserError {
41
+ field?: string[];
42
+ code: string;
43
+ message: string;
44
+ }
45
+
46
+ export interface AddressFormSubmitResult {
47
+ address?: Address;
48
+ userErrors: AddressBookUserError[];
49
+ }
22
50
 
23
51
  export interface AddressFormProps {
24
52
  /** 传 = 编辑模式;不传 = 新增模式 */
25
53
  initial?: Address;
54
+ /** 提交回调(商家实现 mutation)—— 必填 */
55
+ onSubmit?: (input: AddressInput, asDefault: boolean) => Promise<AddressFormSubmitResult>;
26
56
  /** 保存成功后触发 */
27
- onSave?: (address?: Address) => void;
57
+ onSaved?: (address?: Address) => void;
28
58
  /** 取消按钮触发 */
29
59
  onCancel?: () => void;
30
- /** 自定义提交按钮文字 */
31
60
  submitText?: string;
32
61
  cancelText?: string;
33
- /** 显示"设为默认"复选框,默认 true */
62
+ /** 显示"设为默认"复选框 */
34
63
  showDefaultCheckbox?: boolean;
35
- /** 哪些字段必填,默认 ['firstName','phone','address1','city','province'] */
36
64
  requiredFields?: Array<keyof AddressInput>;
37
65
  className?: string;
38
- /** i18n 文案覆盖 */
39
66
  i18n?: Partial<AddressFormI18n>;
40
67
  }
41
68
 
@@ -73,7 +100,7 @@ const DEFAULT_I18N: AddressFormI18n = {
73
100
 
74
101
  export function AddressForm(props: AddressFormProps) {
75
102
  const {
76
- initial, onSave, onCancel,
103
+ initial, onSubmit, onSaved, onCancel,
77
104
  submitText = '保存',
78
105
  cancelText = '取消',
79
106
  showDefaultCheckbox = true,
@@ -82,7 +109,6 @@ export function AddressForm(props: AddressFormProps) {
82
109
  i18n: i18nOverride,
83
110
  } = props;
84
111
 
85
- const { createAddress, updateAddress } = useAddressBook();
86
112
  const i18n = { ...DEFAULT_I18N, ...i18nOverride };
87
113
 
88
114
  const [form, setForm] = React.useState<AddressInput>({
@@ -104,27 +130,31 @@ export function AddressForm(props: AddressFormProps) {
104
130
  const [saving, setSaving] = React.useState(false);
105
131
  const [errors, setErrors] = React.useState<AddressBookUserError[]>([]);
106
132
 
107
- const update = (k: keyof AddressInput, v: string) => setForm((f) => ({ ...f, [k]: v }));
133
+ const update = (k: keyof AddressInput, v: string) =>
134
+ setForm((f) => ({ ...f, [k]: v } as any));
108
135
 
109
136
  const handleSubmit = async (e: React.FormEvent) => {
110
137
  e.preventDefault();
111
- // 客户端必填校验
112
138
  const missing = requiredFields.filter((k) => !(form[k] && String(form[k]).trim()));
113
139
  if (missing.length > 0) {
114
140
  setErrors([{ field: missing as string[], code: 'MISSING_FIELD', message: '请填写完整地址' }]);
115
141
  return;
116
142
  }
143
+ if (!onSubmit) {
144
+ setErrors([{ code: 'NO_HANDLER', message: '商家未传入 onSubmit 处理函数' }]);
145
+ return;
146
+ }
117
147
  setSaving(true);
118
148
  setErrors([]);
119
149
  try {
120
- const result = initial
121
- ? await updateAddress(initial.id, form, asDefault)
122
- : await createAddress(form, asDefault);
150
+ const result = await onSubmit(form, asDefault);
123
151
  if (result.userErrors.length > 0) {
124
152
  setErrors(result.userErrors);
125
153
  return;
126
154
  }
127
- onSave?.(result.address);
155
+ onSaved?.(result.address);
156
+ } catch (e: any) {
157
+ setErrors([{ code: 'SUBMIT_ERROR', message: e?.message || '提交失败' }]);
128
158
  } finally {
129
159
  setSaving(false);
130
160
  }
@@ -3,53 +3,63 @@
3
3
  *
4
4
  * 渲染当前买家的地址列表。
5
5
  *
6
- * 用法:
7
- * <AddressList
8
- * onEdit={(addr) => openEditModal(addr)}
9
- * onAdd={() => openCreateModal()}
10
- * emptyText="还没有保存的地址"
11
- * />
6
+ * helium 0.7:addresses 通过 props 传入(不再用 AddressBookProvider)。
7
+ * 商家从 customer GraphQL 拉 addresses + defaultAddress 传给本组件。
8
+ * setDefault / remove 由商家通过 RR7 action 实现(onSetDefault / onRemove 回调)。
12
9
  */
13
10
 
14
11
  import * as React from 'react';
15
- import { useAddressBook, type Address } from './AddressBookProvider';
16
- import { useMounted } from './hooks/useMounted';
12
+
13
+ export interface Address {
14
+ id: string;
15
+ firstName?: string | null;
16
+ lastName?: string | null;
17
+ name?: string | null;
18
+ company?: string | null;
19
+ phone?: string | null;
20
+ address1?: string | null;
21
+ address2?: string | null;
22
+ city?: string | null;
23
+ province?: string | null;
24
+ provinceCode?: string | null;
25
+ district?: string | null;
26
+ country?: string | null;
27
+ countryCode?: string | null;
28
+ zip?: string | null;
29
+ isDefault?: boolean;
30
+ }
31
+
32
+ export interface AddressListItemActions {
33
+ setDefault: () => Promise<void> | void;
34
+ remove: () => Promise<void> | void;
35
+ edit?: () => void;
36
+ }
17
37
 
18
38
  export interface AddressListProps {
39
+ /** 地址列表(从 loader 传入) */
40
+ addresses?: Address[];
41
+ /** 设为默认地址回调 */
42
+ onSetDefault?: (addressId: string) => Promise<void> | void;
43
+ /** 删除地址回调 */
44
+ onRemove?: (addressId: string) => Promise<void> | void;
19
45
  /** 编辑按钮点击 */
20
46
  onEdit?: (address: Address) => void;
21
- /** "新增" 按钮点击。不传 = 不显示新增按钮(商家自己渲染外面) */
47
+ /** 新增按钮点击 */
22
48
  onAdd?: () => void;
23
49
  /** 空状态文案 */
24
50
  emptyText?: React.ReactNode;
25
- /** 渲染单个地址卡片的覆盖(高级用法) */
51
+ /** 渲染单个地址卡片的覆盖 */
26
52
  renderItem?: (address: Address, actions: AddressListItemActions) => React.ReactNode;
27
- /** 整体容器 className */
28
53
  className?: string;
29
- /** 加载中渲染 */
30
- loadingFallback?: React.ReactNode;
31
- }
32
-
33
- export interface AddressListItemActions {
34
- setDefault: () => Promise<void>;
35
- remove: () => Promise<void>;
36
- edit?: () => void;
37
54
  }
38
55
 
39
56
  export function AddressList(props: AddressListProps) {
40
57
  const {
41
- onEdit, onAdd, emptyText = '还没有保存的地址',
42
- renderItem, className, loadingFallback = null,
58
+ addresses = [],
59
+ onSetDefault, onRemove, onEdit, onAdd,
60
+ emptyText = '还没有保存的地址',
61
+ renderItem, className,
43
62
  } = props;
44
- const mounted = useMounted();
45
- const { addresses, status, setDefault, deleteAddress } = useAddressBook();
46
-
47
- if (!mounted) {
48
- return <div data-address-list data-ssr-placeholder className={className} />;
49
- }
50
- if (status === 'loading' || status === 'unauthenticated') {
51
- return <>{loadingFallback}</>;
52
- }
53
63
 
54
64
  if (addresses.length === 0) {
55
65
  return (
@@ -75,8 +85,8 @@ export function AddressList(props: AddressListProps) {
75
85
  <div data-items>
76
86
  {addresses.map((a) => {
77
87
  const actions: AddressListItemActions = {
78
- setDefault: async () => { await setDefault(a.id); },
79
- remove: async () => { await deleteAddress(a.id); },
88
+ setDefault: () => onSetDefault?.(a.id) as any,
89
+ remove: () => onRemove?.(a.id) as any,
80
90
  edit: onEdit ? () => onEdit(a) : undefined,
81
91
  };
82
92
  if (renderItem) return <React.Fragment key={a.id}>{renderItem(a, actions)}</React.Fragment>;
@@ -87,7 +97,6 @@ export function AddressList(props: AddressListProps) {
87
97
  );
88
98
  }
89
99
 
90
- // 默认渲染
91
100
  function DefaultAddressItem({ address: a, actions }: { address: Address; actions: AddressListItemActions }) {
92
101
  return (
93
102
  <div data-address-item data-default={a.isDefault ? '' : undefined}>
@@ -1,55 +1,52 @@
1
1
  /**
2
2
  * <AddressPicker>
3
3
  *
4
- * Checkout 用的地址选择器。**自带**:
5
- * - 从 useAddressBook() 拿 list
6
- * - radio 渲染 + 默认选 defaultAddress
7
- * - 选 "新地址" 时展开新增表单
4
+ * Checkout 用的地址选择器。
5
+ *
6
+ * helium 0.7:addresses 通过 props 传入。
8
7
  *
9
- * 用法(受控):
10
8
  * <AddressPicker
9
+ * addresses={addresses}
10
+ * defaultAddress={defaultAddress}
11
11
  * value={selectedAddressId}
12
12
  * onChange={(id, addr) => setSelectedAddressId(id)}
13
- * allowNewAddress
14
- * onUseNewAddress={(addr) => setInlineAddress(addr)}
15
13
  * />
16
- *
17
- * 用法(非受控):
18
- * <AddressPicker onSelect={(addr) => ...} />
19
- * 组件内部 state;选择后回调商家拿值。
20
14
  */
21
15
 
22
16
  import * as React from 'react';
23
- import { useAddressBook, type Address } from './AddressBookProvider';
24
- import { useMounted } from './hooks/useMounted';
17
+ import type { Address } from './AddressList';
25
18
  import { AddressForm } from './AddressForm';
26
19
 
27
20
  export interface AddressPickerProps {
21
+ /** 地址列表(从 loader 传入) */
22
+ addresses?: Address[];
23
+ /** 默认地址(用来自动选中) */
24
+ defaultAddress?: Address | null;
28
25
  /** 受控:当前选中的 address ID */
29
26
  value?: string | null;
30
- /** 受控:选中变化(同时拿到 address 对象) */
27
+ /** 受控:选中变化 */
31
28
  onChange?: (addressId: string | null, address: Address | null) => void;
32
- /** 非受控:选择时回调(不需要外面 state) */
29
+ /** 非受控:选择时回调 */
33
30
  onSelect?: (address: Address | null) => void;
34
31
  /** 是否允许"使用新地址"选项 */
35
32
  allowNewAddress?: boolean;
36
- /** 选了"新地址"后填表保存的回调(拿到新建好的 Address) */
33
+ /** 选了"新地址"后保存的回调 */
37
34
  onUseNewAddress?: (address: Address) => void;
38
- /** 空状态:没保存地址时显示什么。默认渲染内嵌 AddressForm */
35
+ /** 空状态 */
39
36
  emptyFallback?: React.ReactNode;
40
37
  className?: string;
41
38
  }
42
39
 
43
40
  export function AddressPicker(props: AddressPickerProps) {
44
41
  const {
42
+ addresses = [],
43
+ defaultAddress = null,
45
44
  value, onChange, onSelect,
46
45
  allowNewAddress = true,
47
46
  onUseNewAddress,
48
47
  emptyFallback,
49
48
  className,
50
49
  } = props;
51
- const mounted = useMounted();
52
- const { addresses, defaultAddress, status } = useAddressBook();
53
50
 
54
51
  // 非受控 fallback
55
52
  const [internalId, setInternalId] = React.useState<string | null>(null);
@@ -71,13 +68,6 @@ export function AddressPicker(props: AddressPickerProps) {
71
68
  }
72
69
  }, [defaultAddress, addresses, selectedId, setSelectedId]);
73
70
 
74
- if (!mounted) {
75
- return <div data-address-picker data-ssr-placeholder className={className} />;
76
- }
77
- if (status === 'loading') {
78
- return <div data-address-picker data-loading>加载地址中…</div>;
79
- }
80
-
81
71
  // 0 个地址:直接展示一个新增表单
82
72
  if (addresses.length === 0) {
83
73
  if (emptyFallback) return <>{emptyFallback}</>;
@@ -85,7 +75,7 @@ export function AddressPicker(props: AddressPickerProps) {
85
75
  <div data-address-picker data-empty className={className}>
86
76
  <div data-picker-title>填写收货地址</div>
87
77
  <AddressForm
88
- onSave={(addr) => {
78
+ onSaved={(addr) => {
89
79
  if (addr) {
90
80
  setSelectedId(addr.id, addr);
91
81
  onUseNewAddress?.(addr);
@@ -129,7 +119,7 @@ export function AddressPicker(props: AddressPickerProps) {
129
119
  />
130
120
  <div>
131
121
  <div data-name>+ 使用新地址</div>
132
- <div data-line>填写一个新地址(默认保存到地址簿)</div>
122
+ <div data-line>填写一个新地址</div>
133
123
  </div>
134
124
  </label>
135
125
  )}
@@ -142,7 +132,7 @@ export function AddressPicker(props: AddressPickerProps) {
142
132
  const fallback = defaultAddress || addresses[0];
143
133
  if (fallback) setSelectedId(fallback.id, fallback);
144
134
  }}
145
- onSave={(addr) => {
135
+ onSaved={(addr) => {
146
136
  if (addr) {
147
137
  setShowNewForm(false);
148
138
  setSelectedId(addr.id, addr);
@@ -27,7 +27,6 @@
27
27
  */
28
28
 
29
29
  import * as React from 'react';
30
- import { useCartOptional } from './CartProvider';
31
30
  import { useShopOptional } from './ShopProvider';
32
31
 
33
32
  export interface AnalyticsEvent {
@@ -51,14 +50,16 @@ export interface AnalyticsProviderProps {
51
50
  children: React.ReactNode;
52
51
  /** 接收所有事件的回调 */
53
52
  onEvent?: (event: AnalyticsEvent) => void;
54
- /** 是否自动监听 cart 变化触发 cart_updated 事件 */
55
- trackCart?: boolean;
53
+ /**
54
+ * 商家可以传入当前 cart(从 loader 拿)。
55
+ * 每次 cart 变化(id 或 totalQuantity 变)自动触发 cart_updated 事件。
56
+ */
57
+ cart?: any;
56
58
  }
57
59
 
58
60
  export function AnalyticsProvider(props: AnalyticsProviderProps) {
59
- const { children, onEvent, trackCart = true } = props;
61
+ const { children, onEvent, cart } = props;
60
62
  const shop = useShopOptional();
61
- const cartCtx = useCartOptional();
62
63
  const onEventRef = React.useRef(onEvent);
63
64
  React.useEffect(() => {
64
65
  onEventRef.current = onEvent;
@@ -76,17 +77,21 @@ export function AnalyticsProvider(props: AnalyticsProviderProps) {
76
77
  [shop],
77
78
  );
78
79
 
79
- // cart 自动追踪
80
+ // cart 自动追踪:通过 useEffect 比较 cart.id + totalQuantity 变化触发事件
81
+ const lastCartRef = React.useRef<{ id?: string; qty?: number } | null>(null);
80
82
  React.useEffect(() => {
81
- if (!trackCart || !cartCtx) return;
82
- return cartCtx.subscribe((c) => {
83
+ if (!cart) return;
84
+ const cur = { id: cart.id as string | undefined, qty: cart.totalQuantity as number | undefined };
85
+ const prev = lastCartRef.current;
86
+ if (!prev || prev.id !== cur.id || prev.qty !== cur.qty) {
87
+ lastCartRef.current = cur;
83
88
  emit('cart_updated', {
84
- cartId: c?.id,
85
- totalQuantity: c?.totalQuantity,
86
- totalAmount: c?.cost.totalAmount,
89
+ cartId: cart.id,
90
+ totalQuantity: cart.totalQuantity,
91
+ totalAmount: cart.cost?.totalAmount,
87
92
  });
88
- });
89
- }, [trackCart, cartCtx, emit]);
93
+ }
94
+ }, [cart, emit]);
90
95
 
91
96
  return <Ctx.Provider value={{ emit }}>{children}</Ctx.Provider>;
92
97
  }