@shopify/shop-minis-react 0.13.2 → 0.15.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.
- package/dist/_virtual/index2.js +4 -4
- package/dist/_virtual/index3.js +2 -5
- package/dist/_virtual/index3.js.map +1 -1
- package/dist/_virtual/index4.js +2 -2
- package/dist/_virtual/index5.js +3 -2
- package/dist/_virtual/index5.js.map +1 -1
- package/dist/_virtual/index6.js +2 -2
- package/dist/_virtual/index7.js +2 -3
- package/dist/_virtual/index7.js.map +1 -1
- package/dist/_virtual/index8.js +2 -2
- package/dist/components/atoms/list.js.map +1 -1
- package/dist/components/commerce/product-link.js.map +1 -1
- package/dist/components/commerce/search.js.map +1 -1
- package/dist/components/navigation/transition-link.js.map +1 -1
- package/dist/hooks/intents/useIntent.js +43 -0
- package/dist/hooks/intents/useIntent.js.map +1 -0
- package/dist/index.js +45 -43
- package/dist/index.js.map +1 -1
- package/dist/internal/components/product-review-stars.js.map +1 -1
- 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
- package/dist/shop-minis-react/node_modules/.pnpm/@videojs_xhr@2.7.0/node_modules/@videojs/xhr/lib/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@xmldom_xmldom@0.8.10/node_modules/@xmldom/xmldom/lib/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/color-convert@3.1.3/node_modules/color-convert/conversions.js +422 -0
- package/dist/shop-minis-react/node_modules/.pnpm/color-convert@3.1.3/node_modules/color-convert/conversions.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/color-convert@3.1.3/node_modules/color-convert/index.js +36 -0
- package/dist/shop-minis-react/node_modules/.pnpm/color-convert@3.1.3/node_modules/color-convert/index.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/color-convert@3.1.3/node_modules/color-convert/route.js +47 -0
- package/dist/shop-minis-react/node_modules/.pnpm/color-convert@3.1.3/node_modules/color-convert/route.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/color-name@2.1.0/node_modules/color-name/index.js +156 -0
- package/dist/shop-minis-react/node_modules/.pnpm/color-name@2.1.0/node_modules/color-name/index.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/color-string@2.1.4/node_modules/color-string/index.js +106 -0
- package/dist/shop-minis-react/node_modules/.pnpm/color-string@2.1.4/node_modules/color-string/index.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/color@5.0.3/node_modules/color/index.js +263 -0
- package/dist/shop-minis-react/node_modules/.pnpm/color@5.0.3/node_modules/color/index.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/mpd-parser@1.3.1/node_modules/mpd-parser/dist/mpd-parser.es.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
- 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
- package/dist/shop-minis-react/node_modules/.pnpm/video.js@8.23.3/node_modules/video.js/dist/video.es.js +1 -1
- package/dist/utils/colors.js +1 -1
- package/package.json +3 -3
- package/src/components/atoms/list.tsx +4 -5
- package/src/components/commerce/product-link.tsx +2 -1
- package/src/components/commerce/search.tsx +6 -3
- package/src/components/navigation/transition-link.tsx +1 -2
- package/src/hooks/index.ts +3 -0
- package/src/hooks/intents/useIntent.test.ts +316 -0
- package/src/hooks/intents/useIntent.ts +111 -0
- package/src/internal/components/product-review-stars.tsx +1 -2
- package/dist/_virtual/index10.js +0 -5
- package/dist/_virtual/index10.js.map +0 -1
- package/dist/_virtual/index11.js +0 -5
- package/dist/_virtual/index11.js.map +0 -1
- package/dist/_virtual/index9.js +0 -5
- package/dist/_virtual/index9.js.map +0 -1
- package/dist/shop-minis-react/node_modules/.pnpm/color-convert@2.0.1/node_modules/color-convert/conversions.js +0 -308
- package/dist/shop-minis-react/node_modules/.pnpm/color-convert@2.0.1/node_modules/color-convert/conversions.js.map +0 -1
- package/dist/shop-minis-react/node_modules/.pnpm/color-convert@2.0.1/node_modules/color-convert/index.js +0 -41
- package/dist/shop-minis-react/node_modules/.pnpm/color-convert@2.0.1/node_modules/color-convert/index.js.map +0 -1
- package/dist/shop-minis-react/node_modules/.pnpm/color-convert@2.0.1/node_modules/color-convert/route.js +0 -53
- package/dist/shop-minis-react/node_modules/.pnpm/color-convert@2.0.1/node_modules/color-convert/route.js.map +0 -1
- package/dist/shop-minis-react/node_modules/.pnpm/color-name@1.1.4/node_modules/color-name/index.js +0 -157
- package/dist/shop-minis-react/node_modules/.pnpm/color-name@1.1.4/node_modules/color-name/index.js.map +0 -1
- package/dist/shop-minis-react/node_modules/.pnpm/color-string@1.9.1/node_modules/color-string/index.js +0 -103
- package/dist/shop-minis-react/node_modules/.pnpm/color-string@1.9.1/node_modules/color-string/index.js.map +0 -1
- package/dist/shop-minis-react/node_modules/.pnpm/color@4.2.3/node_modules/color/index.js +0 -269
- package/dist/shop-minis-react/node_modules/.pnpm/color@4.2.3/node_modules/color/index.js.map +0 -1
- package/dist/shop-minis-react/node_modules/.pnpm/is-arrayish@0.3.2/node_modules/is-arrayish/index.js +0 -10
- package/dist/shop-minis-react/node_modules/.pnpm/is-arrayish@0.3.2/node_modules/is-arrayish/index.js.map +0 -1
- package/dist/shop-minis-react/node_modules/.pnpm/simple-swizzle@0.2.2/node_modules/simple-swizzle/index.js +0 -23
- package/dist/shop-minis-react/node_modules/.pnpm/simple-swizzle@0.2.2/node_modules/simple-swizzle/index.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import S from "../../../../../../../_virtual/window.js";
|
|
2
2
|
import B from "../../../../../../../_virtual/document.js";
|
|
3
|
-
import il from "../../../../../../../_virtual/
|
|
3
|
+
import il from "../../../../../../../_virtual/index2.js";
|
|
4
4
|
import ao from "../../../../../../../_virtual/browser-index.js";
|
|
5
5
|
import xe from "../../../../@babel_runtime@7.27.6/node_modules/@babel/runtime/helpers/esm/extends.js";
|
|
6
6
|
import Kc from "../../../../@videojs_vhs-utils@4.1.1/node_modules/@videojs/vhs-utils/es/resolve-url.js";
|
package/dist/utils/colors.js
CHANGED
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.
|
|
4
|
+
"version": "0.15.0",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
"@shopify/shop-minis-platform": "0.13.0",
|
|
50
50
|
"@tailwindcss/vite": "4.1.8",
|
|
51
51
|
"@tanstack/react-query": "5.86.0",
|
|
52
|
-
"@types/color": "3.0.6",
|
|
53
52
|
"@types/lodash": "4.17.20",
|
|
54
53
|
"@types/react-window": "1.8.8",
|
|
55
54
|
"@types/url-parse": "1.4.9",
|
|
@@ -57,7 +56,7 @@
|
|
|
57
56
|
"@vitejs/plugin-react": "4.5.1",
|
|
58
57
|
"class-variance-authority": "0.7.1",
|
|
59
58
|
"clsx": "2.1.1",
|
|
60
|
-
"color": "
|
|
59
|
+
"color": "5.0.3",
|
|
61
60
|
"embla-carousel-react": "8.6.0",
|
|
62
61
|
"eslint": "8.57.0",
|
|
63
62
|
"eslint-plugin-compat": "6.0.2",
|
|
@@ -94,6 +93,7 @@
|
|
|
94
93
|
"@types/react-dom": "^19.1.2",
|
|
95
94
|
"@vitest/coverage-v8": "^2.1.8",
|
|
96
95
|
"@vitest/ui": "^2.1.8",
|
|
96
|
+
"eslint-formatter-codeframe": "^7.32.1",
|
|
97
97
|
"jsdom": "^25.0.1",
|
|
98
98
|
"react": "^19.1.0",
|
|
99
99
|
"react-dom": "^19.1.0",
|
|
@@ -35,11 +35,10 @@ export interface ListDocProps<T = any> {
|
|
|
35
35
|
enablePullToRefresh?: boolean
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export interface ListProps<T = any>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
> {
|
|
38
|
+
export interface ListProps<T = any> extends Omit<
|
|
39
|
+
VirtuosoProps<T, unknown>,
|
|
40
|
+
'data' | 'itemContent' | 'endReached'
|
|
41
|
+
> {
|
|
43
42
|
items: T[]
|
|
44
43
|
renderItem: (item: T, index: number) => React.ReactNode
|
|
45
44
|
showScrollbar?: boolean
|
|
@@ -35,7 +35,8 @@ const productLinkVariants = cva('', {
|
|
|
35
35
|
|
|
36
36
|
// Primitive components (building blocks)
|
|
37
37
|
export interface ProductLinkRootProps
|
|
38
|
-
extends
|
|
38
|
+
extends
|
|
39
|
+
React.ComponentProps<typeof Card>,
|
|
39
40
|
VariantProps<typeof productLinkVariants> {
|
|
40
41
|
layout?: 'horizontal' | 'vertical'
|
|
41
42
|
asChild?: boolean
|
|
@@ -207,10 +207,13 @@ function SearchResultsList({
|
|
|
207
207
|
)
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
interface SearchProviderPropsWithoutChildren
|
|
211
|
-
|
|
210
|
+
interface SearchProviderPropsWithoutChildren extends Omit<
|
|
211
|
+
SearchProviderProps,
|
|
212
|
+
'children'
|
|
213
|
+
> {}
|
|
212
214
|
export interface SearchResultsProps
|
|
213
|
-
extends
|
|
215
|
+
extends
|
|
216
|
+
SearchProviderPropsWithoutChildren,
|
|
214
217
|
SearchInputProps,
|
|
215
218
|
SearchResultsListProps {
|
|
216
219
|
showSearchInput?: boolean
|
|
@@ -13,8 +13,7 @@ export interface TransitionLinkDocProps {
|
|
|
13
13
|
children?: React.ReactNode
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export interface TransitionLinkProps
|
|
17
|
-
extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
16
|
+
export interface TransitionLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
18
17
|
to: string
|
|
19
18
|
}
|
|
20
19
|
|
package/src/hooks/index.ts
CHANGED
|
@@ -51,6 +51,9 @@ export * from './util/useImagePicker'
|
|
|
51
51
|
export * from './util/useKeyboardAvoidingView'
|
|
52
52
|
export * from './util/useRequestPermissions'
|
|
53
53
|
|
|
54
|
+
// - Intent Hooks
|
|
55
|
+
export * from './intents/useIntent'
|
|
56
|
+
|
|
54
57
|
// - Event Hooks
|
|
55
58
|
export * from './events/useOnMiniFocus'
|
|
56
59
|
export * from './events/useOnMiniBlur'
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import {renderHook} from '@testing-library/react'
|
|
2
|
+
import {describe, expect, it, vi, beforeEach} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {useIntent} from './useIntent'
|
|
5
|
+
|
|
6
|
+
vi.mock('../navigation/useDeeplink', () => ({
|
|
7
|
+
useDeeplink: vi.fn(),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
const {useDeeplink} = await import('../navigation/useDeeplink')
|
|
11
|
+
|
|
12
|
+
describe('useIntent', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.clearAllMocks()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('without intent params', () => {
|
|
18
|
+
it('returns nulls when no query params', () => {
|
|
19
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
20
|
+
path: '/',
|
|
21
|
+
queryParams: undefined,
|
|
22
|
+
hash: '',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const {result} = renderHook(() => useIntent())
|
|
26
|
+
|
|
27
|
+
expect(result.current).toEqual({query: null, data: null})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('returns nulls when intentQuery is missing', () => {
|
|
31
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
32
|
+
path: '/',
|
|
33
|
+
queryParams: {other: 'param'},
|
|
34
|
+
hash: '',
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const {result} = renderHook(() => useIntent())
|
|
38
|
+
|
|
39
|
+
expect(result.current).toEqual({query: null, data: null})
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('intent query parsing', () => {
|
|
44
|
+
it('parses action:type,value format', () => {
|
|
45
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
46
|
+
path: '/',
|
|
47
|
+
queryParams: {
|
|
48
|
+
intentQuery: 'try_on:shopify/Product,gid://shopify/Product/123',
|
|
49
|
+
},
|
|
50
|
+
hash: '',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const {result} = renderHook(() => useIntent())
|
|
54
|
+
|
|
55
|
+
expect(result.current.query).toEqual({
|
|
56
|
+
action: 'try_on',
|
|
57
|
+
type: 'shopify/Product',
|
|
58
|
+
value: 'gid://shopify/Product/123',
|
|
59
|
+
})
|
|
60
|
+
expect(result.current.data).toBeNull()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('parses action:type without value', () => {
|
|
64
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
65
|
+
path: '/',
|
|
66
|
+
queryParams: {intentQuery: 'create:shopify/Product'},
|
|
67
|
+
hash: '',
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const {result} = renderHook(() => useIntent())
|
|
71
|
+
|
|
72
|
+
expect(result.current.query).toEqual({
|
|
73
|
+
action: 'create',
|
|
74
|
+
type: 'shopify/Product',
|
|
75
|
+
value: null,
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('handles URL-encoded intentQuery', () => {
|
|
80
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
81
|
+
path: '/',
|
|
82
|
+
queryParams: {
|
|
83
|
+
intentQuery:
|
|
84
|
+
'try_on%3Ashopify%2FProduct%2Cgid%3A%2F%2Fshopify%2FProduct%2F123',
|
|
85
|
+
},
|
|
86
|
+
hash: '',
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const {result} = renderHook(() => useIntent())
|
|
90
|
+
|
|
91
|
+
expect(result.current.query).toEqual({
|
|
92
|
+
action: 'try_on',
|
|
93
|
+
type: 'shopify/Product',
|
|
94
|
+
value: 'gid://shopify/Product/123',
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('returns null query for invalid format without colon', () => {
|
|
99
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
100
|
+
path: '/',
|
|
101
|
+
queryParams: {intentQuery: 'invalid-no-colon'},
|
|
102
|
+
hash: '',
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const {result} = renderHook(() => useIntent())
|
|
106
|
+
|
|
107
|
+
expect(result.current).toEqual({query: null, data: null})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('returns null value for trailing comma', () => {
|
|
111
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
112
|
+
path: '/',
|
|
113
|
+
queryParams: {intentQuery: 'create:shopify/Product,'},
|
|
114
|
+
hash: '',
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const {result} = renderHook(() => useIntent())
|
|
118
|
+
|
|
119
|
+
expect(result.current.query).toEqual({
|
|
120
|
+
action: 'create',
|
|
121
|
+
type: 'shopify/Product',
|
|
122
|
+
value: null,
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('returns null query when type is empty', () => {
|
|
127
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
128
|
+
path: '/',
|
|
129
|
+
queryParams: {intentQuery: 'try_on:'},
|
|
130
|
+
hash: '',
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const {result} = renderHook(() => useIntent())
|
|
134
|
+
|
|
135
|
+
expect(result.current).toEqual({query: null, data: null})
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('shorthand GID syntax', () => {
|
|
140
|
+
it('infers type and value from full GID', () => {
|
|
141
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
142
|
+
path: '/',
|
|
143
|
+
queryParams: {
|
|
144
|
+
intentQuery: 'edit:gid://shopify/Product/123',
|
|
145
|
+
},
|
|
146
|
+
hash: '',
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const {result} = renderHook(() => useIntent())
|
|
150
|
+
|
|
151
|
+
expect(result.current.query).toEqual({
|
|
152
|
+
action: 'edit',
|
|
153
|
+
type: 'shopify/Product',
|
|
154
|
+
value: 'gid://shopify/Product/123',
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('infers type with null value from GID without ID', () => {
|
|
159
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
160
|
+
path: '/',
|
|
161
|
+
queryParams: {
|
|
162
|
+
intentQuery: 'create:gid://shopify/Product',
|
|
163
|
+
},
|
|
164
|
+
hash: '',
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const {result} = renderHook(() => useIntent())
|
|
168
|
+
|
|
169
|
+
expect(result.current.query).toEqual({
|
|
170
|
+
action: 'create',
|
|
171
|
+
type: 'shopify/Product',
|
|
172
|
+
value: null,
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('parses intentData alongside shorthand GID', () => {
|
|
177
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
178
|
+
path: '/',
|
|
179
|
+
queryParams: {
|
|
180
|
+
intentQuery: 'edit:gid://shopify/Product/123',
|
|
181
|
+
intentData: '{"title":"Updated"}',
|
|
182
|
+
},
|
|
183
|
+
hash: '',
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const {result} = renderHook(() => useIntent())
|
|
187
|
+
|
|
188
|
+
expect(result.current.query).toEqual({
|
|
189
|
+
action: 'edit',
|
|
190
|
+
type: 'shopify/Product',
|
|
191
|
+
value: 'gid://shopify/Product/123',
|
|
192
|
+
})
|
|
193
|
+
expect(result.current.data).toEqual({title: 'Updated'})
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('intent data parsing', () => {
|
|
198
|
+
it('parses JSON intentData', () => {
|
|
199
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
200
|
+
path: '/',
|
|
201
|
+
queryParams: {
|
|
202
|
+
intentQuery: 'try_on:shopify/Product,gid://shopify/Product/123',
|
|
203
|
+
intentData: '{"variantId":"gid://shopify/ProductVariant/456"}',
|
|
204
|
+
},
|
|
205
|
+
hash: '',
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const {result} = renderHook(() => useIntent())
|
|
209
|
+
|
|
210
|
+
expect(result.current.data).toEqual({
|
|
211
|
+
variantId: 'gid://shopify/ProductVariant/456',
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('parses URL-encoded intentData', () => {
|
|
216
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
217
|
+
path: '/',
|
|
218
|
+
queryParams: {
|
|
219
|
+
intentQuery: 'try_on:shopify/Product,gid://shopify/Product/123',
|
|
220
|
+
intentData: '%7B%22variantId%22%3A%22456%22%7D',
|
|
221
|
+
},
|
|
222
|
+
hash: '',
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const {result} = renderHook(() => useIntent())
|
|
226
|
+
|
|
227
|
+
expect(result.current.data).toEqual({variantId: '456'})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('preserves percent-encoded values inside already-decoded JSON', () => {
|
|
231
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
232
|
+
path: '/',
|
|
233
|
+
queryParams: {
|
|
234
|
+
intentQuery: 'edit:shopify/Product,gid://shopify/Product/123',
|
|
235
|
+
intentData: '{"redirect":"https%3A%2F%2Fexample.com"}',
|
|
236
|
+
},
|
|
237
|
+
hash: '',
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const {result} = renderHook(() => useIntent())
|
|
241
|
+
|
|
242
|
+
expect(result.current.data).toEqual({
|
|
243
|
+
redirect: 'https%3A%2F%2Fexample.com',
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('returns null data when intentData is a JSON array', () => {
|
|
248
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
249
|
+
path: '/',
|
|
250
|
+
queryParams: {
|
|
251
|
+
intentQuery: 'try_on:shopify/Product,gid://shopify/Product/123',
|
|
252
|
+
intentData: '[1,2,3]',
|
|
253
|
+
},
|
|
254
|
+
hash: '',
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const {result} = renderHook(() => useIntent())
|
|
258
|
+
|
|
259
|
+
expect(result.current.query).not.toBeNull()
|
|
260
|
+
expect(result.current.data).toBeNull()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('returns null data when intentData is a JSON primitive', () => {
|
|
264
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
265
|
+
path: '/',
|
|
266
|
+
queryParams: {
|
|
267
|
+
intentQuery: 'try_on:shopify/Product,gid://shopify/Product/123',
|
|
268
|
+
intentData: '"just a string"',
|
|
269
|
+
},
|
|
270
|
+
hash: '',
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const {result} = renderHook(() => useIntent())
|
|
274
|
+
|
|
275
|
+
expect(result.current.query).not.toBeNull()
|
|
276
|
+
expect(result.current.data).toBeNull()
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('returns null data for malformed JSON', () => {
|
|
280
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
281
|
+
path: '/',
|
|
282
|
+
queryParams: {
|
|
283
|
+
intentQuery: 'try_on:shopify/Product,gid://shopify/Product/123',
|
|
284
|
+
intentData: 'not-valid-json',
|
|
285
|
+
},
|
|
286
|
+
hash: '',
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
const {result} = renderHook(() => useIntent())
|
|
290
|
+
|
|
291
|
+
expect(result.current.query).not.toBeNull()
|
|
292
|
+
expect(result.current.data).toBeNull()
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
describe('memoization', () => {
|
|
297
|
+
it('returns same reference when queryParams do not change', () => {
|
|
298
|
+
const queryParams = {
|
|
299
|
+
intentQuery: 'try_on:shopify/Product,gid://shopify/Product/123',
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
vi.mocked(useDeeplink).mockReturnValue({
|
|
303
|
+
path: '/',
|
|
304
|
+
queryParams,
|
|
305
|
+
hash: '',
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
const {result, rerender} = renderHook(() => useIntent())
|
|
309
|
+
const firstResult = result.current
|
|
310
|
+
|
|
311
|
+
rerender()
|
|
312
|
+
|
|
313
|
+
expect(result.current).toBe(firstResult)
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {useMemo} from 'react'
|
|
2
|
+
|
|
3
|
+
import {useDeeplink} from '../navigation/useDeeplink'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Structured description of an intent, following the Shopify Intents API
|
|
7
|
+
* URI format: `action:type,value`
|
|
8
|
+
*
|
|
9
|
+
* @see https://shopify.dev/docs/api/admin-extensions/latest/target-apis/utility-apis/intents-api
|
|
10
|
+
*/
|
|
11
|
+
export interface IntentQuery {
|
|
12
|
+
/** Verb describing the operation (e.g., 'try_on', 'create', 'edit') */
|
|
13
|
+
action: string
|
|
14
|
+
/** Resource type identifier (e.g., 'shopify/Product', 'shop/UserImage') */
|
|
15
|
+
type: string
|
|
16
|
+
/** Resource GID (e.g., 'gid://shopify/Product/123') if applicable */
|
|
17
|
+
value: string | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UseIntentReturn {
|
|
21
|
+
/** Parsed intent query, or null if no intent was passed */
|
|
22
|
+
query: IntentQuery | null
|
|
23
|
+
/** Additional JSON data passed with the intent, or null */
|
|
24
|
+
data: {[key: string]: unknown} | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses an intent passed to this mini via deeplink.
|
|
29
|
+
*
|
|
30
|
+
* Follows the Shopify Intents API URI format (`action:type,value`) with
|
|
31
|
+
* an optional JSON data payload passed separately.
|
|
32
|
+
*
|
|
33
|
+
* @see https://shopify.dev/docs/api/admin-extensions/latest/target-apis/utility-apis/intents-api
|
|
34
|
+
*
|
|
35
|
+
* Deeplink format:
|
|
36
|
+
* ?intentQuery=action:type,value&intentData={"key":"value"}
|
|
37
|
+
*
|
|
38
|
+
* Examples:
|
|
39
|
+
* ?intentQuery=try_on:shopify/Product,gid://shopify/Product/123
|
|
40
|
+
* ?intentQuery=create:shopify/Product
|
|
41
|
+
* ?intentQuery=edit:shopify/Product,gid://shopify/Product/123&intentData={"variantId":"456"}
|
|
42
|
+
*
|
|
43
|
+
* Shorthand GID syntax (type inferred from GID):
|
|
44
|
+
* ?intentQuery=edit:gid://shopify/Product/123
|
|
45
|
+
* ?intentQuery=create:gid://shopify/Product
|
|
46
|
+
*
|
|
47
|
+
* Use this hook to receive intents from the host app (Host → Mini direction).
|
|
48
|
+
*/
|
|
49
|
+
export function useIntent(): UseIntentReturn {
|
|
50
|
+
const {queryParams} = useDeeplink()
|
|
51
|
+
|
|
52
|
+
return useMemo(() => {
|
|
53
|
+
const raw = queryParams?.intentQuery
|
|
54
|
+
|
|
55
|
+
if (!raw) return {query: null, data: null}
|
|
56
|
+
|
|
57
|
+
let decoded: string
|
|
58
|
+
try {
|
|
59
|
+
decoded = decodeURIComponent(raw)
|
|
60
|
+
} catch {
|
|
61
|
+
decoded = raw
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const colonIdx = decoded.indexOf(':')
|
|
65
|
+
if (colonIdx === -1) return {query: null, data: null}
|
|
66
|
+
|
|
67
|
+
const action = decoded.slice(0, colonIdx)
|
|
68
|
+
const rest = decoded.slice(colonIdx + 1)
|
|
69
|
+
const commaIdx = rest.indexOf(',')
|
|
70
|
+
|
|
71
|
+
let type = commaIdx === -1 ? rest : rest.slice(0, commaIdx)
|
|
72
|
+
let value = commaIdx === -1 ? null : rest.slice(commaIdx + 1) || null
|
|
73
|
+
|
|
74
|
+
// Shorthand GID syntax: edit:gid://shopify/Product/123
|
|
75
|
+
// Infer type from GID and use full GID as value
|
|
76
|
+
const gidMatch = type.match(/^gid:\/\/shopify\/(\w+)(?:\/(.+))?$/)
|
|
77
|
+
if (gidMatch) {
|
|
78
|
+
type = `shopify/${gidMatch[1]}`
|
|
79
|
+
value = gidMatch[2] ? `gid://shopify/${gidMatch[1]}/${gidMatch[2]}` : null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!type) return {query: null, data: null}
|
|
83
|
+
|
|
84
|
+
let data: {[key: string]: unknown} | null = null
|
|
85
|
+
const rawData = queryParams?.intentData
|
|
86
|
+
if (rawData) {
|
|
87
|
+
let parsed: unknown
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(rawData)
|
|
90
|
+
} catch {
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(decodeURIComponent(rawData))
|
|
93
|
+
} catch {
|
|
94
|
+
// malformed JSON — ignore
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (
|
|
98
|
+
parsed !== null &&
|
|
99
|
+
typeof parsed === 'object' &&
|
|
100
|
+
!Array.isArray(parsed)
|
|
101
|
+
) {
|
|
102
|
+
data = parsed as {[key: string]: unknown}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
query: {action, type, value},
|
|
108
|
+
data,
|
|
109
|
+
}
|
|
110
|
+
}, [queryParams])
|
|
111
|
+
}
|
|
@@ -17,8 +17,7 @@ const textSizeClasses = {
|
|
|
17
17
|
lg: 'text-base',
|
|
18
18
|
} as const
|
|
19
19
|
|
|
20
|
-
export interface ProductReviewStarsProps
|
|
21
|
-
extends React.HTMLAttributes<HTMLDivElement> {
|
|
20
|
+
export interface ProductReviewStarsProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
22
21
|
/**
|
|
23
22
|
* Average rating value (0-5)
|
|
24
23
|
*/
|
package/dist/_virtual/index10.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index10.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
package/dist/_virtual/index11.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index11.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
package/dist/_virtual/index9.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index9.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|