@shopify/shop-minis-react 0.16.1 → 0.17.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/eslint/config.cjs +35 -1
- package/eslint/index.cjs +0 -2
- package/package.json +2 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/intents/useUserImagePicker.ts +36 -0
- package/src/internal/useShopIntent.ts +46 -0
- package/src/mocks.ts +1 -0
- package/eslint/rules/prefer-sdk-hooks.cjs +0 -129
package/eslint/config.cjs
CHANGED
|
@@ -45,7 +45,6 @@ module.exports = {
|
|
|
45
45
|
'shop-minis/no-javascript-files': 'error',
|
|
46
46
|
'shop-minis/no-secrets': ['error'],
|
|
47
47
|
'shop-minis/prefer-sdk-components': 'warn',
|
|
48
|
-
'shop-minis/prefer-sdk-hooks': 'warn',
|
|
49
48
|
'shop-minis/validate-manifest': 'error',
|
|
50
49
|
|
|
51
50
|
// Unsafe code execution (built-in ESLint rules)
|
|
@@ -62,6 +61,16 @@ module.exports = {
|
|
|
62
61
|
message:
|
|
63
62
|
'WebAssembly is not supported in the Shop Minis environment. Consider using alternative JavaScript implementations.',
|
|
64
63
|
},
|
|
64
|
+
{
|
|
65
|
+
name: 'localStorage',
|
|
66
|
+
message:
|
|
67
|
+
'localStorage is not available in the Shop Minis environment. Use useAsyncStorage (or useSecureStorage for sensitive data) from @shopify/shop-minis-react instead.',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'sessionStorage',
|
|
71
|
+
message:
|
|
72
|
+
'sessionStorage is not available in the Shop Minis environment. Use useAsyncStorage (or useSecureStorage for sensitive data) from @shopify/shop-minis-react instead.',
|
|
73
|
+
},
|
|
65
74
|
],
|
|
66
75
|
'no-restricted-syntax': [
|
|
67
76
|
'error',
|
|
@@ -108,6 +117,31 @@ module.exports = {
|
|
|
108
117
|
message:
|
|
109
118
|
'navigator.share is not available in the Shop Minis environment. Use the SDK share functionality instead.',
|
|
110
119
|
},
|
|
120
|
+
// localStorage / sessionStorage access via window or globalThis
|
|
121
|
+
{
|
|
122
|
+
selector:
|
|
123
|
+
"MemberExpression[object.name='window'][property.name='localStorage']",
|
|
124
|
+
message:
|
|
125
|
+
'localStorage is not available in the Shop Minis environment. Use useAsyncStorage (or useSecureStorage for sensitive data) from @shopify/shop-minis-react instead.',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
selector:
|
|
129
|
+
"MemberExpression[object.name='window'][property.name='sessionStorage']",
|
|
130
|
+
message:
|
|
131
|
+
'sessionStorage is not available in the Shop Minis environment. Use useAsyncStorage (or useSecureStorage for sensitive data) from @shopify/shop-minis-react instead.',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
selector:
|
|
135
|
+
"MemberExpression[object.name='globalThis'][property.name='localStorage']",
|
|
136
|
+
message:
|
|
137
|
+
'localStorage is not available in the Shop Minis environment. Use useAsyncStorage (or useSecureStorage for sensitive data) from @shopify/shop-minis-react instead.',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
selector:
|
|
141
|
+
"MemberExpression[object.name='globalThis'][property.name='sessionStorage']",
|
|
142
|
+
message:
|
|
143
|
+
'sessionStorage is not available in the Shop Minis environment. Use useAsyncStorage (or useSecureStorage for sensitive data) from @shopify/shop-minis-react instead.',
|
|
144
|
+
},
|
|
111
145
|
],
|
|
112
146
|
'compat/compat': 'error',
|
|
113
147
|
},
|
package/eslint/index.cjs
CHANGED
|
@@ -11,7 +11,6 @@ const noInternalImports = require('./rules/no-internal-imports.cjs')
|
|
|
11
11
|
const noJavaScriptFiles = require('./rules/no-javascript-files.cjs')
|
|
12
12
|
const noSecrets = require('./rules/no-secrets.cjs')
|
|
13
13
|
const preferSdkComponents = require('./rules/prefer-sdk-components.cjs')
|
|
14
|
-
const preferSdkHooks = require('./rules/prefer-sdk-hooks.cjs')
|
|
15
14
|
const validateManifest = require('./rules/validate-manifest.cjs')
|
|
16
15
|
|
|
17
16
|
module.exports = {
|
|
@@ -23,7 +22,6 @@ module.exports = {
|
|
|
23
22
|
'no-javascript-files': noJavaScriptFiles,
|
|
24
23
|
'no-secrets': noSecrets,
|
|
25
24
|
'prefer-sdk-components': preferSdkComponents,
|
|
26
|
-
'prefer-sdk-hooks': preferSdkHooks,
|
|
27
25
|
'validate-manifest': validateManifest,
|
|
28
26
|
},
|
|
29
27
|
}
|
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.17.0",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"typescript": ">=5.0.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@shopify/shop-minis-platform": "0.
|
|
48
|
+
"@shopify/shop-minis-platform": "0.15.0",
|
|
49
49
|
"@tailwindcss/vite": "4.1.8",
|
|
50
50
|
"@tanstack/react-query": "5.86.0",
|
|
51
51
|
"@types/lodash": "4.17.20",
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {useCallback} from 'react'
|
|
2
|
+
|
|
3
|
+
import {useShopIntent} from '../../internal/useShopIntent'
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
CreateUserImageParams,
|
|
7
|
+
CreateUserImageResponse,
|
|
8
|
+
IntentResult,
|
|
9
|
+
} from '@shopify/shop-minis-platform/actions'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hook for minis to create a user image via the host app's
|
|
13
|
+
* camera or photo library.
|
|
14
|
+
*/
|
|
15
|
+
export function useUserImagePicker() {
|
|
16
|
+
const {invokeQuery} = useShopIntent()
|
|
17
|
+
|
|
18
|
+
const pickImage = useCallback(
|
|
19
|
+
async (
|
|
20
|
+
options: CreateUserImageParams
|
|
21
|
+
): Promise<IntentResult<CreateUserImageResponse>> => {
|
|
22
|
+
const result = await invokeQuery({
|
|
23
|
+
action: 'create',
|
|
24
|
+
type: 'shop/UserImage',
|
|
25
|
+
data: options,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// The bridge returns untyped data; the host handler guarantees
|
|
29
|
+
// the shape matches CreateUserImageResponse for this action+type.
|
|
30
|
+
return result as IntentResult<CreateUserImageResponse>
|
|
31
|
+
},
|
|
32
|
+
[invokeQuery]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return {pickImage}
|
|
36
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {useCallback} from 'react'
|
|
2
|
+
|
|
3
|
+
import {useShopActions} from './useShopActions'
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
IntentQuery,
|
|
7
|
+
InvokeIntentResponse,
|
|
8
|
+
} from '@shopify/shop-minis-platform/actions'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Internal hook that bridges minis to the host intent system
|
|
12
|
+
* via the INVOKE_INTENT shop action.
|
|
13
|
+
*/
|
|
14
|
+
export function useShopIntent() {
|
|
15
|
+
const {invokeIntent} = useShopActions()
|
|
16
|
+
|
|
17
|
+
const invokeQuery = useCallback(
|
|
18
|
+
async (query: IntentQuery): Promise<InvokeIntentResponse> => {
|
|
19
|
+
// The WebView bridge rejects on `*_ERROR` / `ERROR` responses, so any
|
|
20
|
+
// host-side failure (unsupported action on older hosts, middleware
|
|
21
|
+
// exceptions, transport errors) arrives here as a thrown Error rather
|
|
22
|
+
// than a resolved `{ok: false, ...}`. Catch it so `invokeQuery` always
|
|
23
|
+
// honors the declared `Promise<IntentResult>` contract.
|
|
24
|
+
try {
|
|
25
|
+
const result = await invokeIntent(query)
|
|
26
|
+
|
|
27
|
+
if (!result.ok) {
|
|
28
|
+
return {
|
|
29
|
+
code: 'error',
|
|
30
|
+
message: result.error?.message ?? 'Unknown error',
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return result.data
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return {
|
|
37
|
+
code: 'error',
|
|
38
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
[invokeIntent]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return {invokeQuery}
|
|
46
|
+
}
|
package/src/mocks.ts
CHANGED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule to prefer SDK hooks over native browser APIs
|
|
3
|
-
* @fileoverview Enforce using Shop Minis SDK hooks instead of native browser APIs
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
module.exports = {
|
|
7
|
-
meta: {
|
|
8
|
-
type: 'suggestion',
|
|
9
|
-
docs: {
|
|
10
|
-
description:
|
|
11
|
-
'Prefer Shop Minis SDK hooks over native browser APIs for better compatibility and functionality',
|
|
12
|
-
category: 'Best Practices',
|
|
13
|
-
recommended: true,
|
|
14
|
-
},
|
|
15
|
-
messages: {
|
|
16
|
-
preferAsyncStorage:
|
|
17
|
-
'Use useAsyncStorage (or useSecureStorage for sensitive data) from @shopify/shop-minis-react instead of localStorage. The SDK hook provides async storage that works reliably in the Shop mini-app environment.',
|
|
18
|
-
},
|
|
19
|
-
schema: [
|
|
20
|
-
{
|
|
21
|
-
type: 'object',
|
|
22
|
-
properties: {
|
|
23
|
-
apis: {
|
|
24
|
-
type: 'object',
|
|
25
|
-
description: 'Map of native APIs to SDK hooks',
|
|
26
|
-
additionalProperties: {
|
|
27
|
-
type: 'string',
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
additionalProperties: false,
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
},
|
|
35
|
-
|
|
36
|
-
create(context) {
|
|
37
|
-
// Default API mappings
|
|
38
|
-
const defaultApis = {
|
|
39
|
-
localStorage: 'useAsyncStorage',
|
|
40
|
-
sessionStorage: 'useAsyncStorage',
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Get user configuration or use defaults
|
|
44
|
-
const options = context.options[0] || {}
|
|
45
|
-
const apiMap = {
|
|
46
|
-
...defaultApis,
|
|
47
|
-
...(options.apis || {}),
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
MemberExpression(node) {
|
|
52
|
-
// Check for direct access: localStorage.getItem()
|
|
53
|
-
if (
|
|
54
|
-
node.object.type === 'Identifier' &&
|
|
55
|
-
Object.hasOwn(apiMap, node.object.name)
|
|
56
|
-
) {
|
|
57
|
-
const apiName = node.object.name
|
|
58
|
-
const sdkHook = apiMap[apiName]
|
|
59
|
-
|
|
60
|
-
context.report({
|
|
61
|
-
node: node.object,
|
|
62
|
-
messageId: 'preferAsyncStorage',
|
|
63
|
-
data: {
|
|
64
|
-
nativeApi: apiName,
|
|
65
|
-
sdkHook,
|
|
66
|
-
},
|
|
67
|
-
})
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Check for global access: window.localStorage or globalThis.localStorage
|
|
72
|
-
if (
|
|
73
|
-
node.object.type === 'MemberExpression' &&
|
|
74
|
-
node.object.object.type === 'Identifier' &&
|
|
75
|
-
(node.object.object.name === 'window' ||
|
|
76
|
-
node.object.object.name === 'globalThis') &&
|
|
77
|
-
node.object.property.type === 'Identifier' &&
|
|
78
|
-
Object.hasOwn(apiMap, node.object.property.name)
|
|
79
|
-
) {
|
|
80
|
-
const apiName = node.object.property.name
|
|
81
|
-
const sdkHook = apiMap[apiName]
|
|
82
|
-
|
|
83
|
-
context.report({
|
|
84
|
-
node: node.object,
|
|
85
|
-
messageId: 'preferAsyncStorage',
|
|
86
|
-
data: {
|
|
87
|
-
nativeApi: apiName,
|
|
88
|
-
sdkHook,
|
|
89
|
-
},
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
|
|
94
|
-
// Also catch direct references to localStorage/sessionStorage
|
|
95
|
-
Identifier(node) {
|
|
96
|
-
// Only flag if it's being used, not just referenced in imports
|
|
97
|
-
const parent = node.parent
|
|
98
|
-
|
|
99
|
-
// Skip if it's part of an import statement
|
|
100
|
-
if (
|
|
101
|
-
parent.type === 'ImportSpecifier' ||
|
|
102
|
-
parent.type === 'ImportDefaultSpecifier'
|
|
103
|
-
) {
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Skip if it's already part of a MemberExpression (handled above)
|
|
108
|
-
if (parent.type === 'MemberExpression' && parent.object === node) {
|
|
109
|
-
return
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Check if this is a direct reference to localStorage/sessionStorage
|
|
113
|
-
if (!Object.hasOwn(apiMap, node.name)) {
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Skip if it's the variable name being declared (e.g., const localStorage = ...)
|
|
118
|
-
if (parent.type === 'VariableDeclarator' && parent.id === node) {
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
context.report({
|
|
123
|
-
node,
|
|
124
|
-
messageId: 'preferAsyncStorage',
|
|
125
|
-
})
|
|
126
|
-
},
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
}
|