@shopify/shop-minis-react 0.2.7 → 0.3.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 (38) hide show
  1. package/dist/_virtual/index4.js +2 -2
  2. package/dist/_virtual/index5.js +2 -3
  3. package/dist/_virtual/index5.js.map +1 -1
  4. package/dist/_virtual/index6.js +3 -2
  5. package/dist/_virtual/index6.js.map +1 -1
  6. package/dist/components/MinisContainer.js +10 -10
  7. package/dist/components/MinisContainer.js.map +1 -1
  8. package/dist/components/atoms/product-variant-price.js +36 -43
  9. package/dist/components/atoms/product-variant-price.js.map +1 -1
  10. package/dist/components/commerce/add-to-cart.js +70 -53
  11. package/dist/components/commerce/add-to-cart.js.map +1 -1
  12. package/dist/components/commerce/buy-now.js +75 -0
  13. package/dist/components/commerce/buy-now.js.map +1 -0
  14. package/dist/components/commerce/product-card.js +16 -17
  15. package/dist/components/commerce/product-card.js.map +1 -1
  16. package/dist/index.js +230 -230
  17. package/dist/{hooks/shop → internal}/useShopCartActions.js +2 -2
  18. package/dist/internal/useShopCartActions.js.map +1 -0
  19. package/dist/shop-minis-react/node_modules/.pnpm/@videojs_xhr@2.7.0/node_modules/@videojs/xhr/lib/index.js +1 -1
  20. package/dist/shop-minis-react/node_modules/.pnpm/mpd-parser@1.3.1/node_modules/mpd-parser/dist/mpd-parser.es.js +1 -1
  21. package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
  22. package/dist/shop-minis-react/node_modules/.pnpm/simple-swizzle@0.2.2/node_modules/simple-swizzle/index.js +1 -1
  23. package/dist/shop-minis-react/node_modules/.pnpm/use-sync-external-store@1.5.0_react@19.1.0/node_modules/use-sync-external-store/shim/index.js +1 -1
  24. package/generated-hook-maps/hook-actions-map.json +0 -4
  25. package/package.json +2 -2
  26. package/src/components/MinisContainer.tsx +5 -3
  27. package/src/components/atoms/product-variant-price.tsx +1 -5
  28. package/src/components/commerce/add-to-cart.test.tsx +218 -3
  29. package/src/components/commerce/add-to-cart.tsx +40 -16
  30. package/src/components/commerce/buy-now.test.tsx +272 -0
  31. package/src/components/commerce/buy-now.tsx +108 -0
  32. package/src/components/commerce/product-card.tsx +5 -6
  33. package/src/components/index.ts +1 -0
  34. package/src/hooks/index.ts +0 -1
  35. package/src/{hooks/shop → internal}/useShopCartActions.ts +2 -2
  36. package/src/stories/AddToCart.stories.tsx +75 -10
  37. package/src/stories/ProductVariantPrice.stories.tsx +1 -4
  38. package/dist/hooks/shop/useShopCartActions.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useShopCartActions.js","sources":["../../src/internal/useShopCartActions.ts"],"sourcesContent":["import {\n AddToCartParams,\n BuyProductParams,\n} from '@shopify/shop-minis-platform/actions'\n\nimport {useHandleAction} from './useHandleAction'\nimport {useShopActions} from './useShopActions'\n\ninterface UseShopCartActionsReturns {\n /**\n * Add a product to the cart\n */\n addToCart: (params: AddToCartParams) => Promise<void>\n /**\n * Buy a product directly\n */\n buyProduct: (params: BuyProductParams) => Promise<void>\n}\n\nexport const useShopCartActions = (): UseShopCartActionsReturns => {\n const {addToCart, buyProduct} = useShopActions()\n\n return {\n addToCart: useHandleAction(addToCart),\n buyProduct: useHandleAction(buyProduct),\n }\n}\n"],"names":["useShopCartActions","addToCart","buyProduct","useShopActions","useHandleAction"],"mappings":";;AAmBO,MAAMA,IAAqB,MAAiC;AACjE,QAAM,EAAC,WAAAC,GAAW,YAAAC,EAAU,IAAIC,EAAe;AAExC,SAAA;AAAA,IACL,WAAWC,EAAgBH,CAAS;AAAA,IACpC,YAAYG,EAAgBF,CAAU;AAAA,EACxC;AACF;"}
@@ -1,4 +1,4 @@
1
- import { __module as q } from "../../../../../../../../_virtual/index4.js";
1
+ import { __module as q } from "../../../../../../../../_virtual/index5.js";
2
2
  import { __require as F } from "../../../../../global@4.4.0/node_modules/global/window.js";
