@shopify/shop-minis-react 0.0.24 → 0.0.26

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 (52) hide show
  1. package/dist/_virtual/index10.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/_virtual/index7.js +2 -2
  7. package/dist/_virtual/index9.js +2 -2
  8. package/dist/components/atoms/product-variant-price.js +61 -0
  9. package/dist/components/atoms/product-variant-price.js.map +1 -0
  10. package/dist/components/commerce/product-card.js +120 -153
  11. package/dist/components/commerce/product-card.js.map +1 -1
  12. package/dist/components/commerce/product-link.js +12 -16
  13. package/dist/components/commerce/product-link.js.map +1 -1
  14. package/dist/components/content/content-monitor.js +17 -0
  15. package/dist/components/content/content-monitor.js.map +1 -0
  16. package/dist/components/content/content-wrapper.js +17 -0
  17. package/dist/components/content/content-wrapper.js.map +1 -0
  18. package/dist/hooks/content/useContent.js +24 -0
  19. package/dist/hooks/content/useContent.js.map +1 -0
  20. package/dist/hooks/content/useCreateImageContent.js +40 -0
  21. package/dist/hooks/content/useCreateImageContent.js.map +1 -0
  22. package/dist/hooks/storage/useImageUpload.js +49 -40
  23. package/dist/hooks/storage/useImageUpload.js.map +1 -1
  24. package/dist/index.js +225 -217
  25. package/dist/index.js.map +1 -1
  26. package/dist/mocks.js +82 -50
  27. package/dist/mocks.js.map +1 -1
  28. package/dist/shop-minis-platform/src/types/content.js +5 -0
  29. package/dist/shop-minis-platform/src/types/content.js.map +1 -0
  30. package/dist/shop-minis-react/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.1.6_react@19.1.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.js +1 -1
  31. package/dist/shop-minis-react/node_modules/.pnpm/@videojs_xhr@2.7.0/node_modules/@videojs/xhr/lib/index.js +1 -1
  32. package/dist/shop-minis-react/node_modules/.pnpm/@xmldom_xmldom@0.8.10/node_modules/@xmldom/xmldom/lib/index.js +1 -1
  33. package/dist/shop-minis-react/node_modules/.pnpm/mpd-parser@1.3.1/node_modules/mpd-parser/dist/mpd-parser.es.js +1 -1
  34. 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
  35. package/dist/utils/imageToDataUri.js +10 -0
  36. package/dist/utils/imageToDataUri.js.map +1 -0
  37. package/package.json +5 -4
  38. package/src/components/atoms/product-variant-price.tsx +74 -0
  39. package/src/components/commerce/product-card.tsx +7 -56
  40. package/src/components/commerce/product-link.tsx +0 -2
  41. package/src/components/content/content-monitor.tsx +23 -0
  42. package/src/components/content/content-wrapper.tsx +56 -0
  43. package/src/components/index.ts +2 -0
  44. package/src/hooks/content/useContent.ts +50 -0
  45. package/src/hooks/content/useCreateImageContent.ts +80 -0
  46. package/src/hooks/index.ts +1 -0
  47. package/src/hooks/storage/useImageUpload.ts +27 -11
  48. package/src/mocks.ts +32 -0
  49. package/src/stories/ProductVariantPrice.stories.tsx +73 -0
  50. package/src/stories/Toaster.stories.tsx +2 -2
  51. package/src/utils/imageToDataUri.ts +8 -0
  52. package/src/utils/index.ts +1 -0
