@shopify/shop-minis-react 0.2.5 → 0.2.6

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.
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.5",
4
+ "version": "0.2.6",
5
5
  "sideEffects": false,
6
6
  "type": "module",
7
7
  "engines": {
@@ -147,6 +147,33 @@ describe('ProductCard', () => {
147
147
 
148
148
  expect(screen.getByText('No Image')).toBeInTheDocument()
149
149
  })
150
+
151
+ it('renders favorite button by default', () => {
152
+ const product = mockProduct()
153
+ render(<ProductCard product={product} />)
154
+
155
+ // Favorite button should be present
156
+ const favoriteButton = screen.getByRole('button')
157
+ expect(favoriteButton).toBeInTheDocument()
158
+ })
159
+
160
+ it('hides favorite button when favoriteButtonDisabled is true', () => {
161
+ const product = mockProduct()
162
+ render(<ProductCard product={product} favoriteButtonDisabled />)
163
+
164
+ // Favorite button should not be present
165
+ const favoriteButton = screen.queryByRole('button')
166
+ expect(favoriteButton).not.toBeInTheDocument()
167
+ })
168
+
169
+ it('shows favorite button when favoriteButtonDisabled is false', () => {
170
+ const product = mockProduct()
171
+ render(<ProductCard product={product} favoriteButtonDisabled={false} />)
172
+
173
+ // Favorite button should be present
174
+ const favoriteButton = screen.getByRole('button')
175
+ expect(favoriteButton).toBeInTheDocument()
176
+ })
150
177
  })
151
178
 
152
179
  describe('Interactions', () => {
@@ -262,6 +289,28 @@ describe('ProductCard', () => {
262
289
  expect(favoriteButton).toHaveClass('bg-button-overlay/30')
263
290
  })
264
291
  })
292
+
293
+ it('does not allow favorite toggle when favoriteButtonDisabled is true', async () => {
294
+ const product = mockProduct({isFavorited: false})
295
+ const onFavoriteToggled = vi.fn()
296
+
297
+ render(
298
+ <ProductCard
299
+ product={product}
300
+ favoriteButtonDisabled
301
+ onFavoriteToggled={onFavoriteToggled}
302
+ />
303
+ )
304
+
305
+ // Button should not exist at all
306
+ const favoriteButton = screen.queryByRole('button')
307
+ expect(favoriteButton).not.toBeInTheDocument()
308
+
309
+ // Callbacks should not be called
310
+ expect(onFavoriteToggled).not.toHaveBeenCalled()
311
+ expect(mockMinisSDK.saveProduct).not.toHaveBeenCalled()
312
+ expect(mockMinisSDK.unsaveProduct).not.toHaveBeenCalled()
313
+ })
265
314
  })
266
315
 
267
316
  describe('Custom Composition', () => {
@@ -307,6 +356,25 @@ describe('ProductCard', () => {
307
356
  expect(screen.getByText(product.title)).toBeInTheDocument()
308
357
  expect(screen.getByText('$99.99')).toBeInTheDocument()
309
358
  })
359
+
360
+ it('respects favoriteButtonDisabled in custom composition', () => {
361
+ const product = mockProduct()
362
+
363
+ render(
364
+ <ProductCard product={product} favoriteButtonDisabled>
365
+ <ProductCardContainer>
366
+ <ProductCardImageContainer>
367
+ <ProductCardImage />
368
+ <ProductCardFavoriteButton />
369
+ </ProductCardImageContainer>
370
+ </ProductCardContainer>
371
+ </ProductCard>
372
+ )
373
+
374
+ // Favorite button should not be rendered even when explicitly included
375
+ const favoriteButton = screen.queryByRole('button')
376
+ expect(favoriteButton).not.toBeInTheDocument()
377
+ })
310
378
  })
311
379
 
