shopify-nuxt 0.0.2 → 0.0.4
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/README.md +362 -23
- package/dist/module.json +1 -1
- package/dist/module.mjs +25 -6
- package/dist/runtime/components/ShopifyAppProvider.d.vue.ts +18 -0
- package/dist/runtime/components/ShopifyAppProvider.vue +22 -0
- package/dist/runtime/components/ShopifyAppProvider.vue.d.ts +18 -0
- package/dist/runtime/components/polaris/ShCheckbox.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShCheckbox.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShChoiceList.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShChoiceList.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShColorField.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShColorField.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShColorPicker.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShColorPicker.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShDateField.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShDateField.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShDatePicker.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShDatePicker.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShDropZone.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShDropZone.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShEmailField.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShEmailField.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShModal.d.vue.ts +7 -2
- package/dist/runtime/components/polaris/ShModal.vue +4 -1
- package/dist/runtime/components/polaris/ShModal.vue.d.ts +7 -2
- package/dist/runtime/components/polaris/ShMoneyField.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShMoneyField.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShNumberField.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShNumberField.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShPasswordField.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShPasswordField.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShSearchField.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShSearchField.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShSelect.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShSelect.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShSwitch.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShSwitch.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShTextArea.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShTextArea.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShTextField.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShTextField.vue.d.ts +2 -2
- package/dist/runtime/components/polaris/ShUrlField.d.vue.ts +2 -2
- package/dist/runtime/components/polaris/ShUrlField.vue.d.ts +2 -2
- package/dist/runtime/composables/useAppBridge.d.ts +4 -1
- package/dist/runtime/composables/useAppBridge.js +24 -13
- package/dist/runtime/middleware/shopify-auth.js +10 -4
- package/dist/runtime/pages/auth-login.d.vue.ts +3 -0
- package/dist/runtime/pages/auth-login.vue +90 -0
- package/dist/runtime/pages/auth-login.vue.d.ts +3 -0
- package/dist/runtime/server/index.d.ts +2 -0
- package/dist/runtime/server/index.js +4 -0
- package/dist/runtime/server/plugins/shopify-defaults.d.ts +8 -0
- package/dist/runtime/server/plugins/shopify-defaults.js +12 -0
- package/dist/runtime/server/routes/auth-callback.d.ts +1 -1
- package/dist/runtime/server/routes/auth-exit-iframe.d.ts +1 -1
- package/dist/runtime/server/routes/auth-session-token.d.ts +1 -1
- package/dist/runtime/server/routes/auth.d.ts +1 -1
- package/dist/runtime/server/services/shopify.js +3 -3
- package/dist/runtime/server/utils/clients.d.ts +24 -5
- package/dist/runtime/server/utils/clients.js +21 -2
- package/dist/runtime/server/utils/helpers.js +13 -11
- package/dist/runtime/server/utils/unauthenticated-storefront.d.ts +1 -5
- package/dist/runtime/server/utils/unauthenticated-storefront.js +2 -8
- package/dist/runtime/types.d.ts +48 -2
- package/package.json +9 -5
- package/dist/runtime/plugins/polaris.d.ts +0 -1
- package/dist/runtime/plugins/polaris.js +0 -0
package/README.md
CHANGED
|
@@ -47,22 +47,34 @@ export default defineNuxtConfig({
|
|
|
47
47
|
shopify: {
|
|
48
48
|
apiKey: 'ApiKeyFromPartnersDashboard',
|
|
49
49
|
apiSecretKey: 'ApiSecretKeyFromPartnersDashboard',
|
|
50
|
-
scopes: ['read_products', 'write_products'],
|
|
51
50
|
appUrl: 'https://your-app-url.com'
|
|
52
51
|
}
|
|
53
52
|
})
|
|
54
53
|
```
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
All values can also be set via environment variables:
|
|
56
|
+
|
|
57
|
+
| Option | Env Variable |
|
|
58
|
+
| -------------- | ----------------------------- |
|
|
59
|
+
| `apiKey` | `NUXT_SHOPIFY_API_KEY` |
|
|
60
|
+
| `apiSecretKey` | `NUXT_SHOPIFY_API_SECRET_KEY` |
|
|
61
|
+
| `appUrl` | `NUXT_SHOPIFY_APP_URL` |
|
|
62
|
+
|
|
63
|
+
### Session storage
|
|
64
|
+
|
|
65
|
+
By default, the module provides an in-memory session storage (`MemorySessionStorage`) that works out of the box — no configuration needed. For production use, you should use a persistent session storage adapter.
|
|
66
|
+
|
|
67
|
+
To override the default session storage or configure lifecycle hooks, create a Nitro server plugin:
|
|
57
68
|
|
|
58
69
|
```ts
|
|
59
70
|
// server/plugins/shopify.ts
|
|
60
71
|
import { configureShopify } from '#shopify/server'
|
|
61
|
-
import {
|
|
72
|
+
import { PrismaSessionStorage } from '@shopify/shopify-app-session-storage-prisma'
|
|
73
|
+
import { prisma } from '~/server/utils/prisma'
|
|
62
74
|
|
|
63
75
|
export default defineNitroPlugin(() => {
|
|
64
76
|
configureShopify({
|
|
65
|
-
sessionStorage: new
|
|
77
|
+
sessionStorage: new PrismaSessionStorage(prisma),
|
|
66
78
|
hooks: {
|
|
67
79
|
afterAuth: async ({ session, admin }) => {
|
|
68
80
|
// Register webhooks, seed data, etc.
|
|
@@ -72,14 +84,31 @@ export default defineNitroPlugin(() => {
|
|
|
72
84
|
})
|
|
73
85
|
```
|
|
74
86
|
|
|
75
|
-
|
|
87
|
+
Any config passed to `configureShopify()` is merged with the defaults — you only need to specify what you want to override.
|
|
88
|
+
|
|
89
|
+
### Module options
|
|
76
90
|
|
|
77
|
-
|
|
91
|
+
| Option | Type | Default | Description |
|
|
92
|
+
| ----------------- | ----------------- | ---------------- | ----------------------------------------------------------------------- |
|
|
93
|
+
| `apiKey` | `string` | — | Your Shopify API key from the Partners dashboard |
|
|
94
|
+
| `apiSecretKey` | `string` | — | Your Shopify API secret key |
|
|
95
|
+
| `scopes` | `string[]` | `[]` | OAuth scopes (optional — Shopify reads from `shopify.app.toml`) |
|
|
96
|
+
| `appUrl` | `string` | — | Your app's public URL (tunnel URL in development) |
|
|
97
|
+
| `apiVersion` | `ApiVersion` | Latest stable | The Shopify API version to use |
|
|
98
|
+
| `authPathPrefix` | `string` | `/_shopify/auth` | URL prefix for OAuth endpoints |
|
|
99
|
+
| `distribution` | `AppDistribution` | `app_store` | App distribution type (`app_store`, `single_merchant`, `shopify_admin`) |
|
|
100
|
+
| `useOnlineTokens` | `boolean` | `false` | Use online (per-user) tokens in addition to offline (per-shop) tokens |
|
|
101
|
+
| `authPage` | `string \| false` | built-in page | Custom auth page component path, or `false` to disable |
|
|
102
|
+
| `navLinks` | `NavLink[]` | `[]` | Navigation links for the app sidebar (used by `<ShopifyAppProvider>`) |
|
|
103
|
+
|
|
104
|
+
## Authenticating admin requests
|
|
105
|
+
|
|
106
|
+
Use `useShopifyAdmin()` in your server API routes to authenticate requests from the Shopify admin. The returned `admin` object provides **typed GraphQL** and REST clients powered by `@shopify/admin-api-client`:
|
|
78
107
|
|
|
79
108
|
```ts
|
|
80
109
|
// server/api/products.ts
|
|
81
110
|
export default defineEventHandler(async (event) => {
|
|
82
|
-
const { admin } = await useShopifyAdmin(event)
|
|
111
|
+
const { admin, session } = await useShopifyAdmin(event)
|
|
83
112
|
|
|
84
113
|
const response = await admin.graphql(`{
|
|
85
114
|
products(first: 5) {
|
|
@@ -92,13 +121,57 @@ export default defineEventHandler(async (event) => {
|
|
|
92
121
|
}
|
|
93
122
|
}`)
|
|
94
123
|
|
|
95
|
-
return response
|
|
124
|
+
return await response.json()
|
|
96
125
|
})
|
|
97
126
|
```
|
|
98
127
|
|
|
99
|
-
|
|
128
|
+
#### GraphQL with variables
|
|
100
129
|
|
|
101
|
-
|
|
130
|
+
```ts
|
|
131
|
+
const response = await admin.graphql(
|
|
132
|
+
`#graphql
|
|
133
|
+
mutation populateProduct($input: ProductInput!) {
|
|
134
|
+
productCreate(input: $input) {
|
|
135
|
+
product {
|
|
136
|
+
id
|
|
137
|
+
title
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}`,
|
|
141
|
+
{
|
|
142
|
+
variables: {
|
|
143
|
+
input: { title: 'New Product' }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const { data } = await response.json()
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### REST API
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const products = await admin.rest.get({ path: 'products' })
|
|
155
|
+
await admin.rest.post({
|
|
156
|
+
path: 'products',
|
|
157
|
+
data: { product: { title: 'New Product' } }
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### Full `AdminContext` return type
|
|
162
|
+
|
|
163
|
+
| Property | Type | Description |
|
|
164
|
+
| -------------- | -------------------------- | -------------------------------------------------- |
|
|
165
|
+
| `session` | `Session` | The authenticated Shopify session |
|
|
166
|
+
| `admin` | `AdminApiContext` | GraphQL and REST API clients |
|
|
167
|
+
| `sessionToken` | `JwtPayload \| undefined` | Decoded session token (embedded apps only) |
|
|
168
|
+
| `billing` | `BillingContext` | Helpers for `require()`, `check()`, `request()` |
|
|
169
|
+
| `cors` | `(response) => Response` | Add CORS headers to a response |
|
|
170
|
+
| `redirect` | `(url, init?) => Response` | Redirect helper that works inside embedded iframes |
|
|
171
|
+
|
|
172
|
+
## Handling webhooks
|
|
173
|
+
|
|
174
|
+
Use `useShopifyWebhook()` to validate and process incoming webhooks. HMAC verification is handled automatically:
|
|
102
175
|
|
|
103
176
|
```ts
|
|
104
177
|
// server/api/webhooks.ts
|
|
@@ -118,7 +191,81 @@ export default defineEventHandler(async (event) => {
|
|
|
118
191
|
})
|
|
119
192
|
```
|
|
120
193
|
|
|
121
|
-
|
|
194
|
+
#### Registering webhooks programmatically
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
// server/plugins/shopify.ts
|
|
198
|
+
import { configureShopify, registerShopifyWebhooks } from '#shopify/server'
|
|
199
|
+
|
|
200
|
+
export default defineNitroPlugin(() => {
|
|
201
|
+
configureShopify({
|
|
202
|
+
hooks: {
|
|
203
|
+
afterAuth: async ({ session }) => {
|
|
204
|
+
await registerShopifyWebhooks(session)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
> **Tip**: For most apps, registering webhooks via `shopify.app.toml` is sufficient. Use `registerShopifyWebhooks()` only when you need dynamic, per-shop webhook registration.
|
|
212
|
+
|
|
213
|
+
## Authenticating other request types
|
|
214
|
+
|
|
215
|
+
### Shopify Flow
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// server/api/flow.ts
|
|
219
|
+
export default defineEventHandler(async (event) => {
|
|
220
|
+
const { session, admin, payload } = await useShopifyFlow(event)
|
|
221
|
+
// Handle Flow trigger/action
|
|
222
|
+
})
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Public requests (checkout extensions, etc.)
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
// server/api/public/widget.ts
|
|
229
|
+
export default defineEventHandler(async (event) => {
|
|
230
|
+
const { sessionToken, cors } = await useShopifyPublic(event)
|
|
231
|
+
// sessionToken contains the decoded JWT payload
|
|
232
|
+
// Use cors() to wrap your response with CORS headers
|
|
233
|
+
})
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Unauthenticated access (background jobs)
|
|
237
|
+
|
|
238
|
+
For accessing the Shopify API without an incoming request (cron jobs, background tasks):
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
// server/api/cron/sync.ts
|
|
242
|
+
export default defineEventHandler(async () => {
|
|
243
|
+
const { admin } = await useShopifyUnauthenticatedAdmin(
|
|
244
|
+
'my-shop.myshopify.com'
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
const response = await admin.graphql(`{
|
|
248
|
+
products(first: 10) { edges { node { id title } } }
|
|
249
|
+
}`)
|
|
250
|
+
|
|
251
|
+
return await response.json()
|
|
252
|
+
})
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
// Storefront API access
|
|
257
|
+
const { storefront } = await useShopifyUnauthenticatedStorefront(
|
|
258
|
+
'my-shop.myshopify.com'
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
const response = await storefront.graphql(`{
|
|
262
|
+
products(first: 10) { edges { node { id title } } }
|
|
263
|
+
}`)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Both the admin and storefront GraphQL clients are typed via `@shopify/admin-api-client` and `@shopify/storefront-api-client` respectively.
|
|
267
|
+
|
|
268
|
+
## Using App Bridge on the client
|
|
122
269
|
|
|
123
270
|
The module automatically injects the Shopify App Bridge CDN script and provides typed access via composables:
|
|
124
271
|
|
|
@@ -127,15 +274,72 @@ The module automatically injects the Shopify App Bridge CDN script and provides
|
|
|
127
274
|
// Access the App Bridge instance (typed as ShopifyGlobal)
|
|
128
275
|
const shopify = useAppBridge()
|
|
129
276
|
|
|
130
|
-
//
|
|
277
|
+
// Get the current shop
|
|
278
|
+
const shop = shopify.config.shop
|
|
279
|
+
</script>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Authenticated fetch
|
|
283
|
+
|
|
284
|
+
Use `useShopifyFetch()` for client-side API calls that automatically include the session token:
|
|
285
|
+
|
|
286
|
+
```vue
|
|
287
|
+
<script setup>
|
|
131
288
|
const shopifyFetch = useShopifyFetch()
|
|
132
|
-
|
|
289
|
+
|
|
290
|
+
const { data: products } = await useAsyncData(
|
|
291
|
+
'products',
|
|
292
|
+
() => shopifyFetch('/api/products'),
|
|
293
|
+
{ server: false }
|
|
294
|
+
)
|
|
133
295
|
</script>
|
|
134
296
|
```
|
|
135
297
|
|
|
136
|
-
|
|
298
|
+
> **Important**: Always use `server: false` with `useShopifyFetch()` — session tokens are only available on the client side within the Shopify admin iframe.
|
|
299
|
+
|
|
300
|
+
## Using Polaris components
|
|
137
301
|
|
|
138
|
-
|
|
302
|
+
### `<ShopifyAppProvider>`
|
|
303
|
+
|
|
304
|
+
Wrap your app pages with `<ShopifyAppProvider>` to automatically render the [App Bridge navigation menu](https://shopify.dev/docs/api/app-home/app-bridge-web-components/app-nav) from your module config:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
// nuxt.config.ts
|
|
308
|
+
export default defineNuxtConfig({
|
|
309
|
+
modules: ['shopify-nuxt'],
|
|
310
|
+
shopify: {
|
|
311
|
+
// ...
|
|
312
|
+
navLinks: [
|
|
313
|
+
{ label: 'Home', href: '/', rel: 'home' },
|
|
314
|
+
{ label: 'Products', href: '/products' },
|
|
315
|
+
{ label: 'Settings', href: '/settings' }
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
```vue
|
|
322
|
+
<!-- layouts/default.vue -->
|
|
323
|
+
<template>
|
|
324
|
+
<ShopifyAppProvider>
|
|
325
|
+
<slot />
|
|
326
|
+
</ShopifyAppProvider>
|
|
327
|
+
</template>
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
You can also override the links per-component via the `links` prop:
|
|
331
|
+
|
|
332
|
+
```vue
|
|
333
|
+
<ShopifyAppProvider :links="[{ label: 'Home', href: '/', rel: 'home' }]">
|
|
334
|
+
<slot />
|
|
335
|
+
</ShopifyAppProvider>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Each `NavLink` has: `label` (string), `href` (string), and optionally `rel: 'home'` to set the default landing page.
|
|
339
|
+
|
|
340
|
+
### Polaris web components
|
|
341
|
+
|
|
342
|
+
This module provides Vue wrapper components for [Shopify Polaris web components](https://shopify.dev/docs/api/app-home/web-components). Use the `Sh`-prefixed wrappers instead of the raw `s-*` web components — they provide typed props with autocomplete, v-model support for form components, and work seamlessly with Vue's reactivity system.
|
|
139
343
|
|
|
140
344
|
```vue
|
|
141
345
|
<template>
|
|
@@ -150,17 +354,85 @@ This module provides Vue wrapper components for [Shopify Polaris web components]
|
|
|
150
354
|
</ShBanner>
|
|
151
355
|
|
|
152
356
|
<ShTextField
|
|
357
|
+
v-model="title"
|
|
153
358
|
label="Product title"
|
|
154
|
-
:value="title"
|
|
155
359
|
placeholder="Enter product title"
|
|
156
360
|
/>
|
|
361
|
+
|
|
362
|
+
<ShSelect
|
|
363
|
+
v-model="status"
|
|
364
|
+
label="Status"
|
|
365
|
+
:options="[
|
|
366
|
+
{ label: 'Active', value: 'active' },
|
|
367
|
+
{ label: 'Draft', value: 'draft' }
|
|
368
|
+
]"
|
|
369
|
+
/>
|
|
157
370
|
</ShPage>
|
|
158
371
|
</template>
|
|
159
372
|
```
|
|
160
373
|
|
|
161
374
|
All `Sh*` components are auto-imported — no manual imports needed. Each component maps directly to its Polaris web component counterpart (e.g., `<ShButton>` → `<s-button>`, `<ShTextField>` → `<s-text-field>`).
|
|
162
375
|
|
|
163
|
-
###
|
|
376
|
+
### Available components
|
|
377
|
+
|
|
378
|
+
Layout & structure: `ShPage`, `ShBox`, `ShStack`, `ShGrid`, `ShGridItem`, `ShSection`, `ShDivider`
|
|
379
|
+
|
|
380
|
+
Actions: `ShButton`, `ShButtonGroup`, `ShClickable`, `ShLink`
|
|
381
|
+
|
|
382
|
+
Forms: `ShTextField`, `ShNumberField`, `ShEmailField`, `ShPasswordField`, `ShUrlField`, `ShMoneyField`, `ShColorField`, `ShDateField`, `ShTextArea`, `ShSelect`, `ShCheckbox`, `ShSwitch`, `ShChoiceList`, `ShChoice`, `ShSearchField`, `ShDropZone`, `ShColorPicker`, `ShDatePicker`
|
|
383
|
+
|
|
384
|
+
Feedback: `ShBanner`, `ShBadge`, `ShSpinner`, `ShTooltip`
|
|
385
|
+
|
|
386
|
+
Navigation: `ShAppNav`, `ShMenu`, `ShOption`, `ShOptionGroup`, `ShPopover`
|
|
387
|
+
|
|
388
|
+
Data: `ShTable`, `ShTableHeader`, `ShTableHeaderRow`, `ShTableBody`, `ShTableRow`, `ShTableCell`
|
|
389
|
+
|
|
390
|
+
Content: `ShText`, `ShHeading`, `ShParagraph`, `ShIcon`, `ShImage`, `ShThumbnail`, `ShAvatar`, `ShChip`, `ShClickableChip`, `ShListItem`, `ShOrderedList`, `ShUnorderedList`
|
|
391
|
+
|
|
392
|
+
Other: `ShModal`, `ShQueryContainer`
|
|
393
|
+
|
|
394
|
+
### Using `ShModal`
|
|
395
|
+
|
|
396
|
+
`ShModal` wraps the [Polaris `<s-modal>`](https://shopify.dev/docs/api/app-home/web-components/overlays/modal) component — it renders **inside** your app's iframe. Open and close it using `commandFor` / `command` attributes:
|
|
397
|
+
|
|
398
|
+
```vue
|
|
399
|
+
<template>
|
|
400
|
+
<ShButton command-for="my-modal" command="--show">Open</ShButton>
|
|
401
|
+
|
|
402
|
+
<ShModal id="my-modal" heading="Confirm action">
|
|
403
|
+
<ShParagraph>Are you sure?</ShParagraph>
|
|
404
|
+
|
|
405
|
+
<ShButton slot="secondary-actions" command-for="my-modal" command="--hide">
|
|
406
|
+
Cancel
|
|
407
|
+
</ShButton>
|
|
408
|
+
<ShButton
|
|
409
|
+
slot="primary-action"
|
|
410
|
+
variant="primary"
|
|
411
|
+
command-for="my-modal"
|
|
412
|
+
command="--hide"
|
|
413
|
+
>
|
|
414
|
+
Confirm
|
|
415
|
+
</ShButton>
|
|
416
|
+
</ShModal>
|
|
417
|
+
</template>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
> **Note**: `ShModal` is **not** the same as the [App Bridge `<ui-modal>`](https://shopify.dev/docs/api/app-bridge-library/apis/modal) which renders outside the iframe and is controlled via `shopify.modal.show(id)`. If you need the App Bridge modal, use `<ui-modal>` directly.
|
|
421
|
+
|
|
422
|
+
## OAuth routes
|
|
423
|
+
|
|
424
|
+
The module automatically registers these routes:
|
|
425
|
+
|
|
426
|
+
| Route | Purpose |
|
|
427
|
+
| ---------------------------------- | -------------------------------------- |
|
|
428
|
+
| `GET /_shopify/auth` | Start the OAuth flow |
|
|
429
|
+
| `GET /_shopify/auth/callback` | Handle the OAuth callback from Shopify |
|
|
430
|
+
| `GET /_shopify/auth/exit-iframe` | App Bridge iframe escape page |
|
|
431
|
+
| `GET /_shopify/auth/session-token` | Session token bounce page |
|
|
432
|
+
|
|
433
|
+
The prefix `/_shopify/auth` is configurable via the `authPathPrefix` option.
|
|
434
|
+
|
|
435
|
+
## Loading your app in Shopify Admin
|
|
164
436
|
|
|
165
437
|
To load your app within the Shopify Admin, you need to:
|
|
166
438
|
|
|
@@ -175,11 +447,15 @@ To load your app within the Shopify Admin, you need to:
|
|
|
175
447
|
| **Authentication** | OAuth flow, session tokens, token exchange — all handled automatically |
|
|
176
448
|
| **App Bridge** | CDN-based App Bridge with full TypeScript types via `@shopify/app-bridge-types` |
|
|
177
449
|
| **Polaris** | Vue wrapper components (`Sh*`) for all Polaris web components with typed props |
|
|
450
|
+
| **Typed GraphQL** | Admin and Storefront API clients typed via `@shopify/admin-api-client` |
|
|
178
451
|
| **Webhooks** | HMAC validation, payload parsing, and webhook registration |
|
|
179
452
|
| **Admin API** | GraphQL and REST clients with automatic session management |
|
|
453
|
+
| **Storefront API** | Typed GraphQL client for Storefront API via `@shopify/storefront-api-client` |
|
|
180
454
|
| **Billing** | Billing context for subscription and usage-based charges |
|
|
181
|
-
| **Session storage** |
|
|
182
|
-
| **Auto-imports** | Server utilities
|
|
455
|
+
| **Session storage** | Built-in `MemorySessionStorage` default, pluggable via `configureShopify()` |
|
|
456
|
+
| **Auto-imports** | Server utilities, client composables, and components are auto-imported |
|
|
457
|
+
| **Bot detection** | Admin auth automatically detects bots and returns 410 to avoid unnecessary auth |
|
|
458
|
+
| **CORS** | Built-in CORS helpers for public/checkout extension endpoints |
|
|
183
459
|
|
|
184
460
|
## Server utilities
|
|
185
461
|
|
|
@@ -198,14 +474,39 @@ These are auto-imported in your `server/` directory:
|
|
|
198
474
|
| `useShopifyUnauthenticatedStorefront(shop)` | Offline session storefront API access |
|
|
199
475
|
| `registerShopifyWebhooks(session)` | Register webhooks for a shop |
|
|
200
476
|
|
|
477
|
+
### `#shopify/server` exports
|
|
478
|
+
|
|
479
|
+
For use in Nitro plugins and advanced server-side configuration:
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
import {
|
|
483
|
+
configureShopify,
|
|
484
|
+
getShopifyApi,
|
|
485
|
+
getResolvedConfig,
|
|
486
|
+
getSessionStorage,
|
|
487
|
+
registerShopifyWebhooks,
|
|
488
|
+
createAdminApiContext,
|
|
489
|
+
createStorefrontApiContext
|
|
490
|
+
} from '#shopify/server'
|
|
491
|
+
|
|
492
|
+
// Types
|
|
493
|
+
import type {
|
|
494
|
+
AdminApiContext,
|
|
495
|
+
StorefrontApiContext,
|
|
496
|
+
GraphQLClient,
|
|
497
|
+
GraphQLQueryOptions,
|
|
498
|
+
GraphQLResponse
|
|
499
|
+
} from '#shopify/server'
|
|
500
|
+
```
|
|
501
|
+
|
|
201
502
|
## Client composables
|
|
202
503
|
|
|
203
504
|
These are auto-imported in your Vue components:
|
|
204
505
|
|
|
205
|
-
| Composable | Purpose
|
|
206
|
-
| ------------------- |
|
|
207
|
-
| `useAppBridge()` | Returns typed `ShopifyGlobal` from App Bridge CDN
|
|
208
|
-
| `useShopifyFetch()` | Fetch wrapper with automatic session token
|
|
506
|
+
| Composable | Purpose |
|
|
507
|
+
| ------------------- | -------------------------------------------------------------------- |
|
|
508
|
+
| `useAppBridge()` | Returns typed `ShopifyGlobal` from App Bridge CDN |
|
|
509
|
+
| `useShopifyFetch()` | Fetch wrapper with automatic session token in `Authorization` header |
|
|
209
510
|
|
|
210
511
|
## Testing your app
|
|
211
512
|
|
|
@@ -219,8 +520,38 @@ const config = testConfig()
|
|
|
219
520
|
|
|
220
521
|
// testSession() returns a mock Shopify session
|
|
221
522
|
const session = testSession()
|
|
523
|
+
|
|
524
|
+
// Both accept overrides
|
|
525
|
+
const customConfig = testConfig({ apiKey: 'custom-key' })
|
|
526
|
+
const customSession = testSession({ shop: 'custom-shop.myshopify.com' })
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
## TypeScript
|
|
530
|
+
|
|
531
|
+
The module augments Nuxt's `RuntimeConfig` types so you get full autocomplete and type safety when accessing config:
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
// Server — all Shopify config fields are typed
|
|
535
|
+
const config = useRuntimeConfig()
|
|
536
|
+
config.shopify.apiKey // string
|
|
537
|
+
config.shopify.apiSecretKey // string
|
|
538
|
+
config.shopify.scopes // string[]
|
|
539
|
+
config.shopify.appUrl // string
|
|
540
|
+
|
|
541
|
+
// Client — only public fields
|
|
542
|
+
const publicConfig = useRuntimeConfig().public
|
|
543
|
+
publicConfig.shopify.apiKey // string
|
|
544
|
+
publicConfig.shopify.authPagePath // string
|
|
545
|
+
publicConfig.shopify.authPathPrefix // string
|
|
222
546
|
```
|
|
223
547
|
|
|
548
|
+
The types are declared via module augmentation in `nuxt/schema`:
|
|
549
|
+
|
|
550
|
+
| Interface | Key | Fields |
|
|
551
|
+
| --------------------- | --------- | --------------------------------------------------------------------------------------------------------------- |
|
|
552
|
+
| `RuntimeConfig` | `shopify` | `apiKey`, `apiSecretKey`, `scopes`, `appUrl`, `apiVersion`, `authPathPrefix`, `distribution`, `useOnlineTokens` |
|
|
553
|
+
| `PublicRuntimeConfig` | `shopify` | `apiKey`, `authPagePath`, `authPathPrefix` |
|
|
554
|
+
|
|
224
555
|
## Resources
|
|
225
556
|
|
|
226
557
|
Getting started:
|
|
@@ -237,6 +568,14 @@ Shopify:
|
|
|
237
568
|
- [App extensions](https://shopify.dev/docs/apps/app-extensions/list)
|
|
238
569
|
- [Shopify Functions](https://shopify.dev/docs/api/functions)
|
|
239
570
|
|
|
571
|
+
Session storage adapters:
|
|
572
|
+
|
|
573
|
+
- [`@shopify/shopify-app-session-storage-prisma`](https://www.npmjs.com/package/@shopify/shopify-app-session-storage-prisma)
|
|
574
|
+
- [`@shopify/shopify-app-session-storage-drizzle`](https://www.npmjs.com/package/@shopify/shopify-app-session-storage-drizzle)
|
|
575
|
+
- [`@shopify/shopify-app-session-storage-redis`](https://www.npmjs.com/package/@shopify/shopify-app-session-storage-redis)
|
|
576
|
+
- [`@shopify/shopify-app-session-storage-mongodb`](https://www.npmjs.com/package/@shopify/shopify-app-session-storage-mongodb)
|
|
577
|
+
- [`@shopify/shopify-app-session-storage-memory`](https://www.npmjs.com/package/@shopify/shopify-app-session-storage-memory) (default, not for production)
|
|
578
|
+
|
|
240
579
|
## Contributing
|
|
241
580
|
|
|
242
581
|
<details>
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, addServerImportsDir, addImportsDir, addComponentsDir, addTypeTemplate, addPlugin, addServerHandler, addRouteMiddleware } from '@nuxt/kit';
|
|
1
|
+
import { defineNuxtModule, createResolver, addServerImportsDir, addImportsDir, addComponentsDir, addTypeTemplate, addPlugin, addServerHandler, addRouteMiddleware, extendPages } from '@nuxt/kit';
|
|
2
2
|
import { ApiVersion } from '@shopify/shopify-api';
|
|
3
3
|
|
|
4
4
|
const module$1 = defineNuxtModule({
|
|
@@ -9,7 +9,6 @@ const module$1 = defineNuxtModule({
|
|
|
9
9
|
defaults: {
|
|
10
10
|
apiKey: "",
|
|
11
11
|
apiSecretKey: "",
|
|
12
|
-
scopes: [],
|
|
13
12
|
appUrl: "",
|
|
14
13
|
apiVersion: ApiVersion.January26,
|
|
15
14
|
authPathPrefix: "/_shopify/auth",
|
|
@@ -21,7 +20,7 @@ const module$1 = defineNuxtModule({
|
|
|
21
20
|
nuxt.options.runtimeConfig.shopify = {
|
|
22
21
|
apiKey: options.apiKey,
|
|
23
22
|
apiSecretKey: options.apiSecretKey,
|
|
24
|
-
scopes: options.scopes,
|
|
23
|
+
scopes: options.scopes || [],
|
|
25
24
|
appUrl: options.appUrl,
|
|
26
25
|
apiVersion: options.apiVersion || ApiVersion.January26,
|
|
27
26
|
authPathPrefix: options.authPathPrefix || "/_shopify/auth",
|
|
@@ -29,7 +28,10 @@ const module$1 = defineNuxtModule({
|
|
|
29
28
|
useOnlineTokens: options.useOnlineTokens || false
|
|
30
29
|
};
|
|
31
30
|
nuxt.options.runtimeConfig.public.shopify = {
|
|
32
|
-
apiKey: options.apiKey
|
|
31
|
+
apiKey: options.apiKey,
|
|
32
|
+
authPagePath: options.authPage !== false ? "/auth" : "",
|
|
33
|
+
authPathPrefix: options.authPathPrefix || "/_shopify/auth",
|
|
34
|
+
navLinks: options.navLinks || []
|
|
33
35
|
};
|
|
34
36
|
nuxt.options.alias["#shopify/server"] = resolver.resolve("./runtime/server");
|
|
35
37
|
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
@@ -68,7 +70,7 @@ const module$1 = defineNuxtModule({
|
|
|
68
70
|
...nuxt.options.app.head.script || []
|
|
69
71
|
];
|
|
70
72
|
nuxt.options.vue.compilerOptions.isCustomElement = (tag) => {
|
|
71
|
-
return tag.startsWith("s-");
|
|
73
|
+
return tag.startsWith("s-") || tag.startsWith("ui-");
|
|
72
74
|
};
|
|
73
75
|
});
|
|
74
76
|
addTypeTemplate({
|
|
@@ -115,13 +117,30 @@ export {}
|
|
|
115
117
|
path: resolver.resolve("./runtime/middleware/shopify-auth"),
|
|
116
118
|
global: false
|
|
117
119
|
});
|
|
120
|
+
if (options.authPage !== false) {
|
|
121
|
+
extendPages((pages) => {
|
|
122
|
+
pages.push({
|
|
123
|
+
name: "shopify-auth-login",
|
|
124
|
+
path: "/auth",
|
|
125
|
+
file: options.authPage || resolver.resolve("./runtime/pages/auth-login.vue")
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
nuxt.hook("nitro:config", (nitroConfig) => {
|
|
130
|
+
nitroConfig.plugins = nitroConfig.plugins || [];
|
|
131
|
+
nitroConfig.plugins.push(
|
|
132
|
+
resolver.resolve("./runtime/server/plugins/shopify-defaults")
|
|
133
|
+
);
|
|
134
|
+
});
|
|
118
135
|
nuxt.options.build.transpile.push(resolver.resolve("./runtime"));
|
|
119
136
|
nuxt.hook("nitro:config", (nitroConfig) => {
|
|
120
137
|
nitroConfig.externals = nitroConfig.externals || {};
|
|
121
138
|
nitroConfig.externals.inline = nitroConfig.externals.inline || [];
|
|
122
139
|
nitroConfig.externals.inline.push(
|
|
123
140
|
"@shopify/shopify-api",
|
|
124
|
-
"@shopify/shopify-api/adapters/node"
|
|
141
|
+
"@shopify/shopify-api/adapters/node",
|
|
142
|
+
"@shopify/shopify-app-session-storage-memory",
|
|
143
|
+
"isbot"
|
|
125
144
|
);
|
|
126
145
|
});
|
|
127
146
|
nuxt.hook("prepare:types", ({ references }) => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { NavLink } from '../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** Override the nav links from module config */
|
|
4
|
+
links?: NavLink[];
|
|
5
|
+
};
|
|
6
|
+
declare var __VLS_1: {};
|
|
7
|
+
type __VLS_Slots = {} & {
|
|
8
|
+
default?: (props: typeof __VLS_1) => any;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
12
|
+
declare const _default: typeof __VLS_export;
|
|
13
|
+
export default _default;
|
|
14
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
15
|
+
new (): {
|
|
16
|
+
$slots: S;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<slot />
|
|
3
|
+
<ShAppNav v-if="navLinks.length">
|
|
4
|
+
<ShLink
|
|
5
|
+
v-for="link in navLinks"
|
|
6
|
+
:key="link.href"
|
|
7
|
+
:href="link.href"
|
|
8
|
+
:rel="link.rel"
|
|
9
|
+
>
|
|
10
|
+
{{ link.label }}
|
|
11
|
+
</ShLink>
|
|
12
|
+
</ShAppNav>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup>
|
|
16
|
+
import { useRuntimeConfig } from "#app";
|
|
17
|
+
const props = defineProps({
|
|
18
|
+
links: { type: Array, required: false }
|
|
19
|
+
});
|
|
20
|
+
const config = useRuntimeConfig().public.shopify;
|
|
21
|
+
const navLinks = props.links || config.navLinks || [];
|
|
22
|
+
</script>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { NavLink } from '../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** Override the nav links from module config */
|
|
4
|
+
links?: NavLink[];
|
|
5
|
+
};
|
|
6
|
+
declare var __VLS_1: {};
|
|
7
|
+
type __VLS_Slots = {} & {
|
|
8
|
+
default?: (props: typeof __VLS_1) => any;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
12
|
+
declare const _default: typeof __VLS_export;
|
|
13
|
+
export default _default;
|
|
14
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
15
|
+
new (): {
|
|
16
|
+
$slots: S;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -18,11 +18,11 @@ type __VLS_Slots = {} & {
|
|
|
18
18
|
};
|
|
19
19
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
20
20
|
"update:modelValue": (v: boolean) => void;
|
|
21
|
-
change: (event: Event) => void;
|
|
22
21
|
input: (event: Event) => void;
|
|
22
|
+
change: (event: Event) => void;
|
|
23
23
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
24
|
-
onChange?: ((event: Event) => any) | undefined;
|
|
25
24
|
onInput?: ((event: Event) => any) | undefined;
|
|
25
|
+
onChange?: ((event: Event) => any) | undefined;
|
|
26
26
|
"onUpdate:modelValue"?: ((v: boolean) => any) | undefined;
|
|
27
27
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
28
28
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -18,11 +18,11 @@ type __VLS_Slots = {} & {
|
|
|
18
18
|
};
|
|
19
19
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
20
20
|
"update:modelValue": (v: boolean) => void;
|
|
21
|
-
change: (event: Event) => void;
|
|
22
21
|
input: (event: Event) => void;
|
|
22
|
+
change: (event: Event) => void;
|
|
23
23
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
24
|
-
onChange?: ((event: Event) => any) | undefined;
|
|
25
24
|
onInput?: ((event: Event) => any) | undefined;
|
|
25
|
+
onChange?: ((event: Event) => any) | undefined;
|
|
26
26
|
"onUpdate:modelValue"?: ((v: boolean) => any) | undefined;
|
|
27
27
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
28
28
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|