@shopify/shop-minis-react 0.16.1 → 0.16.2

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 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.16.1",
4
+ "version": "0.16.2",
5
5
  "sideEffects": false,
6
6
  "type": "module",
7
7
  "engines": {
@@ -54,6 +54,7 @@ export * from './util/useCheckPermissions'
54
54
 
55
55
  // - Intent Hooks
56
56
  export * from './intents/useIntent'
57
+ export * from './intents/useUserImagePicker'
57
58
 
58
59
  // - Event Hooks
59
60
  export * from './events/useOnMiniFocus'
@@ -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
@@ -579,6 +579,7 @@ export function makeMockActions(): ShopActions {
579
579
  },
580
580
  reportError: undefined,
581
581
  reportFetch: undefined,
582
+ invokeIntent: {code: 'closed' as const},
582
583
  } as const
583
584
 
584
585
  const mock: Partial<ShopActions> = {}
@@ -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
- }