3
3
  import { __require as N } from "../../../../../@babel_runtime@7.27.6/node_modules/@babel/runtime/helpers/extends.js";
4
4
  import { __require as J } from "../../../../../is-function@1.0.2/node_modules/is-function/index.js";
@@ -2,7 +2,7 @@ import L from "../../../../@videojs_vhs-utils@4.1.1/node_modules/@videojs/vhs-ut
2
2
  import T from "../../../../../../../_virtual/window.js";
3
3
  import { forEachMediaGroup as Z } from "../../../../@videojs_vhs-utils@4.1.1/node_modules/@videojs/vhs-utils/es/media-groups.js";
4
4
  import J from "../../../../@videojs_vhs-utils@4.1.1/node_modules/@videojs/vhs-utils/es/decode-b64-to-uint8-array.js";
5
- import { l as Q } from "../../../../../../../_virtual/index5.js";
5
+ import { l as Q } from "../../../../../../../_virtual/index6.js";
6
6
  /*! @name mpd-parser @version 1.3.1 @license Apache-2.0 */
7
7
  const w = (e) => !!e && typeof e == "object", E = (...e) => e.reduce((n, t) => (typeof t != "object" || Object.keys(t).forEach((r) => {
8
8
  Array.isArray(n[r]) && Array.isArray(t[r]) ? n[r] = n[r].concat(t[r]) : w(n[r]) && w(t[r]) ? n[r] = E(n[r], t[r]) : n[r] = t[r];
@@ -1,4 +1,4 @@
1
- import { __exports as i } from "../../../../../../_virtual/index6.js";
1
+ import { __exports as i } from "../../../../../../_virtual/index4.js";
2
2
  var c;
3
3
  function d() {
4
4
  if (c) return i;
@@ -1,4 +1,4 @@
1
- import { __module as t } from "../../../../../../_virtual/index10.js";
1
+ import { __module as t } from "../../../../../../_virtual/index11.js";
2
2
  import { __require as z } from "../../../is-arrayish@0.3.2/node_modules/is-arrayish/index.js";
3
3
  var l;
4
4
  function v() {
@@ -1,4 +1,4 @@
1
- import { __module as r } from "../../../../../../../_virtual/index11.js";
1
+ import { __module as r } from "../../../../../../../_virtual/index10.js";
2
2
  import { __require as o } from "../cjs/use-sync-external-store-shim.production.js";
3
3
  import { __require as i } from "../cjs/use-sync-external-store-shim.development.js";
4
4
  var e;
@@ -116,10 +116,6 @@
116
116
  "useShop": [
117
117
  "GET_SHOP"
118
118
  ],
119
- "useShopCartActions": [
120
- "ADD_TO_CART",
121
- "BUY_PRODUCT"
122
- ],
123
119
  "useShopNavigation": [
124
120
  "NAVIGATE_TO_PRODUCT",
125
121
  "NAVIGATE_TO_SHOP",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shopify/shop-minis-react",
3
3
  "license": "SEE LICENSE IN LICENSE.txt",
4
- "version": "0.2.7",
4
+ "version": "0.3.0",
5
5
  "sideEffects": false,
6
6
  "type": "module",
7
7
  "engines": {
@@ -43,7 +43,7 @@
43
43
  "typescript": ">=5.0.0"
44
44
  },
45
45
  "dependencies": {
46
- "@shopify/shop-minis-platform": "0.7.0",
46
+ "@shopify/shop-minis-platform": "0.8.0",
47
47
  "@tailwindcss/vite": "4.1.8",
48
48
  "@types/color": "3.0.6",
49
49
  "@types/lodash": "4.17.20",
@@ -12,18 +12,20 @@ injectMocks()
12
12
 
13
13
  export function MinisContainer({children}: {children: React.ReactNode}) {
14
14
  const [isSDKReady, setIsSDKReady] = useState(false)
15
- const {reportError} = useShopActions()
15
+ const actions = useShopActions()
16
16
 
17
17
  const handleError = useCallback(
18
18
  async (params: ReportErrorParams) => {
19
19
  try {
20
- await reportError(params)
20
+ if (actions && actions.reportError) {
21
+ await actions.reportError(params)
22
+ }
21
23
  } catch (error) {
22
24
  // If reporting fails, at least log to console
23
25
  console.error('Failed to report error to app:', error)
24
26
  }
25
27
  },
26
- [reportError]
28
+ [actions]
27
29
  )
28
30
 
29
31
  useEffect(() => {
@@ -8,7 +8,6 @@ export interface ProductVariantPriceProps {
8
8
  compareAtPriceCurrencyCode?: string
9
9
  currentPriceClassName?: string
10
10
  originalPriceClassName?: string
11
- containerClassName?: string
12
11
  className?: string
13
12
  }
14
13
 
@@ -19,7 +18,6 @@ export function ProductVariantPrice({
19
18
  compareAtPriceCurrencyCode,
20
19
  currentPriceClassName,
21
20
  originalPriceClassName,
22
- containerClassName,
23
21
  className,
24
22
  }: ProductVariantPriceProps) {
25
23
  if (!amount || !currencyCode) {
@@ -34,9 +32,7 @@ export function ProductVariantPrice({
34
32
  : undefined
35
33
 
36
34
  return (
37
- <div
38
- className={cn('flex items-center gap-2', containerClassName, className)}
39
- >
35
+ <div className={cn('flex items-center gap-2', className)}>
40
36
  {hasDiscount ? (
41
37
  <>
42
38
  <span
@@ -1,20 +1,72 @@
1
+ import {Product} from '@shopify/shop-minis-platform'
1
2
  import {describe, expect, it, vi} from 'vitest'
2
3
 
3
- import {render, screen, mockMinisSDK, resetAllMocks} from '../../test-utils'
4
+ import {
5
+ render,
6
+ screen,
7
+ mockMinisSDK,
8
+ resetAllMocks,
9
+ userEvent,
10
+ } from '../../test-utils'
4
11
 
5
12
  import {AddToCartButton} from './add-to-cart'
6
13
 
7
14
  // Mock hooks
8
- vi.mock('../../hooks/shop/useShopCartActions', () => ({
15
+ vi.mock('../../internal/useShopCartActions', () => ({
9
16
  useShopCartActions: () => ({
10
17
  addToCart: mockMinisSDK.addToCart,
11
18
  buyProduct: mockMinisSDK.buyProduct,
12
19
  }),
13
20
  }))
14
21
 
22
+ vi.mock('../../hooks', () => ({
23
+ useShopNavigation: () => ({
24
+ navigateToProduct: mockMinisSDK.navigateToProduct,
25
+ }),
26
+ useErrorToast: () => ({
27
+ showErrorToast: vi.fn(),
28
+ }),
29
+ }))
30
+
15
31
  describe('AddToCartButton', () => {
32
+ const mockProduct: Product = {
33
+ id: 'gid://shopify/Product/123',
34
+ title: 'Test Product',
35
+ reviewAnalytics: {
36
+ averageRating: null,
37
+ reviewCount: null,
38
+ },
39
+ shop: {
40
+ id: 'gid://shopify/Shop/1',
41
+ name: 'Test Shop',
42
+ },
43
+ defaultVariantId: 'gid://shopify/ProductVariant/456',
44
+ isFavorited: false,
45
+ price: {
46
+ amount: '10.00',
47
+ currencyCode: 'USD',
48
+ },
49
+ variants: [
50
+ {
51
+ id: 'gid://shopify/ProductVariant/456',
52
+ title: 'Default',
53
+ isFavorited: false,
54
+ price: {
55
+ amount: '10.00',
56
+ currencyCode: 'USD',
57
+ },
58
+ image: {
59
+ url: 'https://example.com/variant-image.jpg',
60
+ altText: null,
61
+ width: null,
62
+ height: null,
63
+ },
64
+ },
65
+ ],
66
+ }
67
+
16
68
  const defaultProps = {
17
- productId: 'gid://shopify/Product/123',
69
+ product: mockProduct,
18
70
  productVariantId: 'gid://shopify/ProductVariant/456',
19
71
  }
20
72
 
@@ -70,4 +122,167 @@ describe('AddToCartButton', () => {
70
122
 
71
123
  expect(screen.getByRole('button')).toBeInTheDocument()
72
124
  })
125
+
126
+ it('calls addToCart when clicked and not a referral product', async () => {
127
+ const user = userEvent.setup()
128
+ mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
129
+
130
+ render(<AddToCartButton {...defaultProps} />)
131
+
132
+ const button = screen.getByRole('button')
133
+ await user.click(button)
134
+
135
+ expect(mockMinisSDK.addToCart).toHaveBeenCalledWith({
136
+ productId: mockProduct.id,
137
+ productVariantId: defaultProps.productVariantId,
138
+ quantity: 1,
139
+ discountCodes: undefined,
140
+ variantImageUrl: 'https://example.com/variant-image.jpg',
141
+ })
142
+ })
143
+
144
+ it('navigates to product page when product is referral', async () => {
145
+ const user = userEvent.setup()
146
+ const referralProduct: Product = {...mockProduct, referral: true}
147
+
148
+ render(<AddToCartButton {...defaultProps} product={referralProduct} />)
149
+
150
+ const button = screen.getByRole('button')
151
+ await user.click(button)
152
+
153
+ expect(mockMinisSDK.navigateToProduct).toHaveBeenCalledWith({
154
+ productId: referralProduct.id,
155
+ })
156
+ expect(mockMinisSDK.addToCart).not.toHaveBeenCalled()
157
+ })
158
+
159
+ it('shows success animation after adding to cart', async () => {
160
+ const user = userEvent.setup()
161
+ mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
162
+
163
+ render(<AddToCartButton {...defaultProps} />)
164
+
165
+ const button = screen.getByRole('button')
166
+ await user.click(button)
167
+
168
+ // Check for success state (Added to cart text)
169
+ await screen.findByText('Added to cart')
170
+ })
171
+
172
+ it('handles add to cart error gracefully', async () => {
173
+ const user = userEvent.setup()
174
+ mockMinisSDK.addToCart.mockRejectedValueOnce(new Error('Failed'))
175
+
176
+ render(<AddToCartButton {...defaultProps} />)
177
+
178
+ const button = screen.getByRole('button')
179
+ await user.click(button)
180
+
181
+ expect(mockMinisSDK.addToCart).toHaveBeenCalled()
182
+ })
183
+
184
+ it('does not call addToCart when disabled', async () => {
185
+ const user = userEvent.setup()
186
+
187
+ render(<AddToCartButton {...defaultProps} disabled />)
188
+
189
+ const button = screen.getByRole('button')
190
+ await user.click(button)
191
+
192
+ expect(mockMinisSDK.addToCart).not.toHaveBeenCalled()
193
+ })
194
+
195
+ it('handles product without variants array', async () => {
196
+ const user = userEvent.setup()
197
+ mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
198
+
199
+ const productWithoutVariants: Product = {
200
+ ...mockProduct,
201
+ variants: undefined,
202
+ }
203
+
204
+ render(
205
+ <AddToCartButton
206
+ product={productWithoutVariants}
207
+ productVariantId="gid://shopify/ProductVariant/456"
208
+ />
209
+ )
210
+
211
+ const button = screen.getByRole('button')
212
+ await user.click(button)
213
+
214
+ expect(mockMinisSDK.addToCart).toHaveBeenCalledWith({
215
+ productId: productWithoutVariants.id,
216
+ productVariantId: 'gid://shopify/ProductVariant/456',
217
+ quantity: 1,
218
+ discountCodes: undefined,
219
+ variantImageUrl: undefined,
220
+ })
221
+ })
222
+
223
+ it('handles product without matching variant in array', async () => {
224
+ const user = userEvent.setup()
225
+ mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
226
+
227
+ const productWithDifferentVariant: Product = {
228
+ ...mockProduct,
229
+ variants: [
230
+ {
231
+ id: 'gid://shopify/ProductVariant/999',
232
+ title: 'Different',
233
+ isFavorited: false,
234
+ price: {
235
+ amount: '15.00',
236
+ currencyCode: 'USD',
237
+ },
238
+ },
239
+ ],
240
+ }
241
+
242
+ render(
243
+ <AddToCartButton
244
+ product={productWithDifferentVariant}
245
+ productVariantId="gid://shopify/ProductVariant/456"
246
+ />
247
+ )
248
+
249
+ const button = screen.getByRole('button')
250
+ await user.click(button)
251
+
252
+ expect(mockMinisSDK.addToCart).toHaveBeenCalledWith({
253
+ productId: productWithDifferentVariant.id,
254
+ productVariantId: 'gid://shopify/ProductVariant/456',
255
+ quantity: 1,
256
+ discountCodes: undefined,
257
+ variantImageUrl: undefined,
258
+ })
259
+ })
260
+
261
+ it('handles product without shop data', async () => {
262
+ const user = userEvent.setup()
263
+ mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
264
+
265
+ const productWithoutShop: Product = {
266
+ ...mockProduct,
267
+ shop: undefined as any,
268
+ }
269
+
270
+ render(
271
+ <AddToCartButton
272
+ product={productWithoutShop}
273
+ productVariantId="gid://shopify/ProductVariant/456"
274
+ />
275
+ )
276
+
277
+ const button = screen.getByRole('button')
278
+ await user.click(button)
279
+
280
+ expect(mockMinisSDK.addToCart).toHaveBeenCalledWith({
281
+ productId: productWithoutShop.id,
282
+ productVariantId: 'gid://shopify/ProductVariant/456',
283
+ quantity: 1,
284
+ discountCodes: undefined,
285
+ variantImageUrl: 'https://example.com/variant-image.jpg',
286
+ })
287
+ })
73
288
  })
@@ -1,10 +1,12 @@
1
1
  import * as React from 'react'
2
2
  import {useState, useCallback} from 'react'
3
3
 
4
+ import {Product} from '@shopify/shop-minis-platform'
4
5
  import {CheckIcon} from 'lucide-react'
5
6
  import {motion, AnimatePresence} from 'motion/react'
6
7
 
7
- import {useErrorToast, useShopCartActions} from '../../hooks'
8
+ import {useErrorToast, useShopNavigation} from '../../hooks'
9
+ import {useShopCartActions} from '../../internal/useShopCartActions'
8
10
  import {cn} from '../../lib/utils'
9
11
  import {Button} from '../atoms/button'
10
12
 
@@ -16,42 +18,58 @@ interface AddToCartButtonProps {
16
18
  * The discount codes to apply to the cart.
17
19
  */
18
20
  discountCodes?: string[]
19
- /**
20
- * The GID of the product. E.g. `gid://shopify/Product/123`.
21
- */
22
- productId: string
23
21
  /**
24
22
  * The GID of the product variant. E.g. `gid://shopify/ProductVariant/456`.
25
23
  */
26
24
  productVariantId: string
25
+
26
+ /**
27
+ * The product to add to the cart.
28
+ */
29
+ product?: Product
27
30
  }
28
31
 
29
32
  export function AddToCartButton({
30
33
  disabled = false,
31
34
  className,
32
35
  size = 'default',
33
- productId,
34
36
  productVariantId,
35
37
  discountCodes,
38
+ product,
36
39
  }: AddToCartButtonProps) {
37
40
  const {addToCart} = useShopCartActions()
41
+ const {navigateToProduct} = useShopNavigation()
38
42
  const [isAdded, setIsAdded] = useState(false)
39
43
  const timeoutRef = React.useRef<number | undefined>(undefined)
44
+ const {id, referral, variants} = product ?? {}
45
+
46
+ const variantImageUrl = variants?.find(
47
+ variant => variant.id === productVariantId
48
+ )?.image?.url
40
49
 
41
50
  const {showErrorToast} = useErrorToast()
42
51
 
43
52
  const handleClick = useCallback(async () => {
44
- if (isAdded || disabled) return
53
+ if (disabled) return
54
+
55
+ if (id && referral) {
56
+ navigateToProduct({
57
+ productId: id,
58
+ })
59
+
60
+ return
61
+ }
62
+
63
+ if (isAdded) return
45
64
 
46
65
  try {
47
- // Call the callback if provided
48
- if (productId && productVariantId) {
49
- // Optimistic update with error toast
66
+ if (id && productVariantId) {
50
67
  addToCart({
51
- productId,
68
+ productId: id,
52
69
  productVariantId,
53
70
  quantity: 1,
54
71
  discountCodes,
72
+ variantImageUrl,
55
73
  })
56
74
  .then(() => {})
57
75
  .catch(() => {
@@ -77,12 +95,15 @@ export function AddToCartButton({
77
95
  console.error('Failed to add to cart:', error)
78
96
  }
79
97
  }, [
80
- isAdded,
81
98
  disabled,
82
- addToCart,
83
- productId,
99
+ id,
100
+ referral,
101
+ isAdded,
102
+ navigateToProduct,
84
103
  productVariantId,
104
+ addToCart,
85
105
  discountCodes,
106
+ variantImageUrl,
86
107
  showErrorToast,
87
108
  ])
88
109
 
@@ -95,6 +116,9 @@ export function AddToCartButton({
95
116
  }
96
117
  }, [])
97
118
 
119
+ const addToCartText = isAdded ? 'Added to cart' : 'Add to cart'
120
+ const buttonText = referral ? 'View product' : addToCartText
121
+
98
122
  return (
99
123
  <Button
100
124
  onClick={handleClick}
@@ -116,7 +140,7 @@ export function AddToCartButton({
116
140
  duration: 0.4,
117
141
  ease: [0.175, 0.885, 0.32, 1.275], // bounce effect
118
142
  }}
119
- className="absolute left-0"
143
+ className="absolute left-2"
120
144
  style={{x: -8}}
121
145
  >
122
146
  <CheckIcon className="size-4" />
@@ -124,7 +148,7 @@ export function AddToCartButton({
124
148
  )}
125
149
  </AnimatePresence>
126
150
  <span className={cn(isAdded && 'pl-5', 'transition-all duration-300')}>
127
- {isAdded ? 'Added to cart' : 'Add to cart'}
151
+ {buttonText}
128
152
  </span>
129
153
  </div>
130
154
  </Button>