@@ -0,0 +1,23 @@
1
+ // import {useShopActions} from '../../internal/useShopActions'
2
+ import {Touchable} from '../atoms/touchable'
3
+
4
+ export function ContentMonitor({
5
+ // publicId,
6
+ children,
7
+ }: {
8
+ publicId: string
9
+ children: React.ReactNode
10
+ }) {
11
+ // const {showFeedbackSheet} = useShopActions()
12
+
13
+ return (
14
+ <Touchable
15
+ // TODO: Add long press support to Touchable
16
+ // onLongPress={() => {
17
+ // showFeedbackSheet({publicId})
18
+ // }}
19
+ >
20
+ {children}
21
+ </Touchable>
22
+ )
23
+ }
@@ -0,0 +1,56 @@
1
+ import {useContent} from '../../hooks/content/useContent'
2
+ import {Content} from '../../types'
3
+
4
+ import {ContentMonitor} from './content-monitor'
5
+
6
+ interface BaseContentWrapperProps {
7
+ children: ({
8
+ content,
9
+ loading,
10
+ }: {
11
+ content?: Content
12
+ loading: boolean
13
+ }) => JSX.Element | null
14
+ }
15
+
16
+ interface PublicIdContentWrapperProps extends BaseContentWrapperProps {
17
+ publicId: string
18
+ externalId?: never
19
+ }
20
+
21
+ interface ExternalIdContentWrapperProps extends BaseContentWrapperProps {
22
+ externalId: string
23
+ publicId?: never
24
+ }
25
+
26
+ type ContentWrapperProps =
27
+ | PublicIdContentWrapperProps
28
+ | ExternalIdContentWrapperProps
29
+
30
+ // It's too messy in the docs to show the complete types here so we show a simplified version
31
+ export interface ContentWrapperPropsForDocs extends BaseContentWrapperProps {
32
+ publicId?: string
33
+ externalId?: string
34
+ }
35
+
36
+ export function ContentWrapper({
37
+ publicId,
38
+ externalId,
39
+ children,
40
+ }: ContentWrapperProps) {
41
+ const {content, loading} = useContent({
42
+ identifiers: [{publicId, externalId}],
43
+ })
44
+
45
+ const contentItem = content?.[0]
46
+
47
+ if (loading || !contentItem) {
48
+ return children({loading})
49
+ }
50
+
51
+ return (
52
+ <ContentMonitor publicId={contentItem.publicId}>
53
+ {children({content: contentItem, loading})}
54
+ </ContentMonitor>
55
+ )
56
+ }
@@ -7,6 +7,8 @@ export * from './commerce/product-card-skeleton'
7
7
  export * from './commerce/merchant-card-skeleton'
8
8
  export * from './commerce/quantity-selector'
9
9
 
10
+ export * from './content/content-wrapper'
11
+
10
12
  export * from './navigation/transition-container'
11
13
  export * from './navigation/transition-link'
12
14
 
@@ -0,0 +1,50 @@
1
+ import {useMemo} from 'react'
2
+
3
+ import {useShopActions} from '../../internal/useShopActions'
4
+ import {useShopActionsDataFetching} from '../../internal/useShopActionsDataFetching'
5
+ import {
6
+ Content,
7
+ ContentIdentifierInput,
8
+ DataHookOptionsBase,
9
+ DataHookReturnsBase,
10
+ } from '../../types'
11
+
12
+ export interface UseContentParams extends DataHookOptionsBase {
13
+ /**
14
+ * The identifiers of the content to fetch.
15
+ */
16
+ identifiers: ContentIdentifierInput | ContentIdentifierInput[]
17
+ }
18
+
19
+ interface UseContentReturns extends DataHookReturnsBase {
20
+ /**
21
+ * The content returned from the query.
22
+ */
23
+ content: Content[] | null
24
+ }
25
+
26
+ export const useContent = (params: UseContentParams): UseContentReturns => {
27
+ const {getContent} = useShopActions()
28
+ const {identifiers, skip = false, ...restParams} = params
29
+
30
+ const {data, ...rest} = useShopActionsDataFetching(
31
+ getContent,
32
+ {
33
+ identifiers,
34
+ ...restParams,
35
+ },
36
+ {
37
+ skip,
38
+ hook: 'useContent',
39
+ }
40
+ )
41
+
42
+ const content = useMemo(() => {
43
+ return data ?? null
44
+ }, [data])
45
+
46
+ return {
47
+ ...rest,
48
+ content,
49
+ }
50
+ }
@@ -0,0 +1,80 @@
1
+ import {useCallback, useState} from 'react'
2
+
3
+ import {
4
+ ContentVisibility,
5
+ Content,
6
+ ContentCreateUserErrors,
7
+ } from '@shopify/shop-minis-platform'
8
+
9
+ import {useHandleAction} from '../../internal/useHandleAction'
10
+ import {useShopActions} from '../../internal/useShopActions'
11
+ import {fileToDataUri} from '../../utils'
12
+ import {useImageUpload} from '../storage/useImageUpload'
13
+
14
+ interface CreateImageContentParams {
15
+ image: File
16
+ contentTitle: string
17
+ visibility?: ContentVisibility[] | null
18
+ }
19
+
20
+ interface UseCreateImageContentReturns {
21
+ /**
22
+ * Upload an image and create content.
23
+ */
24
+ createImageContent: (
25
+ params: CreateImageContentParams
26
+ ) => Promise<{data: Content; userErrors?: ContentCreateUserErrors[]}>
27
+ /**
28
+ * Whether the content is being created.
29
+ */
30
+ loading: boolean
31
+ }
32
+
33
+ export const useCreateImageContent = (): UseCreateImageContentReturns => {
34
+ const {createContent} = useShopActions()
35
+ const {uploadImage} = useImageUpload()
36
+ const [loading, setLoading] = useState(false)
37
+
38
+ const createImageContent = useCallback(
39
+ async (params: CreateImageContentParams) => {
40
+ setLoading(true)
41
+
42
+ const {image, contentTitle, visibility} = params
43
+
44
+ if (!image.type) {
45
+ throw new Error('Unable to determine file type')
46
+ }
47
+ if (!image.type.startsWith('image/')) {
48
+ throw new Error('Invalid file type: must be an image')
49
+ }
50
+
51
+ const [uploadImageResult] = await uploadImage([
52
+ {
53
+ mimeType: image.type,
54
+ uri: await fileToDataUri(image),
55
+ },
56
+ ])
57
+ const uploadImageUrl = uploadImageResult.imageUrl
58
+
59
+ if (!uploadImageUrl) {
60
+ throw new Error('Image upload failed')
61
+ }
62
+
63
+ const createContentResult = await createContent({
64
+ title: contentTitle,
65
+ imageUrl: uploadImageUrl,
66
+ visibility,
67
+ })
68
+
69
+ setLoading(false)
70
+
71
+ return createContentResult
72
+ },
73
+ [createContent, uploadImage]
74
+ )
75
+
76
+ return {
77
+ createImageContent: useHandleAction(createImageContent),
78
+ loading,
79
+ }
80
+ }
@@ -39,6 +39,7 @@ export * from './shop/useShopCartActions'
39
39
  export * from './shop/useRecommendedShops'