312
380
  describe('Edge Cases', () => {
@@ -28,6 +28,7 @@ interface ProductCardContextValue {
28
28
 
29
29
  // State
30
30
  isFavorited: boolean
31
+ isFavoriteButtonDisabled: boolean
31
32
 
32
33
  // Actions
33
34
  onClick: () => void
@@ -194,7 +195,10 @@ function ProductCardFavoriteButton({
194
195
  className,
195
196
  ...props
196
197
  }: React.ComponentProps<'div'>) {
197
- const {isFavorited, onFavoriteToggle} = useProductCardContext()
198
+ const {isFavorited, isFavoriteButtonDisabled, onFavoriteToggle} =
199
+ useProductCardContext()
200
+ if (isFavoriteButtonDisabled) return null
201
+
198
202
  return (
199
203
  <div className={cn('absolute bottom-3 right-3 z-10', className)} {...props}>
200
204
  <FavoriteButton onClick={onFavoriteToggle} filled={isFavorited} />
@@ -292,6 +296,8 @@ export interface ProductCardProps {
292
296
  onFavoriteToggled?: (isFavorited: boolean) => void
293
297
  /** Custom layout via children */
294
298
  children?: React.ReactNode
299
+ /** Whether the favorite button is disabled */
300
+ favoriteButtonDisabled?: boolean
295
301
  }
296
302
 
297
303
  function ProductCard({
@@ -304,6 +310,7 @@ function ProductCard({
304
310
  onProductClick,
305
311
  onFavoriteToggled,
306
312
  children,
313
+ favoriteButtonDisabled = false,
307
314
  }: ProductCardProps) {
308
315
  const {navigateToProduct} = useShopNavigation()
309
316
  const {saveProduct, unsaveProduct} = useSavedProductsActions()
@@ -374,7 +381,7 @@ function ProductCard({
374
381
 
375
382
  // State
376
383
  isFavorited: isFavoritedLocal,
377
-
384
+ isFavoriteButtonDisabled: favoriteButtonDisabled,
378
385
  // Actions
379
386
  onClick: handleClick,
380
387
  onFavoriteToggle: handleFavoriteClick,
@@ -389,6 +396,7 @@ function ProductCard({
389
396
  isFavoritedLocal,
390
397
  handleClick,
391
398
  handleFavoriteClick,
399
+ favoriteButtonDisabled,
392
400
  ]
393
401
  )
394
402
 
@@ -184,6 +184,53 @@ describe('ProductLink', () => {
184
184
  // Should not have favorite button
185
185
  expect(screen.queryByRole('button')).not.toBeInTheDocument()
186
186
  })
187
+
188
+ it('renders custom action when provided', () => {
189
+ const product = mockProduct()
190
+ const onCustomActionClick = vi.fn()
191
+ render(
192
+ <ProductLink
193
+ product={product}
194
+ customAction={
195
+ <button type="button" data-testid="custom-cta">
196
+ Add to Cart
197
+ </button>
198
+ }
199
+ onCustomActionClick={onCustomActionClick}
200
+ />
201
+ )
202
+
203
+ // Custom action should be rendered
204
+ expect(screen.getByTestId('custom-cta')).toBeInTheDocument()
205
+ expect(screen.getByText('Add to Cart')).toBeInTheDocument()
206
+
207
+ // Favorite button should not be rendered when custom action is present
208
+ // Check that there's only one button (the custom one)
209
+ const buttons = screen.getAllByRole('button')
210
+ expect(buttons).toHaveLength(1)
211
+ expect(buttons[0]).toHaveAttribute('data-testid', 'custom-cta')
212
+ })
213
+
214
+ it('renders custom action even when hideFavoriteAction is true', () => {
215
+ const product = mockProduct()
216
+ const onCustomActionClick = vi.fn()
217
+ render(
218
+ <ProductLink
219
+ product={product}
220
+ hideFavoriteAction
221
+ customAction={
222
+ <button type="button" data-testid="custom-action">
223
+ Buy Now
224
+ </button>
225
+ }
226
+ onCustomActionClick={onCustomActionClick}
227
+ />
228
+ )
229
+
230
+ // Custom action should still be rendered
231
+ expect(screen.getByTestId('custom-action')).toBeInTheDocument()
232
+ expect(screen.getByText('Buy Now')).toBeInTheDocument()
233
+ })
187
234
  })
188
235
 
189
236
  describe('Interactions', () => {
@@ -313,6 +360,40 @@ describe('ProductLink', () => {
313
360
  expect(onClick).not.toHaveBeenCalled()
314
361
  expect(mockMinisSDK.navigateToProduct).not.toHaveBeenCalled()
315
362
  })
363
+
364
+ it('calls onCustomActionClick when custom action is clicked', async () => {
365
+ const user = userEvent.setup()
366
+ const product = mockProduct()
367
+ const onClick = vi.fn()
368
+ const onCustomActionClick = vi.fn()
369
+
370
+ render(
371
+ <ProductLink
372
+ product={product}
373
+ onClick={onClick}
374
+ customAction={
375
+ <button type="button" data-testid="custom-btn">
376
+ Quick Add
377
+ </button>
378
+ }
379
+ onCustomActionClick={onCustomActionClick}
380
+ />
381
+ )
382
+
383
+ const customButton = screen.getByTestId('custom-btn')
384
+ await user.click(customButton)
385
+
386
+ // Should call custom action handler
387
+ expect(onCustomActionClick).toHaveBeenCalledTimes(1)
388
+
389
+ // Should NOT call favorite save/unsave
390
+ expect(mockMinisSDK.saveProduct).not.toHaveBeenCalled()
391
+ expect(mockMinisSDK.unsaveProduct).not.toHaveBeenCalled()
392
+
393
+ // Should NOT trigger product navigation
394
+ expect(onClick).not.toHaveBeenCalled()
395
+ expect(mockMinisSDK.navigateToProduct).not.toHaveBeenCalled()
396
+ })
316
397
  })
317
398
 
318
399
  describe('Star Rating Display', () => {
@@ -194,13 +194,21 @@ function ProductLinkRating({className, ...props}: React.ComponentProps<'div'>) {
194
194
 
195
195
  function ProductLinkActions({
196
196
  className,
197
+ hideFavoriteAction = false,
197
198
  onPress,
198
199
  filled = false,
200
+ customAction,
199
201
  ...props
200
202
  }: React.ComponentProps<typeof CardAction> & {
203
+ hideFavoriteAction?: boolean
201
204
  onPress?: () => void
202
205
  filled?: boolean
206
+ customAction?: React.ReactNode
203
207
  }) {
208
+ const favoriteAction = hideFavoriteAction ? null : (
209
+ <FavoriteButton filled={filled} onClick={onPress} />
210
+ )
211
+
204
212
  return (
205
213
  <CardAction
206
214
  className={cn('flex-shrink-0 self-center px-0 py-0', className)}
@@ -215,23 +223,34 @@ function ProductLinkActions({
215
223
  scale: {type: 'tween', duration: 0.08, ease: 'easeInOut'},
216
224
  }}
217
225
  >
218
- <FavoriteButton filled={filled} onClick={onPress} />
226
+ {customAction ? <>{customAction}</> : favoriteAction}
219
227
  </Touchable>
220
228
  </CardAction>
221
229
  )
222
230
  }
223
231
 
224
- export interface ProductLinkProps {
232
+ export type ProductLinkProps = {
225
233
  product: Product
226
234
  hideFavoriteAction?: boolean
227
235
  onClick?: (product: Product) => void
228
- }
236
+ } & (
237
+ | {
238
+ customAction?: never
239
+ onCustomActionClick?: never
240
+ }
241
+ | {
242
+ customAction: React.ReactNode
243
+ onCustomActionClick: () => void
244
+ }
245
+ )
229
246
 
230
247
  // Composed ProductLink component
231
248
  function ProductLink({
232
249
  product,
233
250
  hideFavoriteAction = false,
234
251
  onClick,
252
+ customAction,
253
+ onCustomActionClick,
235
254
  }: ProductLinkProps) {
236
255
  const {navigateToProduct} = useShopNavigation()
237
256
  const {saveProduct, unsaveProduct} = useSavedProductsActions()
@@ -272,6 +291,11 @@ function ProductLink({
272
291
  }, [navigateToProduct, id, onClick, product])
273
292
 
274
293
  const handleActionPress = React.useCallback(async () => {
294
+ if (customAction || onCustomActionClick) {
295
+ onCustomActionClick?.()
296
+ return
297
+ }
298
+
275
299
  const previousState = isFavoritedLocal
276
300
 
277
301
  // Optimistic update
@@ -296,13 +320,15 @@ function ProductLink({
296
320
  setIsFavoritedLocal(previousState)
297
321
  }
298
322
  }, [
323
+ customAction,
324
+ onCustomActionClick,
299
325
  isFavoritedLocal,
326
+ unsaveProduct,
300
327
  id,
301
328
  shop.id,
302
329
  selectedVariant?.id,
303
330
  defaultVariantId,
304
331
  saveProduct,
305
- unsaveProduct,
306
332
  ])
307
333
 
308
334
  return (
@@ -366,12 +392,12 @@ function ProductLink({
366
392
  </ProductLinkPrice>
367
393
  </ProductLinkInfo>
368
394
 
369
- {hideFavoriteAction ? null : (
370
- <ProductLinkActions
371
- filled={isFavoritedLocal}
372
- onPress={handleActionPress}
373
- />
374
- )}
395
+ <ProductLinkActions
396
+ filled={isFavoritedLocal}
397
+ onPress={handleActionPress}
398
+ customAction={customAction}
399
+ hideFavoriteAction={hideFavoriteAction}
400
+ />
375
401
  </ProductLinkRoot>
376
402
  )
377
403
  }