@shopify/shopify-app-remix 4.1.1 → 4.2.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/cjs/react/components/AppProvider/AppProvider.js.map +1 -1
- package/dist/cjs/react/components/AppProxyLink/AppProxyLink.js.map +1 -1
- package/dist/cjs/server/authenticate/admin/strategies/auth-code-flow.js.map +1 -1
- package/dist/cjs/server/authenticate/admin/strategies/merchant-custom-app.js.map +1 -1
- package/dist/cjs/server/authenticate/admin/strategies/token-exchange.js.map +1 -1
- package/dist/cjs/server/authenticate/webhooks/authenticate.js +34 -10
- package/dist/cjs/server/authenticate/webhooks/authenticate.js.map +1 -1
- package/dist/cjs/server/version.js +1 -1
- package/dist/cjs/server/version.js.map +1 -1
- package/dist/esm/react/components/AppProvider/AppProvider.mjs.map +1 -1
- package/dist/esm/react/components/AppProxyLink/AppProxyLink.mjs.map +1 -1
- package/dist/esm/server/authenticate/admin/strategies/auth-code-flow.mjs.map +1 -1
- package/dist/esm/server/authenticate/admin/strategies/merchant-custom-app.mjs.map +1 -1
- package/dist/esm/server/authenticate/admin/strategies/token-exchange.mjs.map +1 -1
- package/dist/esm/server/authenticate/webhooks/authenticate.mjs +35 -11
- package/dist/esm/server/authenticate/webhooks/authenticate.mjs.map +1 -1
- package/dist/esm/server/version.mjs +1 -1
- package/dist/esm/server/version.mjs.map +1 -1
- package/dist/ts/react/components/AppProvider/AppProvider.d.ts.map +1 -1
- package/dist/ts/react/components/AppProxyLink/AppProxyLink.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/billing/types.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/strategies/auth-code-flow.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/strategies/merchant-custom-app.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/strategies/token-exchange.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/types.d.ts.map +1 -1
- package/dist/ts/server/authenticate/public/extension/types.d.ts.map +1 -1
- package/dist/ts/server/authenticate/webhooks/authenticate.d.ts.map +1 -1
- package/dist/ts/server/authenticate/webhooks/types.d.ts +34 -2
- package/dist/ts/server/authenticate/webhooks/types.d.ts.map +1 -1
- package/dist/ts/server/config-types.d.ts.map +1 -1
- package/dist/ts/server/version.d.ts +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +18 -14
- package/src/react/components/AppProvider/AppProvider.doc.ts +35 -0
- package/src/react/components/AppProvider/AppProvider.tsx +147 -0
- package/src/react/components/AppProvider/index.ts +1 -0
- package/src/react/components/AppProxyForm/AppProxyForm.doc.ts +28 -0
- package/src/react/components/AppProxyForm/AppProxyForm.tsx +82 -0
- package/src/react/components/AppProxyForm/index.ts +1 -0
- package/src/react/components/AppProxyLink/AppProxyLink.doc.ts +28 -0
- package/src/react/components/AppProxyLink/AppProxyLink.tsx +58 -0
- package/src/react/components/AppProxyLink/index.ts +1 -0
- package/src/react/components/AppProxyProvider/AppProxyProvider.doc.ts +40 -0
- package/src/react/components/AppProxyProvider/AppProxyProvider.tsx +100 -0
- package/src/react/components/AppProxyProvider/index.ts +5 -0
- package/src/react/components/RemixPolarisLink.tsx +14 -0
- package/src/react/components/index.ts +4 -0
- package/src/react/const.ts +2 -0
- package/src/react/index.ts +1 -0
- package/src/server/__test-helpers/const.ts +14 -0
- package/src/server/__test-helpers/expect-admin-api-client.ts +51 -0
- package/src/server/__test-helpers/expect-begin-auth-redirect.ts +22 -0
- package/src/server/__test-helpers/expect-document-request-headers.ts +25 -0
- package/src/server/__test-helpers/expect-exit-iframe.ts +37 -0
- package/src/server/__test-helpers/expect-login-redirect.ts +8 -0
- package/src/server/__test-helpers/expect-storefront-api-client.ts +49 -0
- package/src/server/__test-helpers/expect-token-refresh.ts +188 -0
- package/src/server/__test-helpers/get-hmac.ts +7 -0
- package/src/server/__test-helpers/get-jwt.ts +10 -0
- package/src/server/__test-helpers/get-thrown-error.ts +19 -0
- package/src/server/__test-helpers/get-thrown-response.ts +17 -0
- package/src/server/__test-helpers/index.ts +21 -0
- package/src/server/__test-helpers/mock-graphql-request.ts +60 -0
- package/src/server/__test-helpers/request-mock.ts +169 -0
- package/src/server/__test-helpers/setup-embedded-flow.ts +30 -0
- package/src/server/__test-helpers/setup-fetch-flow.ts +29 -0
- package/src/server/__test-helpers/setup-non-embedded-flow.ts +31 -0
- package/src/server/__test-helpers/setup-valid-request.ts +78 -0
- package/src/server/__test-helpers/setup-valid-session.ts +28 -0
- package/src/server/__test-helpers/sign-request-cookie.ts +21 -0
- package/src/server/__test-helpers/test-config.ts +56 -0
- package/src/server/adapters/node/index.ts +13 -0
- package/src/server/adapters/vercel/index.ts +13 -0
- package/src/server/authenticate/admin/authenticate.admin.doc.example.ts +34 -0
- package/src/server/authenticate/admin/authenticate.admin.doc.ts +51 -0
- package/src/server/authenticate/admin/authenticate.ts +232 -0
- package/src/server/authenticate/admin/billing/authenticate.admin.billing.doc.ts +29 -0
- package/src/server/authenticate/admin/billing/cancel.ts +38 -0
- package/src/server/authenticate/admin/billing/check.ts +41 -0
- package/src/server/authenticate/admin/billing/create-usage-record.ts +36 -0
- package/src/server/authenticate/admin/billing/helpers.ts +42 -0
- package/src/server/authenticate/admin/billing/index.ts +6 -0
- package/src/server/authenticate/admin/billing/request.ts +64 -0
- package/src/server/authenticate/admin/billing/require.ts +58 -0
- package/src/server/authenticate/admin/billing/types.ts +613 -0
- package/src/server/authenticate/admin/billing/update-usage-subscription-capped-amount.ts +55 -0
- package/src/server/authenticate/admin/helpers/begin-auth.ts +17 -0
- package/src/server/authenticate/admin/helpers/create-admin-api-context.ts +20 -0
- package/src/server/authenticate/admin/helpers/ensure-app-is-embedded-if-required.ts +18 -0
- package/src/server/authenticate/admin/helpers/ensure-session-token-search-param-if-required.ts +25 -0
- package/src/server/authenticate/admin/helpers/handle-client-error.ts +45 -0
- package/src/server/authenticate/admin/helpers/index.ts +14 -0
- package/src/server/authenticate/admin/helpers/redirect-to-auth-page.ts +28 -0
- package/src/server/authenticate/admin/helpers/redirect-to-bounce-page.ts +23 -0
- package/src/server/authenticate/admin/helpers/redirect-to-install-page.ts +53 -0
- package/src/server/authenticate/admin/helpers/redirect-to-shopify-or-app-root.ts +21 -0
- package/src/server/authenticate/admin/helpers/redirect-with-app-bridge-headers.ts +13 -0
- package/src/server/authenticate/admin/helpers/redirect-with-exitiframe.ts +28 -0
- package/src/server/authenticate/admin/helpers/redirect.ts +164 -0
- package/src/server/authenticate/admin/helpers/render-app-bridge.ts +46 -0
- package/src/server/authenticate/admin/helpers/trigger-after-auth-hook.ts +29 -0
- package/src/server/authenticate/admin/helpers/validate-redirect-url.ts +67 -0
- package/src/server/authenticate/admin/helpers/validate-shop-and-host-params.ts +46 -0
- package/src/server/authenticate/admin/scope/authenticate.admin.scopes.doc.ts +28 -0
- package/src/server/authenticate/admin/scope/client/fetch-scopes-details.ts +43 -0
- package/src/server/authenticate/admin/scope/client/revoke-scopes.ts +38 -0
- package/src/server/authenticate/admin/scope/factory.ts +21 -0
- package/src/server/authenticate/admin/scope/query.ts +51 -0
- package/src/server/authenticate/admin/scope/request.ts +32 -0
- package/src/server/authenticate/admin/scope/revoke.ts +48 -0
- package/src/server/authenticate/admin/scope/types.ts +126 -0
- package/src/server/authenticate/admin/strategies/auth-code-flow.ts +332 -0
- package/src/server/authenticate/admin/strategies/merchant-custom-app.ts +65 -0
- package/src/server/authenticate/admin/strategies/token-exchange.ts +188 -0
- package/src/server/authenticate/admin/strategies/types.ts +29 -0
- package/src/server/authenticate/admin/types.ts +216 -0
- package/src/server/authenticate/const.ts +15 -0
- package/src/server/authenticate/flow/authenticate.flow.doc.example.ts +39 -0
- package/src/server/authenticate/flow/authenticate.flow.doc.ts +47 -0
- package/src/server/authenticate/flow/authenticate.ts +68 -0
- package/src/server/authenticate/flow/types.ts +85 -0
- package/src/server/authenticate/fulfillment-service/authenticate.fulfillment-service.doc.example.ts +52 -0
- package/src/server/authenticate/fulfillment-service/authenticate.fulfillment-service.doc.ts +48 -0
- package/src/server/authenticate/fulfillment-service/authenticate.ts +82 -0
- package/src/server/authenticate/fulfillment-service/types.ts +82 -0
- package/src/server/authenticate/helpers/add-response-headers.ts +43 -0
- package/src/server/authenticate/helpers/app-bridge-url.ts +10 -0
- package/src/server/authenticate/helpers/ensure-cors-headers.ts +38 -0
- package/src/server/authenticate/helpers/get-session-token-header.ts +11 -0
- package/src/server/authenticate/helpers/get-shop-from-request.ts +4 -0
- package/src/server/authenticate/helpers/idempotent-promise-handler.ts +45 -0
- package/src/server/authenticate/helpers/index.ts +10 -0
- package/src/server/authenticate/helpers/invalidate-access-token.ts +17 -0
- package/src/server/authenticate/helpers/reject-bot-request.ts +27 -0
- package/src/server/authenticate/helpers/respond-to-invalid-session-token.ts +28 -0
- package/src/server/authenticate/helpers/respond-to-options-request.ts +26 -0
- package/src/server/authenticate/helpers/validate-session-token.ts +40 -0
- package/src/server/authenticate/login/login.ts +53 -0
- package/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.example.ts +27 -0
- package/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.ts +55 -0
- package/src/server/authenticate/public/appProxy/authenticate.ts +150 -0
- package/src/server/authenticate/public/appProxy/types.ts +215 -0
- package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.example.offers.ts +21 -0
- package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.example.ts +17 -0
- package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.ts +49 -0
- package/src/server/authenticate/public/checkout/authenticate.ts +13 -0
- package/src/server/authenticate/public/checkout/types.ts +65 -0
- package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.example.offers.ts +36 -0
- package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.example.ts +18 -0
- package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.ts +49 -0
- package/src/server/authenticate/public/customer-account/authenticate.ts +13 -0
- package/src/server/authenticate/public/customer-account/types.ts +65 -0
- package/src/server/authenticate/public/extension/authenticate.ts +54 -0
- package/src/server/authenticate/public/extension/types.ts +11 -0
- package/src/server/authenticate/public/factory.ts +24 -0
- package/src/server/authenticate/public/index.ts +1 -0
- package/src/server/authenticate/public/pos/authenticate.ts +8 -0
- package/src/server/authenticate/public/pos/types.ts +65 -0
- package/src/server/authenticate/public/types.ts +96 -0
- package/src/server/authenticate/webhooks/authenticate.ts +105 -0
- package/src/server/authenticate/webhooks/authenticate.webhooks.doc.example.ts +43 -0
- package/src/server/authenticate/webhooks/authenticate.webhooks.doc.ts +39 -0
- package/src/server/authenticate/webhooks/index.ts +1 -0
- package/src/server/authenticate/webhooks/register.ts +48 -0
- package/src/server/authenticate/webhooks/types.ts +266 -0
- package/src/server/boundary/error.tsx +14 -0
- package/src/server/boundary/headers.ts +15 -0
- package/src/server/boundary/index.ts +39 -0
- package/src/server/boundary/types.ts +4 -0
- package/src/server/clients/admin/authenticate.admin.api.doc.ts +34 -0
- package/src/server/clients/admin/factory.ts +22 -0
- package/src/server/clients/admin/graphql.ts +39 -0
- package/src/server/clients/admin/index.ts +2 -0
- package/src/server/clients/admin/types.ts +120 -0
- package/src/server/clients/index.ts +2 -0
- package/src/server/clients/storefront/authenticate.storefront.api.doc.ts +34 -0
- package/src/server/clients/storefront/factory.ts +32 -0
- package/src/server/clients/storefront/index.ts +2 -0
- package/src/server/clients/storefront/types.ts +78 -0
- package/src/server/clients/types.ts +47 -0
- package/src/server/config-types.ts +309 -0
- package/src/server/errors.ts +3 -0
- package/src/server/future/flags.ts +62 -0
- package/src/server/helpers/create-or-load-offline-session.ts +19 -0
- package/src/server/helpers/ensure-offline-token-is-not-expired.ts +32 -0
- package/src/server/helpers/ensure-valid-offline-session.ts +15 -0
- package/src/server/helpers/index.ts +4 -0
- package/src/server/helpers/refresh-token.ts +35 -0
- package/src/server/index.ts +24 -0
- package/src/server/override-logger.ts +42 -0
- package/src/server/shopify-app.doc.ts +45 -0
- package/src/server/shopify-app.ts +215 -0
- package/src/server/test-helpers/index.ts +1 -0
- package/src/server/test-helpers/test-config.ts +79 -0
- package/src/server/types-contexts.ts +44 -0
- package/src/server/types.ts +507 -0
- package/src/server/unauthenticated/admin/factory.ts +23 -0
- package/src/server/unauthenticated/admin/index.ts +1 -0
- package/src/server/unauthenticated/admin/types.ts +79 -0
- package/src/server/unauthenticated/admin/unauthenticated.admin.doc.ts +40 -0
- package/src/server/unauthenticated/storefront/factory.ts +28 -0
- package/src/server/unauthenticated/storefront/index.ts +1 -0
- package/src/server/unauthenticated/storefront/types.ts +105 -0
- package/src/server/unauthenticated/storefront/unauthenticated.storefront.doc.ts +40 -0
- package/src/server/unauthenticated/types.ts +65 -0
- package/src/server/version.ts +1 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type {BasicParams} from '../../types';
|
|
2
|
+
|
|
3
|
+
import type {RegisterWebhooksOptions} from './types';
|
|
4
|
+
|
|
5
|
+
export function registerWebhooksFactory({api, logger}: BasicParams) {
|
|
6
|
+
return async function registerWebhooks({session}: RegisterWebhooksOptions) {
|
|
7
|
+
return api.webhooks
|
|
8
|
+
.register({session})
|
|
9
|
+
.then((response) => {
|
|
10
|
+
Object.entries(response).forEach(([topic, topicResults]) => {
|
|
11
|
+
topicResults.forEach(({success, ...rest}) => {
|
|
12
|
+
if (success) {
|
|
13
|
+
logger.debug('Registered webhook', {
|
|
14
|
+
topic,
|
|
15
|
+
shop: session.shop,
|
|
16
|
+
operation: rest.operation,
|
|
17
|
+
});
|
|
18
|
+
} else {
|
|
19
|
+
logger.error('Failed to register webhook', {
|
|
20
|
+
topic,
|
|
21
|
+
shop: session.shop,
|
|
22
|
+
result: JSON.stringify(rest.result),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return response;
|
|
29
|
+
})
|
|
30
|
+
.catch((error) => {
|
|
31
|
+
const graphQLErrors: {extensions: {code?: string}}[] =
|
|
32
|
+
error.body?.errors?.graphQLErrors || [];
|
|
33
|
+
|
|
34
|
+
const throttled = graphQLErrors.find(
|
|
35
|
+
({extensions: {code}}) => code === 'THROTTLED',
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (throttled) {
|
|
39
|
+
logger.error('Failed to register webhooks', {
|
|
40
|
+
shop: session.shop,
|
|
41
|
+
error: JSON.stringify(error),
|
|
42
|
+
});
|
|
43
|
+
} else {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import {Session, WebhookTypeValue} from '@shopify/shopify-api';
|
|
2
|
+
|
|
3
|
+
import type {AdminApiContext} from '../../clients';
|
|
4
|
+
|
|
5
|
+
export interface RegisterWebhooksOptions {
|
|
6
|
+
/**
|
|
7
|
+
* The Shopify session used to register webhooks using the Admin API.
|
|
8
|
+
*/
|
|
9
|
+
session: Session;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Context<Topics = string | number | symbol> {
|
|
13
|
+
/**
|
|
14
|
+
* The API version used for the webhook.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* <caption>Webhook API version.</caption>
|
|
18
|
+
* <description>Get the API version used for webhook request.</description>
|
|
19
|
+
* ```ts
|
|
20
|
+
* // /app/routes/webhooks.tsx
|
|
21
|
+
* import { ActionFunctionArgs } from "@remix-run/node";
|
|
22
|
+
* import { authenticate } from "../shopify.server";
|
|
23
|
+
*
|
|
24
|
+
* export const action = async ({ request }: ActionFunctionArgs) => {
|
|
25
|
+
* const { apiVersion } = await authenticate.webhook(request);
|
|
26
|
+
* return new Response();
|
|
27
|
+
* };
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
apiVersion: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The shop where the webhook was triggered.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* <caption>Webhook shop.</caption>
|
|
37
|
+
* <description>Get the shop that triggered a webhook.</description>
|
|
38
|
+
* ```ts
|
|
39
|
+
* // /app/routes/webhooks.tsx
|
|
40
|
+
* import { ActionFunctionArgs } from "@remix-run/node";
|
|
41
|
+
* import { authenticate } from "../shopify.server";
|
|
42
|
+
*
|
|
43
|
+
* export const action = async ({ request }: ActionFunctionArgs) => {
|
|
44
|
+
* const { shop } = await authenticate.webhook(request);
|
|
45
|
+
* return new Response();
|
|
46
|
+
* };
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
shop: string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The topic of the webhook.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* <caption>Webhook topic.</caption>
|
|
56
|
+
* <description>Get the event topic for the webhook.</description>
|
|
57
|
+
* ```ts
|
|
58
|
+
* // /app/routes/webhooks.tsx
|
|
59
|
+
* import { ActionFunctionArgs } from "@remix-run/node";
|
|
60
|
+
* import { authenticate } from "../shopify.server";
|
|
61
|
+
*
|
|
62
|
+
* export const action = async ({ request }: ActionFunctionArgs) => {
|
|
63
|
+
* const { topic } = await authenticate.webhook(request);
|
|
64
|
+
*
|
|
65
|
+
* switch (topic) {
|
|
66
|
+
* case "APP_UNINSTALLED":
|
|
67
|
+
* // Do something when the app is uninstalled.
|
|
68
|
+
* break;
|
|
69
|
+
* }
|
|
70
|
+
*
|
|
71
|
+
* return new Response();
|
|
72
|
+
* };
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
topic: Topics;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A unique ID for the webhook. Useful to keep track of which events your app has already processed.
|
|
79
|
+
*
|
|
80
|
+
* For events webhooks (`webhookType === 'events'`), this is set to the `eventId` value for
|
|
81
|
+
* backwards compatibility. Prefer using `eventId` directly for events webhooks — `webhookId`
|
|
82
|
+
* will be removed from events webhooks in the next major version.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* <caption>Webhook ID.</caption>
|
|
86
|
+
* <description>Get the webhook ID.</description>
|
|
87
|
+
* ```ts
|
|
88
|
+
* // /app/routes/webhooks.tsx
|
|
89
|
+
* import { ActionFunctionArgs } from "@remix-run/node";
|
|
90
|
+
* import { authenticate } from "../shopify.server";
|
|
91
|
+
*
|
|
92
|
+
* export const action = async ({ request }: ActionFunctionArgs) => {
|
|
93
|
+
* const { webhookId } = await authenticate.webhook(request);
|
|
94
|
+
* return new Response();
|
|
95
|
+
* };
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
webhookId: string;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The payload from the webhook request.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* <caption>Webhook payload.</caption>
|
|
105
|
+
* <description>Get the request's POST payload.</description>
|
|
106
|
+
* ```ts
|
|
107
|
+
* // /app/routes/webhooks.tsx
|
|
108
|
+
* import { ActionFunctionArgs } from "@remix-run/node";
|
|
109
|
+
* import { authenticate } from "../shopify.server";
|
|
110
|
+
*
|
|
111
|
+
* export const action = async ({ request }: ActionFunctionArgs) => {
|
|
112
|
+
* const { payload } = await authenticate.webhook(request);
|
|
113
|
+
* return new Response();
|
|
114
|
+
* };
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
payload: Record<string, any>;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The type of webhook: 'webhooks' for traditional webhooks or 'events' for events webhooks.
|
|
121
|
+
*/
|
|
122
|
+
webhookType: WebhookTypeValue;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The sub-topic of the webhook. Only available for traditional webhooks.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* <caption>Webhook sub-topic.</caption>
|
|
129
|
+
* <description>Get the webhook sub-topic.</description>
|
|
130
|
+
* ```ts
|
|
131
|
+
* // /app/routes/webhooks.tsx
|
|
132
|
+
* import { ActionFunctionArgs } from "@remix-run/node";
|
|
133
|
+
* import { authenticate } from "../shopify.server";
|
|
134
|
+
*
|
|
135
|
+
* export const action = async ({ request }: ActionFunctionArgs) => {
|
|
136
|
+
* const { subTopic } = await authenticate.webhook(request);
|
|
137
|
+
* return new Response();
|
|
138
|
+
* };
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
*/
|
|
142
|
+
subTopic?: string;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The name assigned to the webhook subscription. Only available for traditional webhooks.
|
|
146
|
+
*/
|
|
147
|
+
name?: string;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The handle for the webhook subscription. Only available for events webhooks.
|
|
151
|
+
*/
|
|
152
|
+
handle?: string;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The action type: 'create', 'update', or 'delete'. Only available for events webhooks.
|
|
156
|
+
*/
|
|
157
|
+
action?: string;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The GID of the resource that triggered the webhook. Only available for events webhooks.
|
|
161
|
+
*/
|
|
162
|
+
resourceId?: string;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The timestamp when the webhook was triggered.
|
|
166
|
+
*/
|
|
167
|
+
triggeredAt?: string;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The unique event identifier.
|
|
171
|
+
*/
|
|
172
|
+
eventId?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface WebhookContextWithoutSession<
|
|
176
|
+
Topics = string | number | symbol,
|
|
177
|
+
> extends Context<Topics> {
|
|
178
|
+
session: undefined;
|
|
179
|
+
admin: undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface WebhookContextWithSession<
|
|
183
|
+
Topics = string | number | symbol,
|
|
184
|
+
> extends Context<Topics> {
|
|
185
|
+
/**
|
|
186
|
+
* A session with an offline token for the shop.
|
|
187
|
+
*
|
|
188
|
+
* Returned only if there is a session for the shop.
|
|
189
|
+
* Webhook requests can trigger after an app is uninstalled
|
|
190
|
+
* If the app is already uninstalled, the session may be undefined.
|
|
191
|
+
* Therefore, you should check for the session before using it.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* <caption>Protecting against uninstalled apps.</caption>
|
|
195
|
+
* ```ts
|
|
196
|
+
* // /app/routes/webhooks.tsx
|
|
197
|
+
* import type { ActionFunctionArgs } from "@remix-run/node";
|
|
198
|
+
* import { authenticate } from "~/shopify.server";
|
|
199
|
+
|
|
200
|
+
* export const action = async ({ request }: ActionFunctionArgs) => {
|
|
201
|
+
* const { session } = await authenticate.webhook(request);
|
|
202
|
+
*
|
|
203
|
+
* // Webhook requests can trigger after an app is uninstalled
|
|
204
|
+
* // If the app is already uninstalled, the session may be undefined.
|
|
205
|
+
* if (!session) {
|
|
206
|
+
* throw new Response();
|
|
207
|
+
* }
|
|
208
|
+
*
|
|
209
|
+
* // Handle webhook request
|
|
210
|
+
* console.log("Received webhook webhook");
|
|
211
|
+
*
|
|
212
|
+
* return new Response();
|
|
213
|
+
* };
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
session: Session;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* An admin context for the webhook.
|
|
220
|
+
*
|
|
221
|
+
* Returned only if there is a session for the shop.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* <caption>Webhook admin context.</caption>
|
|
225
|
+
* <description>Use the `admin` object in the context to interact with the Admin API.</description>
|
|
226
|
+
* ```ts
|
|
227
|
+
* // /app/routes/webhooks.tsx
|
|
228
|
+
* import { ActionFunctionArgs } from "@remix-run/node";
|
|
229
|
+
* import { authenticate } from "../shopify.server";
|
|
230
|
+
*
|
|
231
|
+
* export async function action({ request }: ActionFunctionArgs) {
|
|
232
|
+
* const { admin } = await authenticate.webhook(request);
|
|
233
|
+
*
|
|
234
|
+
* // Webhook requests can trigger after an app is uninstalled
|
|
235
|
+
* // If the app is already uninstalled, the session may be undefined.
|
|
236
|
+
* if (!session) {
|
|
237
|
+
* throw new Response();
|
|
238
|
+
* }
|
|
239
|
+
*
|
|
240
|
+
* const response = await admin?.graphql(
|
|
241
|
+
* `#graphql
|
|
242
|
+
* mutation populateProduct($input: ProductInput!) {
|
|
243
|
+
* productCreate(input: $input) {
|
|
244
|
+
* product {
|
|
245
|
+
* id
|
|
246
|
+
* }
|
|
247
|
+
* }
|
|
248
|
+
* }`,
|
|
249
|
+
* { variables: { input: { title: "Product Name" } } }
|
|
250
|
+
* );
|
|
251
|
+
*
|
|
252
|
+
* const productData = await response.json();
|
|
253
|
+
* return json({ data: productData.data });
|
|
254
|
+
* }
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
admin: AdminApiContext;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export type WebhookContext<Topics = string | number | symbol> =
|
|
261
|
+
| WebhookContextWithoutSession<Topics>
|
|
262
|
+
| WebhookContextWithSession<Topics>;
|
|
263
|
+
|
|
264
|
+
export type AuthenticateWebhook<Topics = string | number | symbol> = (
|
|
265
|
+
request: Request,
|
|
266
|
+
) => Promise<WebhookContext<Topics>>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function errorBoundary(error: any) {
|
|
2
|
+
if (
|
|
3
|
+
error.constructor.name === 'ErrorResponse' ||
|
|
4
|
+
error.constructor.name === 'ErrorResponseImpl'
|
|
5
|
+
) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
dangerouslySetInnerHTML={{__html: error.data || 'Handling response'}}
|
|
9
|
+
/>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {HeadersArgs} from '@remix-run/server-runtime';
|
|
2
|
+
|
|
3
|
+
export function headersBoundary(headers: HeadersArgs): Headers {
|
|
4
|
+
const {parentHeaders, loaderHeaders, actionHeaders, errorHeaders} = headers;
|
|
5
|
+
|
|
6
|
+
if (errorHeaders && Array.from(errorHeaders.entries()).length > 0) {
|
|
7
|
+
return errorHeaders;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return new Headers([
|
|
11
|
+
...(parentHeaders ? Array.from(parentHeaders.entries()) : []),
|
|
12
|
+
...(loaderHeaders ? Array.from(loaderHeaders.entries()) : []),
|
|
13
|
+
...(actionHeaders ? Array.from(actionHeaders.entries()) : []),
|
|
14
|
+
]);
|
|
15
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {headersBoundary} from './headers';
|
|
2
|
+
import {errorBoundary} from './error';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A collection of functions that handle the necessary code for error boundaries in routes using authenticate.admin.
|
|
6
|
+
*/
|
|
7
|
+
export const boundary = {
|
|
8
|
+
/**
|
|
9
|
+
* A function that handles errors or thrown responses.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <caption>Catching errors in a route</caption>
|
|
13
|
+
* ```ts
|
|
14
|
+
* // /app/routes/admin/widgets.ts
|
|
15
|
+
* import { boundary } from "@shopify/shopify-app-remix/server";
|
|
16
|
+
*
|
|
17
|
+
* export function ErrorBoundary() {
|
|
18
|
+
* return boundary.error(useRouteError());
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
error: errorBoundary,
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A function that sets the appropriate document response headers.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* <caption>Catching errors in a route</caption>
|
|
29
|
+
* ```ts
|
|
30
|
+
* // /app/routes/admin/widgets.ts
|
|
31
|
+
* import { boundary } from "@shopify/shopify-app-remix/server";
|
|
32
|
+
*
|
|
33
|
+
* export const headers = (headersArgs) => {
|
|
34
|
+
* return boundary.headers(headersArgs);
|
|
35
|
+
* };
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
headers: headersBoundary,
|
|
39
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';
|
|
2
|
+
|
|
3
|
+
const data: ReferenceEntityTemplateSchema = {
|
|
4
|
+
name: 'Admin API',
|
|
5
|
+
description:
|
|
6
|
+
'Contains objects used to interact with the Admin API.' +
|
|
7
|
+
'\n\nThis object is returned as part of different contexts, such as [`admin`](/docs/api/shopify-app-remix/authenticate/admin), [`unauthenticated.admin`](/docs/api/shopify-app-remix/unauthenticated/unauthenticated-admin), and [`webhook`](/docs/api/shopify-app-remix/authenticate/webhook).',
|
|
8
|
+
category: 'APIs',
|
|
9
|
+
type: 'object',
|
|
10
|
+
isVisualComponent: false,
|
|
11
|
+
definitions: [
|
|
12
|
+
{
|
|
13
|
+
title: 'admin',
|
|
14
|
+
description:
|
|
15
|
+
'Provides utilities that apps can use to make requests to the Admin API.',
|
|
16
|
+
type: 'AdminApiContext',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
jsDocTypeExamples: ['AdminApiContext'],
|
|
20
|
+
related: [
|
|
21
|
+
{
|
|
22
|
+
name: 'Authenticated context',
|
|
23
|
+
subtitle: 'Authenticate requests from Shopify Admin.',
|
|
24
|
+
url: '/docs/api/shopify-app-remix/authenticate/admin',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'Unauthenticated context',
|
|
28
|
+
subtitle: 'Interact with the Admin API on non-Shopify requests.',
|
|
29
|
+
url: '/docs/api/shopify-app-remix/unauthenticated/unauthenticated-admin',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default data;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {Session} from '@shopify/shopify-api';
|
|
2
|
+
|
|
3
|
+
import {BasicParams} from '../../types';
|
|
4
|
+
|
|
5
|
+
import {graphqlClientFactory} from './graphql';
|
|
6
|
+
import type {AdminApiContext} from './types';
|
|
7
|
+
|
|
8
|
+
interface AdminClientOptions {
|
|
9
|
+
params: BasicParams;
|
|
10
|
+
session: Session;
|
|
11
|
+
handleClientError?: (error: any) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function adminClientFactory({
|
|
15
|
+
params,
|
|
16
|
+
handleClientError,
|
|
17
|
+
session,
|
|
18
|
+
}: AdminClientOptions): AdminApiContext {
|
|
19
|
+
return {
|
|
20
|
+
graphql: graphqlClientFactory({params, session, handleClientError}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {AdminOperations} from '@shopify/admin-api-client';
|
|
2
|
+
|
|
3
|
+
import {GraphQLClient} from '../types';
|
|
4
|
+
|
|
5
|
+
import {AdminClientOptions} from './types';
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line no-warning-comments
|
|
8
|
+
// TODO: This is actually just a call through to the Shopify API client, but with a different API. We should eventually
|
|
9
|
+
// move this over to the library layer.
|
|
10
|
+
export function graphqlClientFactory({
|
|
11
|
+
params,
|
|
12
|
+
handleClientError,
|
|
13
|
+
session,
|
|
14
|
+
}: AdminClientOptions): GraphQLClient<AdminOperations> {
|
|
15
|
+
return async function query(operation, options) {
|
|
16
|
+
const client = new params.api.clients.Graphql({
|
|
17
|
+
session,
|
|
18
|
+
apiVersion: options?.apiVersion,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// We convert the incoming response to a Response object to bring this client closer to the Remix client.
|
|
23
|
+
const apiResponse = await client.request(operation, {
|
|
24
|
+
variables: options?.variables,
|
|
25
|
+
retries: options?.tries ? options.tries - 1 : 0,
|
|
26
|
+
headers: options?.headers,
|
|
27
|
+
signal: options?.signal,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return new Response(JSON.stringify(apiResponse));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (handleClientError) {
|
|
33
|
+
throw await handleClientError({error, params, session});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {AdminOperations} from '@shopify/admin-api-client';
|
|
2
|
+
import {Session} from '@shopify/shopify-api';
|
|
3
|
+
|
|
4
|
+
import {BasicParams} from '../../types';
|
|
5
|
+
import {GraphQLClient} from '../types';
|
|
6
|
+
|
|
7
|
+
export interface AdminClientOptions {
|
|
8
|
+
params: BasicParams;
|
|
9
|
+
session: Session;
|
|
10
|
+
handleClientError?: HandleAdminClientError;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type HandleAdminClientError = (arg: {
|
|
14
|
+
error: any;
|
|
15
|
+
params: BasicParams;
|
|
16
|
+
session: Session;
|
|
17
|
+
}) => Promise<void>;
|
|
18
|
+
|
|
19
|
+
export interface AdminApiContext {
|
|
20
|
+
/**
|
|
21
|
+
* Methods for interacting with the Shopify Admin GraphQL API
|
|
22
|
+
*
|
|
23
|
+
* {@link https://shopify.dev/docs/api/admin-graphql}
|
|
24
|
+
* {@link https://github.com/Shopify/shopify-app-js/blob/main/packages/apps/shopify-api/docs/reference/clients/Graphql.md}
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* <caption>Querying the GraphQL API.</caption>
|
|
28
|
+
* <description>Use `admin.graphql` to make query / mutation requests.</description>
|
|
29
|
+
* ```ts
|
|
30
|
+
* // /app/routes/**\/*.ts
|
|
31
|
+
* import { ActionFunctionArgs } from "@remix-run/node";
|
|
32
|
+
* import { authenticate } from "../shopify.server";
|
|
33
|
+
*
|
|
34
|
+
* export const action = async ({ request }: ActionFunctionArgs) => {
|
|
35
|
+
* const { admin } = await authenticate.admin(request);
|
|
36
|
+
*
|
|
37
|
+
* const response = await admin.graphql(
|
|
38
|
+
* `#graphql
|
|
39
|
+
* mutation populateProduct($input: ProductInput!) {
|
|
40
|
+
* productCreate(input: $input) {
|
|
41
|
+
* product {
|
|
42
|
+
* id
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* }`,
|
|
46
|
+
* {
|
|
47
|
+
* variables: {
|
|
48
|
+
* input: { title: "Product Name" },
|
|
49
|
+
* },
|
|
50
|
+
* },
|
|
51
|
+
* );
|
|
52
|
+
*
|
|
53
|
+
* const productData = await response.json();
|
|
54
|
+
* return json({
|
|
55
|
+
* productId: productData.data?.productCreate?.product?.id,
|
|
56
|
+
* });
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* // /app/shopify.server.ts
|
|
62
|
+
* import { shopifyApp } from "@shopify/shopify-app-remix/server";
|
|
63
|
+
*
|
|
64
|
+
* const shopify = shopifyApp({
|
|
65
|
+
* // ...
|
|
66
|
+
* });
|
|
67
|
+
* export default shopify;
|
|
68
|
+
* export const authenticate = shopify.authenticate;
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* <caption>Handling GraphQL errors.</caption>
|
|
73
|
+
* <description>Catch `GraphqlQueryError` errors to see error messages from the API.</description>
|
|
74
|
+
* ```ts
|
|
75
|
+
* // /app/routes/**\/*.ts
|
|
76
|
+
* import { ActionFunctionArgs } from "@remix-run/node";
|
|
77
|
+
* import { authenticate } from "../shopify.server";
|
|
78
|
+
*
|
|
79
|
+
* export const action = async ({ request }: ActionFunctionArgs) => {
|
|
80
|
+
* const { admin } = await authenticate.admin(request);
|
|
81
|
+
*
|
|
82
|
+
* try {
|
|
83
|
+
* const response = await admin.graphql(
|
|
84
|
+
* `#graphql
|
|
85
|
+
* query incorrectQuery {
|
|
86
|
+
* products(first: 10) {
|
|
87
|
+
* nodes {
|
|
88
|
+
* not_a_field
|
|
89
|
+
* }
|
|
90
|
+
* }
|
|
91
|
+
* }`,
|
|
92
|
+
* );
|
|
93
|
+
*
|
|
94
|
+
* return json({ data: await response.json() });
|
|
95
|
+
* } catch (error) {
|
|
96
|
+
* if (error instanceof GraphqlQueryError) {
|
|
97
|
+
* // error.body.errors:
|
|
98
|
+
* // { graphQLErrors: [
|
|
99
|
+
* // { message: "Field 'not_a_field' doesn't exist on type 'Product'" }
|
|
100
|
+
* // ] }
|
|
101
|
+
* return json({ errors: error.body?.errors }, { status: 500 });
|
|
102
|
+
* }
|
|
103
|
+
* return json({ message: "An error occurred" }, { status: 500 });
|
|
104
|
+
* }
|
|
105
|
+
* }
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* ```ts
|
|
109
|
+
* // /app/shopify.server.ts
|
|
110
|
+
* import { shopifyApp } from "@shopify/shopify-app-remix/server";
|
|
111
|
+
*
|
|
112
|
+
* const shopify = shopifyApp({
|
|
113
|
+
* // ...
|
|
114
|
+
* });
|
|
115
|
+
* export default shopify;
|
|
116
|
+
* export const authenticate = shopify.authenticate;
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
graphql: GraphQLClient<AdminOperations>;
|
|
120
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';
|
|
2
|
+
|
|
3
|
+
const data: ReferenceEntityTemplateSchema = {
|
|
4
|
+
name: 'Storefront API',
|
|
5
|
+
description:
|
|
6
|
+
'Contains objects used to interact with the Storefront API.' +
|
|
7
|
+
'\n\nThis object is returned as part of different contexts, such as [`appProxy`](/docs/api/shopify-app-remix/authenticate/public/app-proxy), and [`unauthenticated.storefront`](/docs/api/shopify-app-remix/unauthenticated/unauthenticated-storefront).',
|
|
8
|
+
category: 'APIs',
|
|
9
|
+
type: 'object',
|
|
10
|
+
isVisualComponent: false,
|
|
11
|
+
definitions: [
|
|
12
|
+
{
|
|
13
|
+
title: 'storefront',
|
|
14
|
+
description:
|
|
15
|
+
'Provides utilities that apps can use to make requests to the Storefront API.',
|
|
16
|
+
type: 'StorefrontContext',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
jsDocTypeExamples: ['StorefrontContext'],
|
|
20
|
+
related: [
|
|
21
|
+
{
|
|
22
|
+
name: 'App proxy context',
|
|
23
|
+
subtitle: 'Authenticate requests from Shopify app proxies.',
|
|
24
|
+
url: '/docs/api/shopify-app-remix/authenticate/public/app-proxy',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'Unauthenticated context',
|
|
28
|
+
subtitle: 'Interact with the Storefront API on non-Shopify requests.',
|
|
29
|
+
url: '/docs/api/shopify-app-remix/unauthenticated/unauthenticated-storefront',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default data;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {Session} from '@shopify/shopify-api';
|
|
2
|
+
|
|
3
|
+
import {BasicParams} from '../../types';
|
|
4
|
+
|
|
5
|
+
import type {StorefrontContext} from '.';
|
|
6
|
+
|
|
7
|
+
export function storefrontClientFactory({
|
|
8
|
+
params,
|
|
9
|
+
session,
|
|
10
|
+
}: {
|
|
11
|
+
params: BasicParams;
|
|
12
|
+
session: Session;
|
|
13
|
+
}): StorefrontContext {
|
|
14
|
+
const {api} = params;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
graphql: async (query, options = {}) => {
|
|
18
|
+
const client = new api.clients.Storefront({
|
|
19
|
+
session,
|
|
20
|
+
apiVersion: options.apiVersion,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const apiResponse = await client.request(query, {
|
|
24
|
+
variables: options?.variables,
|
|
25
|
+
retries: options?.tries ? options.tries - 1 : 0,
|
|
26
|
+
headers: options?.headers,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return new Response(JSON.stringify(apiResponse));
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|