40
40
 
41
41
  // - Content Hooks
42
+ export * from './content/useCreateImageContent'
42
43
 
43
44
  // - Utility Hooks
44
45
  export * from './util/useErrorToast'
@@ -12,7 +12,7 @@ export interface UploadImageParams {
12
12
  /**
13
13
  * The size of the image in bytes.
14
14
  */
15
- fileSize: number
15
+ fileSize?: number
16
16
  /**
17
17
  * The URI of the image to upload.
18
18
  */
@@ -41,16 +41,29 @@ interface UseImageUploadReturns {
41
41
  uploadImage: (params: UploadImageParams[]) => Promise<UploadedImage[]>
42
42
  }
43
43
 
44
+ // Fetch file data and detect file sizes if not provided
45
+ // Works with file://, data:, and http(s):// URIs
46
+ const processFileData = async (image: UploadImageParams) => {
47
+ const response = await fetch(image.uri)
48
+ const blob = await response.blob()
49
+
50
+ return {
51
+ ...image,
52
+ fileSize: image.fileSize ?? blob.size,
53
+ fileBlob: blob,
54
+ }
55
+ }
56
+
44
57
  const uploadFileToGCS = async (
45
- image: UploadImageParams,
58
+ image: UploadImageParams & {fileSize: number; fileBlob: Blob},
46
59
  target: UploadTarget
47
60
  ) => {
48
61
  const formData = new FormData()
49
62
  target.parameters.forEach(({name, value}: {name: string; value: string}) => {
50
63
  formData.append(name, value)
51
64
  })
52
- // Append the actual file data last
53
- formData.append('file', new Blob([image.uri], {type: image.mimeType}))
65
+
66
+ formData.append('file', image.fileBlob)
54
67
 
55
68
  const uploadResponse = await fetch(target.url, {
56
69
  method: 'POST',
@@ -76,11 +89,16 @@ export const useImageUpload = (): UseImageUploadReturns => {
76
89
  throw new Error('Multiple image upload is not supported yet')
77
90
  }
78
91
 
92
+ const imageParams = params[0]
93
+ const processedImageParams = await processFileData(imageParams)
94
+
79
95
  const links = await createImageUploadLink({
80
- input: params.map(image => ({
81
- mimeType: image.mimeType,
82
- fileSize: image.fileSize,
83
- })),
96
+ input: [
97
+ {
98
+ mimeType: processedImageParams.mimeType,
99
+ fileSize: processedImageParams.fileSize,
100
+ },
101
+ ],
84
102
  })
85
103
 
86
104
  if (!links.ok) {
@@ -88,9 +106,8 @@ export const useImageUpload = (): UseImageUploadReturns => {
88
106
  }
89
107
 
90
108
  // Upload single file to GCS
91
- // TODO: Upload multiple files to GCS
92
109
  const {error: uploadError} = await uploadFileToGCS(
93
- params[0],
110
+ processedImageParams,
94
111
  links?.data?.targets?.[0]!
95
112
  )
96
113
 
@@ -110,7 +127,6 @@ export const useImageUpload = (): UseImageUploadReturns => {
110
127
  throw new Error(result.error.message)
111
128
  }
112
129
 
113
- // TODO: Add support for multiple files
114
130
  if (result.data?.files?.[0]?.fileStatus === 'READY') {
115
131
  return [
116
132
  {
package/src/mocks.ts CHANGED
@@ -405,6 +405,38 @@ function makeMockActions(): ShopActions {
405
405
  pageInfo: createPagination(),
406
406
  },
407
407
  previewProductInAr: undefined,
408
+ createContent: {
409
+ data: {
410
+ publicId: 'content-123',
411
+ externalId: null,
412
+ image: {
413
+ id: 'img-123',
414
+ url: 'https://example.com/content-image.jpg',
415
+ width: 800,
416
+ height: 600,
417
+ },
418
+ title: 'Mock Content',
419
+ description: 'This is a mock content item',
420
+ visibility: ['DISCOVERABLE'],
421
+ shareableUrl: 'https://example.com/content/123',
422
+ products: null,
423
+ },
424
+ },
425
+ getContent: {
426
+ data: [
427
+ {
428
+ publicId: 'content-123',
429
+ image: {
430
+ id: 'img-123',
431
+ url: 'https://example.com/content-image.jpg',
432
+ width: 800,
433
+ height: 600,
434
+ },
435
+ title: 'Mock Content',
436
+ visibility: ['DISCOVERABLE'],
437
+ },
438
+ ],
439
+ },
408
440
  } as const
409
441
 
410
442
  const mock: Partial<ShopActions> = {}
@@ -0,0 +1,73 @@
1
+ import {ProductVariantPrice} from '../components/atoms/product-variant-price'
2
+
3
+ import type {Meta, StoryObj} from '@storybook/react-vite'
4
+
5
+ type ProductVariantPriceProps = React.ComponentProps<typeof ProductVariantPrice>
6
+
7
+ const meta = {
8
+ title: 'Atoms/ProductVariantPrice',
9
+ component: ProductVariantPrice,
10
+ parameters: {
11
+ layout: 'padded',
12
+ },
13
+ argTypes: {
14
+ amount: {
15
+ control: 'text',
16
+ },
17
+ currencyCode: {
18
+ control: 'select',
19
+ options: ['USD', 'CAD', 'EUR', 'GBP', 'JPY'],
20
+ },
21
+ compareAtPriceAmount: {
22
+ control: 'text',
23
+ },
24
+ compareAtPriceCurrencyCode: {
25
+ control: 'select',
26
+ options: ['USD', 'CAD', 'EUR', 'GBP', 'JPY'],
27
+ },
28
+ className: {
29
+ control: 'text',
30
+ },
31
+ currentPriceClassName: {
32
+ control: 'text',
33
+ },
34
+ originalPriceClassName: {
35
+ control: 'text',
36
+ },
37
+ containerClassName: {
38
+ control: 'text',
39
+ },
40
+ },
41
+ tags: ['autodocs'],
42
+ } satisfies Meta<ProductVariantPriceProps>
43
+
44
+ export default meta
45
+ type Story = StoryObj<typeof meta>
46
+
47
+ export const Default: Story = {
48
+ args: {
49
+ amount: '29.99',
50
+ currencyCode: 'USD',
51
+ },
52
+ }
53
+
54
+ export const WithDiscount: Story = {
55
+ name: 'With Discount',
56
+ args: {
57
+ amount: '24.99',
58
+ currencyCode: 'USD',
59
+ compareAtPriceAmount: '39.99',
60
+ },
61
+ }
62
+
63
+ export const CustomStyling: Story = {
64
+ name: 'Custom Styling',
65
+ args: {
66
+ amount: '89.99',
67
+ currencyCode: 'USD',
68
+ compareAtPriceAmount: '119.99',
69
+ currentPriceClassName: 'text-2xl font-bold text-green-600',
70
+ originalPriceClassName: 'text-lg text-red-500 line-through',
71
+ containerClassName: 'gap-3 p-4 bg-gray-50 rounded-lg',
72
+ },
73
+ }
@@ -26,7 +26,7 @@ type Story = StoryObj<typeof meta>
26
26
 
27
27
  export const SuccessToast: Story = {
28
28
  decorators: [
29
- Story => (
29
+ () => (
30
30
  <Button onClick={() => toast.success('Success toast!')}>
31
31
  Show success Toast
32
32
  </Button>
@@ -37,7 +37,7 @@ export const SuccessToast: Story = {
37
37
 
38
38
  export const ErrorToast: Story = {
39
39
  decorators: [
40
- Story => (
40
+ () => (
41
41
  <Button onClick={() => toast.error('Error toast!')}>
42
42
  Show error Toast
43
43
  </Button>
@@ -0,0 +1,8 @@
1
+ export function fileToDataUri(file: File): Promise<string> {
2
+ return new Promise((resolve, reject) => {
3
+ const reader = new FileReader()
4
+ reader.onloadend = () => resolve(reader.result as string)
5
+ reader.onerror = reject
6
+ reader.readAsDataURL(file)
7
+ })
8
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './errors'
2
2
  export * from './merchant-card'
3
3
  export * from './parseUrl'
4
+ export * from './imageToDataUri'